ensemble-analyzer 1__tar.gz → 1a0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ensemble_analyzer-1/src/ensemble_analyzer.egg-info → ensemble_analyzer-1a0}/PKG-INFO +1 -1
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/pyproject.toml +2 -2
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_clustering/cluster_config.py +2 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_clustering/cluster_manager.py +7 -3
- ensemble_analyzer-1a0/src/ensemble_analyzer/cli/clustering.py +407 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/ensemble_io.py +5 -2
- {ensemble_analyzer-1 → ensemble_analyzer-1a0/src/ensemble_analyzer.egg-info}/PKG-INFO +1 -1
- ensemble_analyzer-1/src/ensemble_analyzer/cli/clustering.py +0 -153
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/LICENSE +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/README.md +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/setup.cfg +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/__init__.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/__main__.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_calculators/__init__.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_calculators/_gaussian.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_calculators/_orca.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_calculators/base.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_clustering/__init__.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_conformer/__init__.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_conformer/conformer.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_conformer/energy_data.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_conformer/spectral_data.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_logger/__init__.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_logger/create_log.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_logger/formatter.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_logger/logger.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_managers/__init__.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_managers/calculation_config.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_managers/calculation_executor.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_managers/calculator_orchestration.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_managers/checkpoint_manager.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_managers/protocol_executor.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_managers/protocol_manager.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_managers/pruning_manager.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_parsers/__init__.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_parsers/_gaussian.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_parsers/_orca.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_parsers/base.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_protocol/__init__.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_protocol/protocol.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_protocol/solvent.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_spectral/__init__.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_spectral/base.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_spectral/comp_electronic.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_spectral/comp_vibronic.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_spectral/compare.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_spectral/experimental.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_spectral/graph_default.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/cli/__init__.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/cli/check_setup.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/cli/get_av_energy.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/cli/pickle_editor/__init__.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/cli/pickle_editor/cli.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/cli/pickle_editor/core.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/cli/pickle_editor/tui.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/cli/protocol_wizard.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/cli/regraph.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/clustering.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/constants.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/graph.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/io_utils.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/launch.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/parameters_file/basis_sets +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/parameters_file/default_protocol.json +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/parameters_file/default_threshold.json +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/parameters_file/functionals +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/parser_arguments.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/parser_parameter.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/rrho.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/title.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer.egg-info/SOURCES.txt +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer.egg-info/dependency_links.txt +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer.egg-info/entry_points.txt +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer.egg-info/requires.txt +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer.egg-info/top_level.txt +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/test/test_calculation_executor.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/test/test_calculator.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/test/test_checkpoint_manager.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/test/test_clustering.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/test/test_config.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/test/test_conformer.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/test/test_data_structure.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/test/test_ensemble_io.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/test/test_experimental.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/test/test_high_level.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/test/test_integration.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/test/test_io_utils.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/test/test_logger.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/test/test_parser_parameters.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/test/test_parsers.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/test/test_protocol_executor.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/test/test_pruning_manager.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/test/test_rrho.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/test/test_spectral.py +0 -0
- {ensemble_analyzer-1 → ensemble_analyzer-1a0}/test/test_spectral_compare.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ensemble-analyzer
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1a0
|
|
4
4
|
Summary: Automatic Refinement of Conformational Ensemble
|
|
5
5
|
Author-email: Andrea Pellegrini <andrea.pellegrini15@unibo.it>, Paolo Righi <paolo.righi@unibo.it>, Andrea Mazzanti <andrea.mazzanti@unibo.it>, Michele Mancinelli <michele.mancinelli@unibo.it>
|
|
6
6
|
Maintainer-email: Andrea Pellegrini <andrea.pellegrini15@unibo.it>, Paolo Righi <paolo.righi@unibo.it>
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ensemble-analyzer"
|
|
7
|
-
version = '
|
|
7
|
+
version = '1a'
|
|
8
8
|
description = "Automatic Refinement of Conformational Ensemble"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -76,4 +76,4 @@ ensemble_analyzer = ["parameters_file/*"]
|
|
|
76
76
|
|
|
77
77
|
[project.urls]
|
|
78
78
|
Homepage = "https://github.com/andre-cloud/ensemble_analyzer"
|
|
79
|
-
Documentation = "https://enan.readthedocs.io/en/latest/"
|
|
79
|
+
Documentation = "https://enan.readthedocs.io/en/latest/"
|
{ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_clustering/cluster_config.py
RENAMED
|
@@ -16,6 +16,8 @@ class PCAResult:
|
|
|
16
16
|
energies: np.ndarray # Relative energies array
|
|
17
17
|
explained_variance: np.ndarray # Variance ratio per component
|
|
18
18
|
n_clusters: Optional[int] = None # Number of clusters found/used
|
|
19
|
+
original_features: Optional[np.ndarray] = None # Added for before/after plot
|
|
20
|
+
components: Optional[np.ndarray] = None # Added for loading plot
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
@dataclass
|
{ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_clustering/cluster_manager.py
RENAMED
|
@@ -142,7 +142,7 @@ class ClusteringManager:
|
|
|
142
142
|
ensemble = sorted(conformers) if sort_by_energy else conformers[:]
|
|
143
143
|
|
|
144
144
|
# Track which clusters we've seen
|
|
145
|
-
seen_clusters =
|
|
145
|
+
seen_clusters = []
|
|
146
146
|
deactivated_count = 0
|
|
147
147
|
|
|
148
148
|
for conf in ensemble:
|
|
@@ -154,7 +154,7 @@ class ClusteringManager:
|
|
|
154
154
|
conf.active = False
|
|
155
155
|
deactivated_count += 1
|
|
156
156
|
else:
|
|
157
|
-
seen_clusters.
|
|
157
|
+
seen_clusters.append(conf.cluster)
|
|
158
158
|
|
|
159
159
|
active_count = len([c for c in ensemble if c.active])
|
|
160
160
|
|
|
@@ -229,6 +229,7 @@ class ClusteringManager:
|
|
|
229
229
|
kmeans = KMeans(
|
|
230
230
|
n_clusters=n_clusters,
|
|
231
231
|
n_init='auto',
|
|
232
|
+
random_state=self.config.random_state
|
|
232
233
|
)
|
|
233
234
|
cluster_labels = kmeans.fit_predict(pca_scores)
|
|
234
235
|
|
|
@@ -244,7 +245,9 @@ class ClusteringManager:
|
|
|
244
245
|
numbers=numbers,
|
|
245
246
|
energies=energies,
|
|
246
247
|
explained_variance=pca.explained_variance_ratio_,
|
|
247
|
-
n_clusters=n_clusters
|
|
248
|
+
n_clusters=n_clusters,
|
|
249
|
+
original_features=eigenvalues,
|
|
250
|
+
components=pca.components_
|
|
248
251
|
)
|
|
249
252
|
|
|
250
253
|
def _calculate_distance_matrix_eigenvalues(
|
|
@@ -314,6 +317,7 @@ class ClusteringManager:
|
|
|
314
317
|
kmeans = KMeans(
|
|
315
318
|
n_clusters=k,
|
|
316
319
|
n_init='auto',
|
|
320
|
+
random_state=self.config.random_state
|
|
317
321
|
)
|
|
318
322
|
labels = kmeans.fit_predict(features)
|
|
319
323
|
score = silhouette_score(features, labels)
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
import numpy as np
|
|
5
|
+
from sklearn.cluster import KMeans
|
|
6
|
+
from sklearn.metrics import silhouette_score
|
|
7
|
+
from unittest.mock import Mock
|
|
8
|
+
|
|
9
|
+
from ensemble_analyzer.ensemble_io import read_ensemble, save_snapshot
|
|
10
|
+
from ensemble_analyzer.clustering import *
|
|
11
|
+
from ensemble_analyzer._clustering.cluster_config import ClusteringConfig
|
|
12
|
+
from ensemble_analyzer._clustering.cluster_manager import ClusteringManager
|
|
13
|
+
|
|
14
|
+
def parse_args():
|
|
15
|
+
parser = argparse.ArgumentParser(
|
|
16
|
+
description='Perform PCA analysis and clustering on conformer ensemble',
|
|
17
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
18
|
+
epilog="""
|
|
19
|
+
Examples:
|
|
20
|
+
# Basic PCA with 5 clusters
|
|
21
|
+
python clustering.py ensemble.xyz -nc 5
|
|
22
|
+
# Auto-detect optimal clusters, exclude hydrogen
|
|
23
|
+
python clustering.py ensemble.xyz --no-H
|
|
24
|
+
# Save reduced ensemble
|
|
25
|
+
python clustering.py ensemble.xyz -nc 10 --save-reduced
|
|
26
|
+
# Custom output and title
|
|
27
|
+
python clustering.py ensemble.xyz -o my_pca.png --title "Drug Conformers"
|
|
28
|
+
"""
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
parser.add_argument('file', help='Input ensemble file (XYZ format)')
|
|
32
|
+
parser.add_argument('-nc', '--ncluster', type=int, default=None,
|
|
33
|
+
help='Number of clusters (default: auto-detect using silhouette score)')
|
|
34
|
+
parser.add_argument('--no-H', action='store_false', dest='include_H',
|
|
35
|
+
help='Exclude hydrogen atoms from distance matrix calculation')
|
|
36
|
+
parser.add_argument('--no-legend', action='store_false', dest='legend',
|
|
37
|
+
help='Exclude conformer legend from plot')
|
|
38
|
+
parser.add_argument('--title', default='PCA Cluster Analysis',
|
|
39
|
+
help='Plot title (default: "PCA Cluster Analysis")')
|
|
40
|
+
parser.add_argument('-o', '--output', default='cluster.png',
|
|
41
|
+
help='Output image filename (default: cluster.png)')
|
|
42
|
+
parser.add_argument('--write', action='store_true',
|
|
43
|
+
help='Save cluster-reduced ensemble to clustered.xyz')
|
|
44
|
+
parser.add_argument('--dpi', type=int, default=300,
|
|
45
|
+
help='Output image DPI (default: 300)')
|
|
46
|
+
parser.add_argument('-v', '--verbose', action='store_true',
|
|
47
|
+
help='Enable verbose output')
|
|
48
|
+
|
|
49
|
+
args = parser.parse_args()
|
|
50
|
+
return args
|
|
51
|
+
|
|
52
|
+
class CLILogger:
|
|
53
|
+
def __init__(self, verbose=False):
|
|
54
|
+
self.verbose = verbose
|
|
55
|
+
|
|
56
|
+
def info(self, msg):
|
|
57
|
+
print(f"ℹ️ {msg}")
|
|
58
|
+
|
|
59
|
+
def debug(self, msg):
|
|
60
|
+
if self.verbose:
|
|
61
|
+
print(f"🔍 {msg}")
|
|
62
|
+
|
|
63
|
+
def warning(self, msg):
|
|
64
|
+
print(f"⚠️ {msg}")
|
|
65
|
+
|
|
66
|
+
def error(self, msg):
|
|
67
|
+
print(f"❌ {msg}")
|
|
68
|
+
|
|
69
|
+
def plot_component_analysis(result, output_base, logger):
|
|
70
|
+
"""
|
|
71
|
+
Generate loading plot showing feature contributions to principal components.
|
|
72
|
+
"""
|
|
73
|
+
out_dir = os.path.dirname(output_base) or "."
|
|
74
|
+
base_name = os.path.splitext(os.path.basename(output_base))[0]
|
|
75
|
+
|
|
76
|
+
logger.info("Generating component loading analysis...")
|
|
77
|
+
|
|
78
|
+
if getattr(result, 'components', None) is None:
|
|
79
|
+
logger.warning("No component loadings available, skipping loading plot")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
components = result.components[:2] # First 2 PCs
|
|
83
|
+
n_features = components.shape[1]
|
|
84
|
+
|
|
85
|
+
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
|
|
86
|
+
|
|
87
|
+
for idx, (ax, pc) in enumerate(zip(axes, components)):
|
|
88
|
+
pc_variance = result.explained_variance[idx] * 100
|
|
89
|
+
feature_indices = np.arange(n_features)
|
|
90
|
+
colors = ['#2E86AB' if x >= 0 else '#A23B72' for x in pc]
|
|
91
|
+
|
|
92
|
+
ax.bar(feature_indices, pc, color=colors, alpha=0.7, edgecolor='black', linewidth=0.5)
|
|
93
|
+
ax.axhline(y=0, color='black', linestyle='-', linewidth=0.8)
|
|
94
|
+
ax.set_xlabel('Feature Index', fontsize=10)
|
|
95
|
+
ax.set_ylabel('Loading Value', fontsize=10)
|
|
96
|
+
ax.set_title(f'PC{idx+1} Loadings ({pc_variance:.1f}% variance)', fontsize=11, fontweight='bold')
|
|
97
|
+
ax.grid(True, alpha=0.3, axis='y')
|
|
98
|
+
|
|
99
|
+
# Highlight top contributors
|
|
100
|
+
top_indices = np.argsort(np.abs(pc))[-5:]
|
|
101
|
+
for top_idx in top_indices:
|
|
102
|
+
ax.text(top_idx, pc[top_idx], f'{top_idx}',
|
|
103
|
+
ha='center', va='bottom' if pc[top_idx] > 0 else 'top',
|
|
104
|
+
fontsize=8, fontweight='bold')
|
|
105
|
+
|
|
106
|
+
plt.tight_layout()
|
|
107
|
+
loading_file = os.path.join(out_dir, f"{base_name}_loadings.png")
|
|
108
|
+
plt.savefig(loading_file, dpi=300, bbox_inches='tight')
|
|
109
|
+
plt.close()
|
|
110
|
+
logger.info(f"✓ Component loadings saved: {loading_file}")
|
|
111
|
+
|
|
112
|
+
def plot_before_after_pca(result, output_base, logger):
|
|
113
|
+
"""
|
|
114
|
+
Side-by-side comparison of data before and after PCA transformation,
|
|
115
|
+
with PCA axes drawn in the original space.
|
|
116
|
+
"""
|
|
117
|
+
out_dir = os.path.dirname(output_base) or "."
|
|
118
|
+
base_name = os.path.splitext(os.path.basename(output_base))[0]
|
|
119
|
+
|
|
120
|
+
logger.info("Generating before/after PCA comparison...")
|
|
121
|
+
|
|
122
|
+
if getattr(result, 'original_features', None) is None:
|
|
123
|
+
logger.warning("Original features not available, skipping before/after plot")
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
original = result.original_features
|
|
127
|
+
transformed = result.scores
|
|
128
|
+
labels = result.clusters
|
|
129
|
+
|
|
130
|
+
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
|
|
131
|
+
|
|
132
|
+
unique_labels = np.unique(labels)
|
|
133
|
+
colors = plt.cm.tab10(np.linspace(0, 1, len(unique_labels)))
|
|
134
|
+
|
|
135
|
+
# Plot 1: Original space (first 2 features)
|
|
136
|
+
ax1 = axes[0]
|
|
137
|
+
for idx, label in enumerate(unique_labels):
|
|
138
|
+
mask = labels == label
|
|
139
|
+
ax1.scatter(original[mask, 0], original[mask, 1],
|
|
140
|
+
c=[colors[idx]],
|
|
141
|
+
alpha=0.6, s=50, edgecolors='black', linewidth=0.5)
|
|
142
|
+
|
|
143
|
+
# Draw PCA axes in the original space if components are available
|
|
144
|
+
if getattr(result, 'components', None) is not None:
|
|
145
|
+
# Calculate the center of the original data (since PCA is centered)
|
|
146
|
+
mean_0 = np.mean(original[:, 0])
|
|
147
|
+
mean_1 = np.mean(original[:, 1])
|
|
148
|
+
|
|
149
|
+
# Extract the projection of the first two PCs onto the first two original features
|
|
150
|
+
v1_0, v1_1 = result.components[0, 0], result.components[0, 1]
|
|
151
|
+
v2_0, v2_1 = result.components[1, 0], result.components[1, 1]
|
|
152
|
+
|
|
153
|
+
# Scale vectors based on the standard deviation of the scores
|
|
154
|
+
scale1 = np.std(transformed[:, 0]) * 2
|
|
155
|
+
scale2 = np.std(transformed[:, 1]) * 2
|
|
156
|
+
|
|
157
|
+
# Plot PC1 Axis (Red)
|
|
158
|
+
ax1.plot([mean_0 - v1_0*scale1, mean_0 + v1_0*scale1],
|
|
159
|
+
[mean_1 - v1_1*scale1, mean_1 + v1_1*scale1],
|
|
160
|
+
color='red', linewidth=2, linestyle='--')
|
|
161
|
+
ax1.text(mean_0 + v1_0*scale1, mean_1 + v1_1*scale1, 'PC1', color='red', fontsize=12, fontweight='bold')
|
|
162
|
+
|
|
163
|
+
# Plot PC2 Axis (Green)
|
|
164
|
+
ax1.plot([mean_0 - v2_0*scale2, mean_0 + v2_0*scale2],
|
|
165
|
+
[mean_1 - v2_1*scale2, mean_1 + v2_1*scale2],
|
|
166
|
+
color='green', linewidth=2, linestyle='--')
|
|
167
|
+
ax1.text(mean_0 + v2_0*scale2, mean_1 + v2_1*scale2, 'PC2', color='green', fontsize=12, fontweight='bold')
|
|
168
|
+
|
|
169
|
+
ax1.set_xlabel('Feature 1 (Original Space)', fontsize=11)
|
|
170
|
+
ax1.set_ylabel('Feature 2 (Original Space)', fontsize=11)
|
|
171
|
+
ax1.set_title('Original Feature Space with PC Axes', fontsize=12, fontweight='bold')
|
|
172
|
+
ax1.grid(True, alpha=0.3)
|
|
173
|
+
|
|
174
|
+
# Plot 2: PCA space (PC1 vs PC2)
|
|
175
|
+
ax2 = axes[1]
|
|
176
|
+
for idx, label in enumerate(unique_labels):
|
|
177
|
+
mask = labels == label
|
|
178
|
+
ax2.scatter(transformed[mask, 0], transformed[mask, 1],
|
|
179
|
+
c=[colors[idx]],
|
|
180
|
+
alpha=0.6, s=50, edgecolors='black', linewidth=0.5)
|
|
181
|
+
|
|
182
|
+
var1, var2 = result.explained_variance[0] * 100, result.explained_variance[1] * 100
|
|
183
|
+
ax2.set_xlabel(f'PC1 ({var1:.1f}% variance)', fontsize=11)
|
|
184
|
+
ax2.set_ylabel(f'PC2 ({var2:.1f}% variance)', fontsize=11)
|
|
185
|
+
ax2.set_title(f'PCA-Transformed Space', fontsize=12, fontweight='bold')
|
|
186
|
+
ax2.grid(True, alpha=0.3)
|
|
187
|
+
|
|
188
|
+
plt.tight_layout()
|
|
189
|
+
comparison_file = os.path.join(out_dir, f"{base_name}_before_after.png")
|
|
190
|
+
plt.savefig(comparison_file, dpi=300, bbox_inches='tight')
|
|
191
|
+
plt.close()
|
|
192
|
+
logger.info(f"✓ Before/After comparison saved: {comparison_file}")
|
|
193
|
+
|
|
194
|
+
def plot_clustering_metrics(result, output_base, logger):
|
|
195
|
+
"""
|
|
196
|
+
Generate evaluation plots: Scree plot with cumulative variance and Silhouette scores.
|
|
197
|
+
"""
|
|
198
|
+
out_dir = os.path.dirname(output_base) or "."
|
|
199
|
+
base_name = os.path.splitext(os.path.basename(output_base))[0]
|
|
200
|
+
|
|
201
|
+
logger.info("Generating clustering metric plots...")
|
|
202
|
+
|
|
203
|
+
variance = result.explained_variance * 100
|
|
204
|
+
cumulative_variance = np.cumsum(variance)
|
|
205
|
+
n_components = len(variance)
|
|
206
|
+
|
|
207
|
+
fig, ax1 = plt.subplots(figsize=(10, 6))
|
|
208
|
+
|
|
209
|
+
x_pos = np.arange(1, n_components + 1)
|
|
210
|
+
bars = ax1.bar(x_pos, variance, alpha=0.7, color='#3498db',
|
|
211
|
+
edgecolor='black', linewidth=0.8, label='Individual Variance')
|
|
212
|
+
|
|
213
|
+
ax1.set_xlabel('Principal Component', fontsize=12, fontweight='bold')
|
|
214
|
+
ax1.set_ylabel('Explained Variance (%)', fontsize=12, fontweight='bold', color='#3498db')
|
|
215
|
+
ax1.tick_params(axis='y', labelcolor='#3498db')
|
|
216
|
+
# ax1.set_xticks(x_pos)
|
|
217
|
+
ax1.grid(True, alpha=0.3, axis='y', linestyle='--')
|
|
218
|
+
|
|
219
|
+
ax2 = ax1.twinx()
|
|
220
|
+
line = ax2.plot(x_pos, cumulative_variance, color='#e74c3c', marker='o',
|
|
221
|
+
linewidth=2.5, markersize=8, label='Cumulative Variance')
|
|
222
|
+
|
|
223
|
+
ax2.set_ylabel('Cumulative Variance (%)', fontsize=12, fontweight='bold', color='#e74c3c')
|
|
224
|
+
ax2.tick_params(axis='y', labelcolor='#e74c3c')
|
|
225
|
+
ax2.set_ylim([0, 105])
|
|
226
|
+
|
|
227
|
+
ax2.axhline(y=90, color='gray', linestyle='--', linewidth=1.5, alpha=0.7, label='90% threshold')
|
|
228
|
+
|
|
229
|
+
for i, (bar, val) in enumerate(zip(bars, variance)):
|
|
230
|
+
if i < 5:
|
|
231
|
+
height = bar.get_height()
|
|
232
|
+
ax1.text(bar.get_x() + bar.get_width()/2., height,
|
|
233
|
+
f'{val:.1f}%', ha='center', va='bottom', fontsize=9, fontweight='bold')
|
|
234
|
+
|
|
235
|
+
lines1, labels1 = ax1.get_legend_handles_labels()
|
|
236
|
+
lines2, labels2 = ax2.get_legend_handles_labels()
|
|
237
|
+
ax1.legend(lines1 + lines2, labels1 + labels2, loc='lower right', fontsize=10)
|
|
238
|
+
|
|
239
|
+
plt.title('Scree Plot: Explained Variance per Component', fontsize=13, fontweight='bold', pad=15)
|
|
240
|
+
plt.tight_layout()
|
|
241
|
+
|
|
242
|
+
scree_file = os.path.join(out_dir, f"{base_name}_scree.png")
|
|
243
|
+
plt.savefig(scree_file, dpi=300, bbox_inches='tight')
|
|
244
|
+
plt.close()
|
|
245
|
+
logger.info(f"✓ Scree plot saved: {scree_file}")
|
|
246
|
+
|
|
247
|
+
features = result.scores
|
|
248
|
+
min_k = max(int(len(features) * 0.1), 2)
|
|
249
|
+
max_k = int(len(features) * 0.8)
|
|
250
|
+
|
|
251
|
+
if max_k > min_k:
|
|
252
|
+
k_range = range(min_k, max_k + 1)
|
|
253
|
+
scores = []
|
|
254
|
+
for k in k_range:
|
|
255
|
+
kmeans = KMeans(n_clusters=k, n_init='auto', random_state=500)
|
|
256
|
+
labels = kmeans.fit_predict(features)
|
|
257
|
+
scores.append(silhouette_score(features, labels))
|
|
258
|
+
|
|
259
|
+
plt.figure(figsize=(8, 5))
|
|
260
|
+
plt.plot(k_range, scores, marker='s', color='orange')
|
|
261
|
+
plt.axvline(
|
|
262
|
+
x=result.n_clusters, color='red', linestyle='--',
|
|
263
|
+
label=f'Selected k={result.n_clusters}'
|
|
264
|
+
)
|
|
265
|
+
plt.title('Silhouette Score vs Number of Clusters')
|
|
266
|
+
plt.xlabel('Number of Clusters (k)')
|
|
267
|
+
plt.ylabel('Silhouette Score')
|
|
268
|
+
plt.legend()
|
|
269
|
+
plt.grid(True, alpha=0.3)
|
|
270
|
+
|
|
271
|
+
sil_file = os.path.join(out_dir, f"{base_name}_silhouette.png")
|
|
272
|
+
plt.savefig(sil_file, dpi=300, bbox_inches='tight')
|
|
273
|
+
plt.close()
|
|
274
|
+
logger.info(f"✓ Silhouette plot saved: {sil_file}")
|
|
275
|
+
else:
|
|
276
|
+
logger.warning("Not enough data points to generate Silhouette plot")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def plot_3d_original_space(result, output_base, logger):
|
|
280
|
+
"""
|
|
281
|
+
Generate a 3D scatter plot of the first 3 original features to illustrate
|
|
282
|
+
the complexity of the data before PCA transformation.
|
|
283
|
+
"""
|
|
284
|
+
out_dir = os.path.dirname(output_base) or "."
|
|
285
|
+
base_name = os.path.splitext(os.path.basename(output_base))[0]
|
|
286
|
+
|
|
287
|
+
logger.info("Generating 3D original space plot...")
|
|
288
|
+
|
|
289
|
+
if getattr(result, 'original_features', None) is None or result.original_features.shape[1] < 3:
|
|
290
|
+
logger.warning("Not enough original features available for 3D plot, skipping")
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
original = result.original_features
|
|
294
|
+
labels = result.clusters
|
|
295
|
+
|
|
296
|
+
fig, ax = plt.subplots(figsize=(5, 6))
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
unique_labels = np.unique(labels)
|
|
300
|
+
colors = plt.cm.tab10(np.linspace(0, 1, len(unique_labels)))
|
|
301
|
+
|
|
302
|
+
for idx, label in enumerate(unique_labels):
|
|
303
|
+
mask = labels == label
|
|
304
|
+
ax.scatter(
|
|
305
|
+
original[mask, 0],
|
|
306
|
+
original[mask, 1],
|
|
307
|
+
c=[colors[idx]],
|
|
308
|
+
# label=f'Cluster {label}',
|
|
309
|
+
alpha=0.7,
|
|
310
|
+
s=40,
|
|
311
|
+
edgecolors='black',
|
|
312
|
+
linewidth=0.5
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
ax.set_xlabel('Feature 1', fontsize=10)
|
|
316
|
+
ax.set_ylabel('Feature 2', fontsize=10)
|
|
317
|
+
# ax.set_zlabel('Feature 3', fontsize=10)
|
|
318
|
+
ax.set_title('Original Features (Pre-PCA)', fontsize=12, fontweight='bold')
|
|
319
|
+
ax.grid()
|
|
320
|
+
# Imposta un angolo di visualizzazione iniziale
|
|
321
|
+
# ax.view_init(elev=20, azim=45)
|
|
322
|
+
|
|
323
|
+
plt.tight_layout()
|
|
324
|
+
plot_file = os.path.join(out_dir, f"{base_name}_3d_original.png")
|
|
325
|
+
plt.savefig(plot_file, dpi=300, bbox_inches='tight')
|
|
326
|
+
plt.close()
|
|
327
|
+
|
|
328
|
+
logger.info(f"✓ 3D Original Space plot saved: {plot_file}")
|
|
329
|
+
|
|
330
|
+
def main():
|
|
331
|
+
args = parse_args()
|
|
332
|
+
logger = CLILogger(verbose=args.verbose)
|
|
333
|
+
|
|
334
|
+
print(f"\n{'='*60}")
|
|
335
|
+
print(f"PCA CLUSTERING ANALYSIS")
|
|
336
|
+
print(f"{'='*60}\n")
|
|
337
|
+
|
|
338
|
+
logger.info(f"Loading ensemble from {args.file}...")
|
|
339
|
+
try:
|
|
340
|
+
ensemble = read_ensemble(args.file, raw=True)
|
|
341
|
+
logger.info(f"✓ Loaded {len(ensemble)} conformers")
|
|
342
|
+
except TypeError:
|
|
343
|
+
try:
|
|
344
|
+
ensemble = read_ensemble(args.file, logger, raw=True)
|
|
345
|
+
logger.info(f"✓ Loaded {len(ensemble)} conformers")
|
|
346
|
+
except Exception as e:
|
|
347
|
+
logger.error(f"Failed to load ensemble: {e}")
|
|
348
|
+
exit(1)
|
|
349
|
+
except Exception as e:
|
|
350
|
+
logger.error(f"Failed to load ensemble: {e}")
|
|
351
|
+
exit(1)
|
|
352
|
+
|
|
353
|
+
config = ClusteringConfig(
|
|
354
|
+
n_clusters=args.ncluster,
|
|
355
|
+
include_H=args.include_H,
|
|
356
|
+
set_cluster_attribute=True
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
manager = ClusteringManager(logger=logger, config=config)
|
|
360
|
+
|
|
361
|
+
result = manager.perform_pca(
|
|
362
|
+
conformers=ensemble,
|
|
363
|
+
n_clusters=args.ncluster,
|
|
364
|
+
output_file=args.output,
|
|
365
|
+
title=args.title,
|
|
366
|
+
include_legend=args.legend
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
if result:
|
|
370
|
+
plot_clustering_metrics(result, args.output, logger)
|
|
371
|
+
plot_component_analysis(result, args.output, logger)
|
|
372
|
+
plot_before_after_pca(result, args.output, logger)
|
|
373
|
+
|
|
374
|
+
print(f"\n{'='*60}")
|
|
375
|
+
print(f"RESULTS")
|
|
376
|
+
print(f"{'='*60}")
|
|
377
|
+
print(f"✓ Clusters identified: {result.n_clusters}")
|
|
378
|
+
print(f"✓ Variance explained (PC1): {result.explained_variance[0]*100:.1f}%")
|
|
379
|
+
print(f"✓ Variance explained (PC2): {result.explained_variance[1]*100:.1f}%")
|
|
380
|
+
print(f"✓ Total variance (PC1+PC2): {result.explained_variance[:2].sum()*100:.1f}%")
|
|
381
|
+
print(f"✓ Output saved: {args.output}")
|
|
382
|
+
|
|
383
|
+
if getattr(result, 'components', None) is not None:
|
|
384
|
+
print(f"\n{'='*60}")
|
|
385
|
+
print(f"COMPONENT ANALYSIS")
|
|
386
|
+
print(f"{'='*60}")
|
|
387
|
+
for i in range(min(3, len(result.components))):
|
|
388
|
+
pc = result.components[i]
|
|
389
|
+
top_features = np.argsort(np.abs(pc))[-5:][::-1]
|
|
390
|
+
print(f"PC{i+1} top features: {list(top_features)} (variance: {result.explained_variance[i]*100:.1f}%)")
|
|
391
|
+
print(f"{'='*60}")
|
|
392
|
+
|
|
393
|
+
if args.write:
|
|
394
|
+
logger.info("\nReducing ensemble by clusters...")
|
|
395
|
+
print([i.cluster for i in ensemble])
|
|
396
|
+
reduced = manager.reduce_by_clusters(ensemble, True)
|
|
397
|
+
save_snapshot("clustered.xyz", reduced, logger)
|
|
398
|
+
active = len([c for c in reduced if c.active])
|
|
399
|
+
logger.info(f"✓ Reduced ensemble: {active} conformers → clustered.xyz")
|
|
400
|
+
|
|
401
|
+
print(f"{'='*60}\n")
|
|
402
|
+
else:
|
|
403
|
+
logger.error("PCA analysis failed or was skipped")
|
|
404
|
+
exit(1)
|
|
405
|
+
|
|
406
|
+
if __name__ == '__main__':
|
|
407
|
+
main()
|
|
@@ -4,6 +4,7 @@ import numpy as np
|
|
|
4
4
|
from typing import List, Tuple, Optional
|
|
5
5
|
|
|
6
6
|
from ensemble_analyzer._conformer.conformer import Conformer
|
|
7
|
+
from ensemble_analyzer._conformer.energy_data import EnergyRecord
|
|
7
8
|
from ensemble_analyzer._logger.logger import Logger
|
|
8
9
|
|
|
9
10
|
|
|
@@ -20,7 +21,7 @@ def _parse_xyz_str(fl: List[str], raw: bool =False) -> Tuple[np.ndarray, np.ndar
|
|
|
20
21
|
"""
|
|
21
22
|
e = None
|
|
22
23
|
if raw:
|
|
23
|
-
e = float(re.findall(r'([- ]\d*\.\d*)
|
|
24
|
+
e = float(re.findall(r'([- ]\d*\.\d*)', fl[1].strip())[0])
|
|
24
25
|
fl = fl[2:]
|
|
25
26
|
atoms, geom = [], []
|
|
26
27
|
for line in fl:
|
|
@@ -47,7 +48,7 @@ def read_ensemble(file: str, log:Logger, raw: bool=False) -> list:
|
|
|
47
48
|
str: If the file does not end with .xyz.
|
|
48
49
|
"""
|
|
49
50
|
|
|
50
|
-
confs = []
|
|
51
|
+
confs : List[Conformer] = []
|
|
51
52
|
|
|
52
53
|
if not file.endswith(".xyz"):
|
|
53
54
|
raise ValueError("Ensemble file must be an XYZ (multi)geometry file")
|
|
@@ -63,6 +64,8 @@ def read_ensemble(file: str, log:Logger, raw: bool=False) -> list:
|
|
|
63
64
|
continue
|
|
64
65
|
atoms, geom, e = _parse_xyz_str(fl[old_idx:i], raw=raw)
|
|
65
66
|
confs.append(Conformer(counter, geom=geom, atoms=atoms))
|
|
67
|
+
if raw:
|
|
68
|
+
confs[-1].energies.add(protocol_number=0, record=EnergyRecord(E=e))
|
|
66
69
|
old_idx = i
|
|
67
70
|
counter += 1
|
|
68
71
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ensemble-analyzer
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1a0
|
|
4
4
|
Summary: Automatic Refinement of Conformational Ensemble
|
|
5
5
|
Author-email: Andrea Pellegrini <andrea.pellegrini15@unibo.it>, Paolo Righi <paolo.righi@unibo.it>, Andrea Mazzanti <andrea.mazzanti@unibo.it>, Michele Mancinelli <michele.mancinelli@unibo.it>
|
|
6
6
|
Maintainer-email: Andrea Pellegrini <andrea.pellegrini15@unibo.it>, Paolo Righi <paolo.righi@unibo.it>
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
from unittest.mock import Mock
|
|
3
|
-
from ensemble_analyzer.ensemble_io import read_ensemble, save_snapshot
|
|
4
|
-
from ensemble_analyzer.clustering import *
|
|
5
|
-
|
|
6
|
-
def parse_args():
|
|
7
|
-
parser = argparse.ArgumentParser(
|
|
8
|
-
description='Perform PCA analysis and clustering on conformer ensemble',
|
|
9
|
-
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
10
|
-
epilog="""
|
|
11
|
-
Examples:
|
|
12
|
-
# Basic PCA with 5 clusters
|
|
13
|
-
python clustering.py ensemble.xyz -nc 5
|
|
14
|
-
# Auto-detect optimal clusters, exclude hydrogen
|
|
15
|
-
python clustering.py ensemble.xyz --no-H
|
|
16
|
-
# Save reduced ensemble
|
|
17
|
-
python clustering.py ensemble.xyz -nc 10 --save-reduced
|
|
18
|
-
# Custom output and title
|
|
19
|
-
python clustering.py ensemble.xyz -o my_pca.png --title "Drug Conformers"
|
|
20
|
-
"""
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
parser.add_argument(
|
|
24
|
-
'file',
|
|
25
|
-
help='Input ensemble file (XYZ format)'
|
|
26
|
-
)
|
|
27
|
-
parser.add_argument(
|
|
28
|
-
'-nc', '--ncluster',
|
|
29
|
-
type=int,
|
|
30
|
-
default=None,
|
|
31
|
-
help='Number of clusters (default: auto-detect using silhouette score)'
|
|
32
|
-
)
|
|
33
|
-
parser.add_argument(
|
|
34
|
-
'--no-H',
|
|
35
|
-
action='store_false',
|
|
36
|
-
dest='include_H',
|
|
37
|
-
help='Exclude hydrogen atoms from distance matrix calculation'
|
|
38
|
-
)
|
|
39
|
-
parser.add_argument(
|
|
40
|
-
'--no-legend',
|
|
41
|
-
action='store_false',
|
|
42
|
-
dest='legend',
|
|
43
|
-
help='Exclude conformer legend from plot'
|
|
44
|
-
)
|
|
45
|
-
parser.add_argument(
|
|
46
|
-
'--title',
|
|
47
|
-
default='PCA Cluster Analysis',
|
|
48
|
-
help='Plot title (default: "PCA Cluster Analysis")'
|
|
49
|
-
)
|
|
50
|
-
parser.add_argument(
|
|
51
|
-
'-o', '--output',
|
|
52
|
-
default='cluster.png',
|
|
53
|
-
help='Output image filename (default: cluster.png)'
|
|
54
|
-
)
|
|
55
|
-
parser.add_argument(
|
|
56
|
-
'--save-reduced',
|
|
57
|
-
action='store_true',
|
|
58
|
-
help='Save cluster-reduced ensemble to clustered.xyz'
|
|
59
|
-
)
|
|
60
|
-
parser.add_argument(
|
|
61
|
-
'--dpi',
|
|
62
|
-
type=int,
|
|
63
|
-
default=300,
|
|
64
|
-
help='Output image DPI (default: 300)'
|
|
65
|
-
)
|
|
66
|
-
parser.add_argument(
|
|
67
|
-
'-v', '--verbose',
|
|
68
|
-
action='store_true',
|
|
69
|
-
help='Enable verbose output'
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
args = parser.parse_args()
|
|
73
|
-
return args
|
|
74
|
-
|
|
75
|
-
class CLILogger:
|
|
76
|
-
def __init__(self, verbose=False):
|
|
77
|
-
self.verbose = verbose
|
|
78
|
-
|
|
79
|
-
def info(self, msg):
|
|
80
|
-
print(f"ℹ️ {msg}")
|
|
81
|
-
|
|
82
|
-
def debug(self, msg):
|
|
83
|
-
if self.verbose:
|
|
84
|
-
print(f"🔍 {msg}")
|
|
85
|
-
|
|
86
|
-
def warning(self, msg):
|
|
87
|
-
print(f"⚠️ {msg}")
|
|
88
|
-
|
|
89
|
-
def error(self, msg):
|
|
90
|
-
print(f"❌ {msg}")
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def main():
|
|
94
|
-
args = parse_args()
|
|
95
|
-
logger = CLILogger(verbose=args.verbose)
|
|
96
|
-
|
|
97
|
-
# Load ensemble
|
|
98
|
-
print(f"\n{'='*60}")
|
|
99
|
-
print(f"PCA CLUSTERING ANALYSIS")
|
|
100
|
-
print(f"{'='*60}\n")
|
|
101
|
-
|
|
102
|
-
logger.info(f"Loading ensemble from {args.file}...")
|
|
103
|
-
try:
|
|
104
|
-
ensemble = read_ensemble(args.file, logger, raw=True)
|
|
105
|
-
logger.info(f"✓ Loaded {len(ensemble)} conformers")
|
|
106
|
-
except Exception as e:
|
|
107
|
-
logger.error(f"Failed to load ensemble: {e}")
|
|
108
|
-
exit(1)
|
|
109
|
-
|
|
110
|
-
# Perform PCA
|
|
111
|
-
config = ClusteringConfig(
|
|
112
|
-
n_clusters=args.ncluster,
|
|
113
|
-
include_H=args.include_H,
|
|
114
|
-
set_cluster_attribute=False
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
manager = ClusteringManager(logger=logger, config=config)
|
|
118
|
-
|
|
119
|
-
result = manager.perform_pca(
|
|
120
|
-
conformers=ensemble,
|
|
121
|
-
n_clusters=args.ncluster,
|
|
122
|
-
output_file=args.output,
|
|
123
|
-
title=args.title,
|
|
124
|
-
include_legend=args.legend
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
if result:
|
|
128
|
-
print(f"\n{'='*60}")
|
|
129
|
-
print(f"RESULTS")
|
|
130
|
-
print(f"{'='*60}")
|
|
131
|
-
print(f"✓ Clusters identified: {result.n_clusters}")
|
|
132
|
-
print(f"✓ Variance explained (PC1): {result.explained_variance[0]*100:.1f}%")
|
|
133
|
-
print(f"✓ Variance explained (PC2): {result.explained_variance[1]*100:.1f}%")
|
|
134
|
-
print(f"✓ Total variance (PC1+PC2): {result.explained_variance[:2].sum()*100:.1f}%")
|
|
135
|
-
print(f"✓ Output saved: {args.output}")
|
|
136
|
-
|
|
137
|
-
# Save reduced ensemble if requested
|
|
138
|
-
if args.save_reduced:
|
|
139
|
-
logger.info("\nReducing ensemble by clusters...")
|
|
140
|
-
reduced = manager.reduce_by_clusters(ensemble)
|
|
141
|
-
save_snapshot("clustered.xyz", reduced, logger)
|
|
142
|
-
active = len([c for c in reduced if c.active])
|
|
143
|
-
logger.info(f"✓ Reduced ensemble: {active} conformers → clustered.xyz")
|
|
144
|
-
|
|
145
|
-
print(f"{'='*60}\n")
|
|
146
|
-
|
|
147
|
-
else:
|
|
148
|
-
logger.error("PCA analysis failed or was skipped")
|
|
149
|
-
exit(1)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if __name__ == '__main__':
|
|
153
|
-
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_calculators/__init__.py
RENAMED
|
File without changes
|
{ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_calculators/_gaussian.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_conformer/energy_data.py
RENAMED
|
File without changes
|
{ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_conformer/spectral_data.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_managers/calculation_config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_managers/checkpoint_manager.py
RENAMED
|
File without changes
|
{ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_managers/protocol_executor.py
RENAMED
|
File without changes
|
{ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_managers/protocol_manager.py
RENAMED
|
File without changes
|
{ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_managers/pruning_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_spectral/comp_electronic.py
RENAMED
|
File without changes
|
{ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_spectral/comp_vibronic.py
RENAMED
|
File without changes
|
|
File without changes
|
{ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_spectral/experimental.py
RENAMED
|
File without changes
|
{ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/_spectral/graph_default.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/cli/pickle_editor/__init__.py
RENAMED
|
File without changes
|
{ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/cli/pickle_editor/cli.py
RENAMED
|
File without changes
|
{ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/cli/pickle_editor/core.py
RENAMED
|
File without changes
|
{ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/cli/pickle_editor/tui.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/parameters_file/basis_sets
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer/parameters_file/functionals
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{ensemble_analyzer-1 → ensemble_analyzer-1a0}/src/ensemble_analyzer.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|