csa3d 0.2.0__tar.gz → 0.2.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csa3d
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Extract and analyze enzyme catalytic sites in 3D from the Mechanism and Catalytic Site Atlas (M-CSA)
5
5
  Author-email: Ioannis Riziotis <riziotis@ebi.ac.uk>
6
6
  License: Apache-2.0
@@ -30,9 +30,10 @@ Requires-Dist: scipy>=1.7.0
30
30
  Requires-Dist: scikit-learn>=0.24.0
31
31
  Requires-Dist: rdkit>=2021.09.0
32
32
  Requires-Dist: natsort>=8.0.0
33
- Requires-Dist: dendrogram-cut>=0.1.0
34
- Requires-Dist: parity-core>=0.5.0
35
33
  Requires-Dist: rmsd>=1.4.0
34
+ Provides-Extra: clustering
35
+ Requires-Dist: matplotlib>=3.5; extra == "clustering"
36
+ Requires-Dist: plotly>=5.0; extra == "clustering"
36
37
  Provides-Extra: dev
37
38
  Requires-Dist: pytest>=7.0; extra == "dev"
38
39
  Requires-Dist: pytest-cov>=4.0; extra == "dev"
@@ -105,28 +106,43 @@ pip install -e ".[dev,docs]"
105
106
 
106
107
  ## Quick Start
107
108
 
108
- ### 1. Configure Data Directory
109
+ ### 1. Automatic Data Setup (Recommended)
109
110
 
110
- First, you need to set up the M-CSA data files. You can either:
111
+ Download and setup M-CSA data automatically:
111
112
 
112
- **Option A: Download automatically (recommended)**
113
113
  ```python
114
- from csa3d import config
115
- config.setup_data("/path/to/csa3d_data") # Downloads ~50 GB
114
+ from csa3d import setup_data
115
+
116
+ # Downloads ~50 GB of data to ~/.csa3d/data
117
+ # Set up specific location with:
118
+ # setup_data("/path/to/csa3d_data")
119
+ success = setup_data()
120
+
121
+ if success:
122
+ print("✓ Data setup complete!")
116
123
  ```
117
124
 
118
- **Option B: Set environment variable**
125
+ Or via command line:
126
+ ```bash
127
+ python -m csa3d.data_setup /path/to/csa3d_data
128
+ ```
129
+
130
+ ### 2. Manual Data Configuration
131
+
132
+ If you prefer to download data manually:
133
+
134
+ **Option A: Environment variable**
119
135
  ```bash
120
136
  export CSA3D_DATA_DIR="/path/to/csa3d_data"
121
137
  ```
122
138
 
123
- **Option C: Set in code**
139
+ **Option B: Set in code**
124
140
  ```python
125
- from csa3d import config
126
- config.set_data_dir("/path/to/csa3d_data")
141
+ from csa3d import set_data_dir
142
+ set_data_dir("/path/to/csa3d_data")
127
143
  ```
128
144
 
129
- ### 2. Load and Inspect Active Sites
145
+ ### 3. Load and Inspect Active Sites
130
146
 
131
147
  ```python
132
148
  from csa3d import Mcsa
@@ -151,7 +167,7 @@ for entry in db:
151
167
  print(f" Resolution: {site.resolution} Å")
152
168
  ```
153
169
 
154
- ### 3. Clustering and Template Creation
170
+ ### 4. Clustering and Template Creation
155
171
 
156
172
  ```python
157
173
  # Build multiple entries
@@ -51,28 +51,43 @@ pip install -e ".[dev,docs]"
51
51
 
52
52
  ## Quick Start
53
53
 
54
- ### 1. Configure Data Directory
54
+ ### 1. Automatic Data Setup (Recommended)
55
55
 
56
- First, you need to set up the M-CSA data files. You can either:
56
+ Download and setup M-CSA data automatically:
57
57
 
58
- **Option A: Download automatically (recommended)**
59
58
  ```python
60
- from csa3d import config
61
- config.setup_data("/path/to/csa3d_data") # Downloads ~50 GB
59
+ from csa3d import setup_data
60
+
61
+ # Downloads ~50 GB of data to ~/.csa3d/data
62
+ # Set up specific location with:
63
+ # setup_data("/path/to/csa3d_data")
64
+ success = setup_data()
65
+
66
+ if success:
67
+ print("✓ Data setup complete!")
62
68
  ```
63
69
 
64
- **Option B: Set environment variable**
70
+ Or via command line:
71
+ ```bash
72
+ python -m csa3d.data_setup /path/to/csa3d_data
73
+ ```
74
+
75
+ ### 2. Manual Data Configuration
76
+
77
+ If you prefer to download data manually:
78
+
79
+ **Option A: Environment variable**
65
80
  ```bash
66
81
  export CSA3D_DATA_DIR="/path/to/csa3d_data"
67
82
  ```
68
83
 
69
- **Option C: Set in code**
84
+ **Option B: Set in code**
70
85
  ```python
71
- from csa3d import config
72
- config.set_data_dir("/path/to/csa3d_data")
86
+ from csa3d import set_data_dir
87
+ set_data_dir("/path/to/csa3d_data")
73
88
  ```
74
89
 
75
- ### 2. Load and Inspect Active Sites
90
+ ### 3. Load and Inspect Active Sites
76
91
 
77
92
  ```python
78
93
  from csa3d import Mcsa
@@ -97,7 +112,7 @@ for entry in db:
97
112
  print(f" Resolution: {site.resolution} Å")
98
113
  ```
99
114
 
100
- ### 3. Clustering and Template Creation
115
+ ### 4. Clustering and Template Creation
101
116
 
102
117
  ```python
103
118
  # Build multiple entries
@@ -0,0 +1,12 @@
1
+ """CSA-3D: Extract and analyze enzyme catalytic sites from M-CSA."""
2
+
3
+ from .data_setup import setup_data
4
+ from .config import set_data_dir, get_data_dir
5
+
6
+ __version__ = "0.2.2"
7
+
8
+ __all__ = [
9
+ "setup_data",
10
+ "set_data_dir",
11
+ "get_data_dir",
12
+ ]
@@ -0,0 +1,7 @@
1
+ """Vendored dependencies for CSA-3D.
2
+
3
+ This package contains minimal implementations of research-specific dependencies
4
+ that are not available on PyPI.
5
+ """
6
+
7
+ __all__ = ['dendrogram_cut']
@@ -0,0 +1,469 @@
1
+ import itertools
2
+
3
+ import numpy as np
4
+ import scipy.spatial
5
+ import scipy.cluster
6
+ import matplotlib.cm
7
+ import plotly.graph_objs as go
8
+ from sklearn.metrics import silhouette_score
9
+
10
+
11
+ class DendrogramCut:
12
+ def __init__(self, k_max, method='average'):
13
+ self.k_max = k_max
14
+ self.method = method
15
+
16
+ def fit(self, distance_matrix):
17
+ '''
18
+ Build linkage_stats
19
+ css: cross sum of square when merging c1 and c2
20
+ tss: total sum of square of merged cluster
21
+ '''
22
+ # Handle both condensed and square distance matrices
23
+ if distance_matrix.ndim == 1:
24
+ self.distance_matrix = scipy.spatial.distance.squareform(distance_matrix)
25
+ self.linkage = scipy.cluster.hierarchy.linkage(distance_matrix, method=self.method, optimal_ordering=True)
26
+ else:
27
+ self.distance_matrix = distance_matrix
28
+ self.linkage = scipy.cluster.hierarchy.linkage(scipy.spatial.distance.pdist(distance_matrix), method=self.method, optimal_ordering=True)
29
+
30
+ self.n_data = self.distance_matrix.shape[0]
31
+ self.linkage_stats = [{'c1': 0, 'c2': 0, 'css': 0, 'tss': 0, 'indices': set()} for _ in range(2 * self.n_data - 1)]
32
+
33
+ for i in range(self.n_data):
34
+ self.linkage_stats[i]['c1'] = i
35
+ self.linkage_stats[i]['c2'] = i
36
+ self.linkage_stats[i]['indices'].add(i)
37
+
38
+ for i, (c1, c2, _, _) in enumerate(self.linkage):
39
+ c1 = int(c1)
40
+ c2 = int(c2)
41
+ self.linkage_stats[i + self.n_data]['c1'] = c1
42
+ self.linkage_stats[i + self.n_data]['c2'] = c2
43
+ self.linkage_stats[i + self.n_data]['indices'].update(self.linkage_stats[c1]['indices'])
44
+ self.linkage_stats[i + self.n_data]['indices'].update(self.linkage_stats[c2]['indices'])
45
+
46
+ for i in range(self.n_data, 2 * self.n_data - 1):
47
+ c1 = self.linkage_stats[i]['c1']
48
+ c2 = self.linkage_stats[i]['c2']
49
+ c1_indices = np.asarray(list(self.linkage_stats[c1]['indices']))
50
+ c2_indices = np.asarray(list(self.linkage_stats[c2]['indices']))
51
+
52
+ sample_distances = self.distance_matrix[c1_indices, :][:, c2_indices]
53
+ self.linkage_stats[i]['css'] = np.sum(sample_distances ** 2)
54
+ self.linkage_stats[i]['tss'] = self.linkage_stats[i]['css'] + self.linkage_stats[c1]['tss'] + self.linkage_stats[c2]['tss']
55
+
56
+ ### dynamic programming ###
57
+ '''
58
+ Dynamic programming
59
+ kl_mat[i, k]: the number of clusters in the left branch of linkage_stat[$i], maximal cluster $k
60
+ mss_mat[i, k]: the optimal mean square error achieved at linkage_stat[$i], maximal cluster $k
61
+ '''
62
+ self.kl_mat = np.zeros((self.n_data * 2 - 1, self.k_max + 1), dtype=int)
63
+ self.mss_mat = np.zeros((self.n_data * 2 - 1, self.k_max + 1), dtype=float) + np.inf
64
+
65
+ for i in range(self.n_data):
66
+ self.mss_mat[i, 1] = 0
67
+
68
+ for i in range(self.n_data, 2 * self.n_data - 1):
69
+ self.mss_mat[i, 1] = self.linkage_stats[i]['tss'] / len(self.linkage_stats[i]['indices'])
70
+
71
+ for i in range(self.n_data, 2 * self.n_data - 1):
72
+ for k in range(2, self.k_max + 1):
73
+ kl_min = 0
74
+ mss_min = np.inf
75
+ for kl in range(1, k):
76
+ tss = self.mss_mat[self.linkage_stats[i]['c1'], kl] + self.mss_mat[self.linkage_stats[i]['c2'], k - kl]
77
+ if tss < mss_min:
78
+ kl_min = kl
79
+ mss_min = tss
80
+ self.kl_mat[i, k] = kl_min
81
+ self.mss_mat[i, k] = mss_min
82
+
83
+ return self
84
+
85
+ def _get_cut_nodes(self, v, k):
86
+ if k == 1:
87
+ yield v
88
+ else:
89
+ yield from self._get_cut_nodes(self.linkage_stats[v]['c1'], self.kl_mat[v, k])
90
+ yield from self._get_cut_nodes(self.linkage_stats[v]['c2'], k - self.kl_mat[v, k])
91
+
92
+ def get_cluster_mss(self, k):
93
+ total_mss = 0.
94
+ for cid in self._get_cut_nodes(2 * self.n_data - 2, k):
95
+ total_mss += self.linkage_stats[cid]['tss'] / len(self.linkage_stats[cid]['indices'])
96
+
97
+ return total_mss
98
+
99
+ def get_cluster_label(self, k):
100
+ ### get flat clusters ###
101
+ z = np.zeros(self.n_data, dtype=int) - 1
102
+ for c, cid in enumerate(self._get_cut_nodes(2 * self.n_data - 2, k)):
103
+ for i in self.linkage_stats[cid]['indices']:
104
+ if z[i] != -1:
105
+ print(i)
106
+ z[i] = c
107
+
108
+ return z
109
+
110
+ def _dirichlet_process_kl(self, n_list, alpha_):
111
+ out = 0.
112
+ count = alpha_
113
+ for n in n_list:
114
+ out -= np.log(alpha_ / count)
115
+ count += 1
116
+ for i in range(1, n):
117
+ out -= np.log(i / count)
118
+ count += 1
119
+ return out
120
+
121
+ def pac_bayesian_cut(self, alpha_=1., lambda_=1.):
122
+ min_loss = np.inf
123
+ min_loss_k = None
124
+
125
+ for k in range(1, self.k_max + 1):
126
+ n_list = [len(self.linkage_stats[c]['indices']) for c in self._get_cut_nodes(2 * self.n_data - 2, k)]
127
+ total_mss = self.get_cluster_mss(k)
128
+ loss = total_mss + self._dirichlet_process_kl(n_list, alpha_) / lambda_
129
+ if loss < min_loss:
130
+ min_loss = loss
131
+ min_loss_k = k
132
+
133
+ return min_loss_k
134
+
135
+ def dendrogram_plot(self, k=None, label=None):
136
+ cmap = matplotlib.cm.get_cmap('viridis')
137
+
138
+ order = scipy.cluster.hierarchy.leaves_list(self.linkage)
139
+
140
+ dendrogram_stat = dict((v, {'x': i, 'y': 0}) for i, v in enumerate(order))
141
+
142
+ data = []
143
+
144
+ ### plot dendrogram ###
145
+ if k is None:
146
+ norm = np.max(self.linkage[:, 2])
147
+
148
+ for res in self.linkage:
149
+ new_cluster = len(dendrogram_stat)
150
+ dendrogram_stat[new_cluster] = {'x': (dendrogram_stat[res[0]]['x'] + dendrogram_stat[res[1]]['x']) / 2, 'y': res[2]}
151
+ x_range = np.array([dendrogram_stat[res[0]]['x'], dendrogram_stat[res[0]]['x'], dendrogram_stat[res[1]]['x'], dendrogram_stat[res[1]]['x']])
152
+ y_range = np.array([dendrogram_stat[res[0]]['y'], dendrogram_stat[new_cluster]['y'], dendrogram_stat[new_cluster]['y'], dendrogram_stat[res[1]]['y']])
153
+
154
+ data.append(
155
+ {
156
+ 'hoverinfo': 'text',
157
+ 'marker': {'color': 'rgb({}, {}, {})'.format(*cmap(res[2] / norm, bytes=True)[:3])},
158
+ 'mode': 'lines',
159
+ 'type': 'scatter',
160
+ 'x': x_range,
161
+ 'xaxis': 'x',
162
+ 'y': y_range,
163
+ 'yaxis': 'y'
164
+ }
165
+ )
166
+ else:
167
+ pivot = set(self._get_cut_nodes(2 * self.n_data - 2, k))
168
+ z = self.get_cluster_label(k)
169
+ colorscale = dict((i, z[i] / (k - 1)) for i in range(self.n_data))
170
+
171
+ for res in self.linkage:
172
+ new_cluster = len(dendrogram_stat)
173
+ colorscale[new_cluster] = (colorscale[int(res[0])] + colorscale[int(res[1])]) / 2
174
+ dendrogram_stat[new_cluster] = {'x': (dendrogram_stat[res[0]]['x'] + dendrogram_stat[res[1]]['x']) / 2, 'y': res[2]}
175
+ x_range = np.array([dendrogram_stat[res[0]]['x'], dendrogram_stat[res[0]]['x'], dendrogram_stat[res[1]]['x'], dendrogram_stat[res[1]]['x']])
176
+ y_range = np.array([dendrogram_stat[res[0]]['y'], dendrogram_stat[new_cluster]['y'], dendrogram_stat[new_cluster]['y'], dendrogram_stat[res[1]]['y']])
177
+
178
+ if new_cluster in pivot:
179
+ data.append(
180
+ {
181
+ 'x': [dendrogram_stat[new_cluster]['x']],
182
+ 'y': [dendrogram_stat[new_cluster]['y']],
183
+ 'type': 'scatter',
184
+ 'mode': 'markers',
185
+ 'xaxis': 'x',
186
+ 'yaxis': 'y',
187
+ 'marker': {'size': [20], 'color': 'rgb({}, {}, {})'.format(*cmap(colorscale[new_cluster], bytes=True)[:3])}
188
+ }
189
+ )
190
+
191
+ data.append(
192
+ {
193
+ 'hoverinfo': 'text',
194
+ 'marker': {'color': 'rgb({}, {}, {})'.format(*cmap(colorscale[new_cluster], bytes=True)[:3])},
195
+ 'mode': 'lines',
196
+ 'type': 'scatter',
197
+ 'x': x_range,
198
+ 'xaxis': 'x',
199
+ 'y': y_range,
200
+ 'yaxis': 'y'
201
+ }
202
+ )
203
+
204
+ fig = go.Figure(data=data)
205
+
206
+ ### edit layout ###
207
+ if label is not None:
208
+ fig.update_layout(
209
+ {
210
+ 'xaxis': {
211
+ 'tickmode': 'array',
212
+ 'tickvals': np.arange(len(label)),
213
+ 'ticktext': label[order]
214
+ }
215
+ }
216
+ )
217
+
218
+ fig.update_layout(
219
+ {
220
+ 'width': 800,
221
+ 'height': 800,
222
+ 'showlegend': False,
223
+ 'hovermode': 'closest',
224
+ 'xaxis': {
225
+ 'domain': [0, 1],
226
+ 'rangemode': 'tozero',
227
+ 'mirror': False,
228
+ 'showgrid': False,
229
+ 'showline': False,
230
+ 'zeroline': False,
231
+ 'showticklabels': True,
232
+ 'ticks': ""
233
+ },
234
+ 'yaxis': {
235
+ 'domain': [0, 1],
236
+ 'rangemode': 'tozero',
237
+ 'mirror': False,
238
+ 'showgrid': False,
239
+ 'showline': False,
240
+ 'zeroline': False,
241
+ 'showticklabels': True,
242
+ 'ticks': ""
243
+ },
244
+ 'paper_bgcolor': 'rgba(0,0,0,0)',
245
+ 'plot_bgcolor': 'rgba(0,0,0,0)'
246
+ }
247
+ )
248
+
249
+ fig.show()
250
+
251
+ def heatmap_with_dendrogram_plot(self, k=None, label=None):
252
+ cmap = matplotlib.cm.get_cmap('viridis')
253
+
254
+ order = scipy.cluster.hierarchy.leaves_list(self.linkage)
255
+
256
+ dendrogram_stat = dict((v, {'x': i, 'y': 0}) for i, v in enumerate(order))
257
+
258
+ data = []
259
+
260
+ ### plot dendrogram ###
261
+ if k is None:
262
+ norm = np.max(self.linkage[:, 2])
263
+
264
+ for res in self.linkage:
265
+ new_cluster = len(dendrogram_stat)
266
+ dendrogram_stat[new_cluster] = {'x': (dendrogram_stat[res[0]]['x'] + dendrogram_stat[res[1]]['x']) / 2, 'y': res[2]}
267
+ x_range = np.array([dendrogram_stat[res[0]]['x'], dendrogram_stat[res[0]]['x'], dendrogram_stat[res[1]]['x'], dendrogram_stat[res[1]]['x']])
268
+ y_range = np.array([dendrogram_stat[res[0]]['y'], dendrogram_stat[new_cluster]['y'], dendrogram_stat[new_cluster]['y'], dendrogram_stat[res[1]]['y']])
269
+
270
+ data.append(
271
+ {
272
+ 'hoverinfo': 'text',
273
+ 'marker': {'color': 'rgb({}, {}, {})'.format(*cmap(res[2] / norm, bytes=True)[:3])},
274
+ 'mode': 'lines',
275
+ 'type': 'scatter',
276
+ 'x': x_range,
277
+ 'xaxis': 'x',
278
+ 'y': y_range,
279
+ 'yaxis': 'y2'
280
+ }
281
+ )
282
+
283
+ data.append(
284
+ {
285
+ 'hoverinfo': 'text',
286
+ 'marker': {'color': 'rgb({}, {}, {})'.format(*cmap(res[2] / norm, bytes=True)[:3])},
287
+ 'mode': 'lines',
288
+ 'type': 'scatter',
289
+ 'x': -y_range,
290
+ 'xaxis': 'x2',
291
+ 'y': x_range,
292
+ 'yaxis': 'y'
293
+ }
294
+ )
295
+ else:
296
+ pivot = set(self._get_cut_nodes(2 * self.n_data - 2, k))
297
+ z = self.get_cluster_label(k)
298
+ colorscale = dict((i, z[i] / (k - 1)) for i in range(self.n_data))
299
+
300
+ for res in self.linkage:
301
+ new_cluster = len(dendrogram_stat)
302
+ colorscale[new_cluster] = (colorscale[int(res[0])] + colorscale[int(res[1])]) / 2
303
+ dendrogram_stat[new_cluster] = {'x': (dendrogram_stat[res[0]]['x'] + dendrogram_stat[res[1]]['x']) / 2, 'y': res[2]}
304
+ x_range = np.array([dendrogram_stat[res[0]]['x'], dendrogram_stat[res[0]]['x'], dendrogram_stat[res[1]]['x'], dendrogram_stat[res[1]]['x']])
305
+ y_range = np.array([dendrogram_stat[res[0]]['y'], dendrogram_stat[new_cluster]['y'], dendrogram_stat[new_cluster]['y'], dendrogram_stat[res[1]]['y']])
306
+
307
+ if new_cluster in pivot:
308
+ data.append(
309
+ {
310
+ 'x': [dendrogram_stat[new_cluster]['x']],
311
+ 'y': [dendrogram_stat[new_cluster]['y']],
312
+ 'type': 'scatter',
313
+ 'mode': 'markers',
314
+ 'xaxis': 'x',
315
+ 'yaxis': 'y2',
316
+ 'marker': {'size': [10], 'color': 'rgb({}, {}, {})'.format(*cmap(colorscale[new_cluster], bytes=True)[:3])}
317
+ }
318
+ )
319
+ data.append(
320
+ {
321
+ 'x': [-dendrogram_stat[new_cluster]['y']],
322
+ 'y': [dendrogram_stat[new_cluster]['x']],
323
+ 'type': 'scatter',
324
+ 'mode': 'markers',
325
+ 'xaxis': 'x2',
326
+ 'yaxis': 'y',
327
+ 'marker': {'size': [10], 'color': 'rgb({}, {}, {})'.format(*cmap(colorscale[new_cluster], bytes=True)[:3])}
328
+ }
329
+ )
330
+
331
+ data.append(
332
+ {
333
+ 'hoverinfo': 'text',
334
+ 'marker': {'color': 'rgb({}, {}, {})'.format(*cmap(colorscale[new_cluster], bytes=True)[:3])},
335
+ 'mode': 'lines',
336
+ 'type': 'scatter',
337
+ 'x': x_range,
338
+ 'xaxis': 'x',
339
+ 'y': y_range,
340
+ 'yaxis': 'y2'
341
+ }
342
+ )
343
+
344
+ data.append(
345
+ {
346
+ 'hoverinfo': 'text',
347
+ 'marker': {'color': 'rgb({}, {}, {})'.format(*cmap(colorscale[new_cluster], bytes=True)[:3])},
348
+ 'mode': 'lines',
349
+ 'type': 'scatter',
350
+ 'x': -y_range,
351
+ 'xaxis': 'x2',
352
+ 'y': x_range,
353
+ 'yaxis': 'y'
354
+ }
355
+ )
356
+
357
+ ### plot hard cluster region ###
358
+
359
+ ordered_z = z[order]
360
+ block_size = [sum(1 for __ in g) for _, g in itertools.groupby(ordered_z)]
361
+ start = -0.5
362
+ for i in range(len(block_size)):
363
+ data.append(
364
+ go.Scatter(
365
+ x=[start, start, start+block_size[i], start+block_size[i], start],
366
+ y=[start, start+block_size[i], start+block_size[i], start, start],
367
+ mode='lines',
368
+ hoverinfo='none',
369
+ line=dict(color='#FF0000')
370
+ )
371
+ )
372
+ start += block_size[i]
373
+
374
+ ### plot heatmap ###
375
+ data.append(
376
+ go.Heatmap(
377
+ z=self.distance_matrix[order, :][:, order],
378
+ colorscale='Blues',
379
+ reversescale=True,
380
+ hoverinfo='x+y+z'
381
+ )
382
+ )
383
+
384
+ fig = go.Figure(data=data)
385
+
386
+ ### edit layout ###
387
+ if label is not None:
388
+ fig.update_layout(
389
+ {
390
+ 'xaxis': {
391
+ 'tickmode': 'array',
392
+ 'tickvals': np.arange(len(label)),
393
+ 'ticktext': label[order]
394
+ },
395
+ 'yaxis': {
396
+ 'tickmode': 'array',
397
+ 'tickvals': np.arange(len(label)),
398
+ 'ticktext': label[order],
399
+ 'autorange': 'reversed'
400
+ }
401
+ }
402
+ )
403
+
404
+ fig.update_layout(
405
+ {
406
+ 'width': 800,
407
+ 'height': 800,
408
+ 'showlegend': False,
409
+ 'hovermode': 'closest',
410
+ 'xaxis': {
411
+ 'domain': [.15, 1],
412
+ 'rangemode': 'tozero',
413
+ 'mirror': False,
414
+ 'showgrid': False,
415
+ 'showline': False,
416
+ 'zeroline': False,
417
+ 'showticklabels': False,
418
+ 'ticks': ""
419
+ },
420
+ 'xaxis2': {
421
+ 'domain': [0, .15],
422
+ 'rangemode': 'tozero',
423
+ 'mirror': False,
424
+ 'showgrid': False,
425
+ 'showline': False,
426
+ 'zeroline': False,
427
+ 'showticklabels': False,
428
+ 'ticks': ""
429
+ },
430
+ 'yaxis': {
431
+ 'domain': [0, .85],
432
+ 'rangemode': 'tozero',
433
+ 'mirror': False,
434
+ 'showgrid': False,
435
+ 'showline': False,
436
+ 'zeroline': False,
437
+ 'showticklabels': False,
438
+ 'ticks': ""
439
+ },
440
+ 'yaxis2': {
441
+ 'domain':[.825, .975],
442
+ 'rangemode': 'tozero',
443
+ 'mirror': False,
444
+ 'showgrid': False,
445
+ 'showline': False,
446
+ 'zeroline': False,
447
+ 'showticklabels': False,
448
+ 'ticks': ""
449
+ }
450
+ }
451
+ )
452
+
453
+ fig.show()
454
+
455
+ def elbow_plot(self):
456
+ x = np.arange(self.k_max) + 1
457
+
458
+ y = np.asarray([sum(self.linkage_stats[i]['tss'] / len(self.linkage_stats[i]['indices']) for i in self._get_cut_nodes(-1, k)) for k in range(1, self.k_max + 1)])
459
+ y = (y[0] - y) / y[0]
460
+
461
+ fig = go.Figure(data=[go.Scatter(x=x, y=y, mode='lines')])
462
+ fig.show()
463
+
464
+ def silhouette_score_plot(self):
465
+ x = np.arange(2, self.k_max + 1)
466
+ y = [silhouette_score(self.distance_matrix, self.get_cluster_label(k), metric='precomputed') for k in x]
467
+
468
+ fig = go.Figure(data=go.Scatter(x=x, y=y, mode='lines'))
469
+ fig.show()
@@ -0,0 +1,224 @@
1
+ """Automatic data setup and download for CSA-3D.
2
+
3
+ Downloads and organizes required data files from official sources.
4
+ """
5
+
6
+ import logging
7
+ import os
8
+ from pathlib import Path
9
+ from typing import Optional
10
+ import urllib.request
11
+ import urllib.error
12
+ from urllib.parse import urljoin
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class DataDownloader:
18
+ """Download and setup CSA-3D data files."""
19
+
20
+ # Data source URLs
21
+ SIFTS_URL = "https://www.ebi.ac.uk/pdbe/download/sifts/csv/"
22
+ KEGG_URL = "https://rest.kegg.jp/get/"
23
+ RHEA_URL = "https://www.rhea-db.org/download/rhea/"
24
+ PDBE_URL = "https://www.ebi.ac.uk/pdbe/download/"
25
+ MCSA_URL = "https://www.ebi.ac.uk/thornton-srv/m-csa/download/"
26
+
27
+ def __init__(self, data_dir: Path):
28
+ """Initialize data downloader.
29
+
30
+ Args:
31
+ data_dir: Root directory for CSA-3D data
32
+ """
33
+ self.data_dir = Path(data_dir)
34
+ self.data_dir.mkdir(parents=True, exist_ok=True)
35
+
36
+ def download_file(self, url: str, output_path: Path) -> bool:
37
+ """Download a file from URL.
38
+
39
+ Args:
40
+ url: URL to download from
41
+ output_path: Path to save file
42
+
43
+ Returns:
44
+ True if successful, False otherwise
45
+ """
46
+ output_path.parent.mkdir(parents=True, exist_ok=True)
47
+
48
+ try:
49
+ logger.info(f"Downloading {url} → {output_path}")
50
+ urllib.request.urlretrieve(url, output_path)
51
+ logger.info(f"✓ Downloaded {output_path.name}")
52
+ return True
53
+ except urllib.error.URLError as e:
54
+ logger.warning(f"✗ Failed to download {url}: {e}")
55
+ return False
56
+ except Exception as e:
57
+ logger.error(f"Unexpected error downloading {url}: {e}")
58
+ return False
59
+
60
+ def setup_sifts(self) -> bool:
61
+ """Setup SIFTS (PDB-UniProt mappings).
62
+
63
+ Downloads from EBI PDBe SIFTS data.
64
+
65
+ Returns:
66
+ True if successful
67
+ """
68
+ logger.info("Setting up SIFTS data...")
69
+ sifts_dir = self.data_dir / "sifts"
70
+ sifts_dir.mkdir(parents=True, exist_ok=True)
71
+
72
+ files = {
73
+ "uniprot_pdb.csv": "uniprot_pdb.csv",
74
+ "pdb_chain_uniprot.csv": "pdb_chain_uniprot.csv",
75
+ "pdb_chain_enzyme.csv": "pdb_chain_enzyme.csv",
76
+ }
77
+
78
+ success = True
79
+ for filename, remote_name in files.items():
80
+ url = urljoin(self.SIFTS_URL, remote_name)
81
+ output = sifts_dir / filename
82
+ if not self.download_file(url, output):
83
+ success = False
84
+
85
+ return success
86
+
87
+ def setup_kegg(self) -> bool:
88
+ """Setup KEGG reaction data.
89
+
90
+ Returns:
91
+ True if successful
92
+ """
93
+ logger.info("Setting up KEGG data...")
94
+ kegg_dir = self.data_dir / "kegg"
95
+ kegg_dir.mkdir(parents=True, exist_ok=True)
96
+
97
+ # KEGG reactions endpoint
98
+ output = kegg_dir / "kegg_reaction_components.csv"
99
+ logger.info("Note: KEGG data requires special API handling")
100
+ logger.info("Please obtain KEGG reaction data from: https://www.kegg.jp/")
101
+ return False
102
+
103
+ def setup_rhea(self) -> bool:
104
+ """Setup RHEA reaction data.
105
+
106
+ Downloads from RHEA database.
107
+
108
+ Returns:
109
+ True if successful
110
+ """
111
+ logger.info("Setting up RHEA data...")
112
+ rhea_dir = self.data_dir / "rhea"
113
+ rhea_dir.mkdir(parents=True, exist_ok=True)
114
+
115
+ url = urljoin(self.RHEA_URL, "rhea_reaction_components.csv")
116
+ output = rhea_dir / "rhea_reaction_components.csv"
117
+ return self.download_file(url, output)
118
+
119
+ def setup_pdbe(self) -> bool:
120
+ """Setup PDBe data (ions, cofactors, resolution).
121
+
122
+ Returns:
123
+ True if successful
124
+ """
125
+ logger.info("Setting up PDBe data...")
126
+ pdbe_dir = self.data_dir / "pdbe"
127
+ pdbe_dir.mkdir(parents=True, exist_ok=True)
128
+
129
+ files = {
130
+ "pdb_ions.csv": "pdb_ions.csv",
131
+ "pdb_cofactors.csv": "pdb_cofactors.csv",
132
+ "resolu.idx": "resolu.idx",
133
+ }
134
+
135
+ success = True
136
+ for filename, remote_name in files.items():
137
+ url = urljoin(self.PDBE_URL, remote_name)
138
+ output = pdbe_dir / filename
139
+ if not self.download_file(url, output):
140
+ success = False
141
+
142
+ return success
143
+
144
+ def setup_mcsa(self) -> bool:
145
+ """Setup M-CSA catalytic site data.
146
+
147
+ Downloads from M-CSA database.
148
+
149
+ Returns:
150
+ True if successful
151
+ """
152
+ logger.info("Setting up M-CSA data...")
153
+ mcsa_dir = self.data_dir / "mcsa"
154
+ mcsa_dir.mkdir(parents=True, exist_ok=True)
155
+
156
+ url = urljoin(self.MCSA_URL, "mcsa_cofactors.csv")
157
+ output = mcsa_dir / "mcsa_cofactors.csv"
158
+ return self.download_file(url, output)
159
+
160
+ def setup_all(self, skip_optional: bool = False) -> dict:
161
+ """Setup all data files.
162
+
163
+ Args:
164
+ skip_optional: Skip optional data sources (KEGG)
165
+
166
+ Returns:
167
+ Dictionary with setup status for each source
168
+ """
169
+ logger.info(f"Starting automatic CSA-3D data setup to {self.data_dir}")
170
+
171
+ results = {}
172
+ results["sifts"] = self.setup_sifts()
173
+ results["rhea"] = self.setup_rhea()
174
+ results["pdbe"] = self.setup_pdbe()
175
+ results["mcsa"] = self.setup_mcsa()
176
+
177
+ if not skip_optional:
178
+ results["kegg"] = self.setup_kegg()
179
+
180
+ # Summary
181
+ logger.info("\n" + "=" * 60)
182
+ logger.info("Data Setup Summary:")
183
+ for source, success in results.items():
184
+ status = "✓ Complete" if success else "✗ Failed/Skipped"
185
+ logger.info(f" {source.upper()}: {status}")
186
+ logger.info("=" * 60)
187
+
188
+ return results
189
+
190
+
191
+ def setup_data(data_dir: Optional[str] = None) -> bool:
192
+ """Setup CSA-3D data automatically.
193
+
194
+ Args:
195
+ data_dir: Directory for data files. Defaults to ~/.csa3d/data
196
+
197
+ Returns:
198
+ True if setup successful
199
+ """
200
+ if data_dir is None:
201
+ data_dir = str(Path.home() / ".csa3d" / "data")
202
+
203
+ logger.info(f"Setting up CSA-3D data in: {data_dir}")
204
+
205
+ downloader = DataDownloader(Path(data_dir))
206
+ results = downloader.setup_all()
207
+
208
+ success = all(results.values())
209
+ if success:
210
+ logger.info(f"\n✓ Data setup complete!")
211
+ logger.info(f"Set environment variable: export CSA3D_DATA_DIR={data_dir}")
212
+ else:
213
+ logger.warning("\n⚠ Some data sources failed to download")
214
+ logger.info("Please check logs above and retry")
215
+
216
+ return success
217
+
218
+
219
+ if __name__ == "__main__":
220
+ logging.basicConfig(
221
+ level=logging.INFO,
222
+ format="%(levelname)s: %(message)s"
223
+ )
224
+ setup_data()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csa3d
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Extract and analyze enzyme catalytic sites in 3D from the Mechanism and Catalytic Site Atlas (M-CSA)
5
5
  Author-email: Ioannis Riziotis <riziotis@ebi.ac.uk>
6
6
  License: Apache-2.0
@@ -30,9 +30,10 @@ Requires-Dist: scipy>=1.7.0
30
30
  Requires-Dist: scikit-learn>=0.24.0
31
31
  Requires-Dist: rdkit>=2021.09.0
32
32
  Requires-Dist: natsort>=8.0.0
33
- Requires-Dist: dendrogram-cut>=0.1.0
34
- Requires-Dist: parity-core>=0.5.0
35
33
  Requires-Dist: rmsd>=1.4.0
34
+ Provides-Extra: clustering
35
+ Requires-Dist: matplotlib>=3.5; extra == "clustering"
36
+ Requires-Dist: plotly>=5.0; extra == "clustering"
36
37
  Provides-Extra: dev
37
38
  Requires-Dist: pytest>=7.0; extra == "dev"
38
39
  Requires-Dist: pytest-cov>=4.0; extra == "dev"
@@ -105,28 +106,43 @@ pip install -e ".[dev,docs]"
105
106
 
106
107
  ## Quick Start
107
108
 
108
- ### 1. Configure Data Directory
109
+ ### 1. Automatic Data Setup (Recommended)
109
110
 
110
- First, you need to set up the M-CSA data files. You can either:
111
+ Download and setup M-CSA data automatically:
111
112
 
112
- **Option A: Download automatically (recommended)**
113
113
  ```python
114
- from csa3d import config
115
- config.setup_data("/path/to/csa3d_data") # Downloads ~50 GB
114
+ from csa3d import setup_data
115
+
116
+ # Downloads ~50 GB of data to ~/.csa3d/data
117
+ # Set up specific location with:
118
+ # setup_data("/path/to/csa3d_data")
119
+ success = setup_data()
120
+
121
+ if success:
122
+ print("✓ Data setup complete!")
116
123
  ```
117
124
 
118
- **Option B: Set environment variable**
125
+ Or via command line:
126
+ ```bash
127
+ python -m csa3d.data_setup /path/to/csa3d_data
128
+ ```
129
+
130
+ ### 2. Manual Data Configuration
131
+
132
+ If you prefer to download data manually:
133
+
134
+ **Option A: Environment variable**
119
135
  ```bash
120
136
  export CSA3D_DATA_DIR="/path/to/csa3d_data"
121
137
  ```
122
138
 
123
- **Option C: Set in code**
139
+ **Option B: Set in code**
124
140
  ```python
125
- from csa3d import config
126
- config.set_data_dir("/path/to/csa3d_data")
141
+ from csa3d import set_data_dir
142
+ set_data_dir("/path/to/csa3d_data")
127
143
  ```
128
144
 
129
- ### 2. Load and Inspect Active Sites
145
+ ### 3. Load and Inspect Active Sites
130
146
 
131
147
  ```python
132
148
  from csa3d import Mcsa
@@ -151,7 +167,7 @@ for entry in db:
151
167
  print(f" Resolution: {site.resolution} Å")
152
168
  ```
153
169
 
154
- ### 3. Clustering and Template Creation
170
+ ### 4. Clustering and Template Creation
155
171
 
156
172
  ```python
157
173
  # Build multiple entries
@@ -1,11 +1,15 @@
1
1
  LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
+ csa3d/__init__.py
5
+ csa3d/data_setup.py
4
6
  csa3d.egg-info/PKG-INFO
5
7
  csa3d.egg-info/SOURCES.txt
6
8
  csa3d.egg-info/dependency_links.txt
7
9
  csa3d.egg-info/requires.txt
8
10
  csa3d.egg-info/top_level.txt
11
+ csa3d/_vendor/__init__.py
12
+ csa3d/_vendor/dendrogram_cut.py
9
13
  csa3d/config/__init__.py
10
14
  csa3d/config/loader.py
11
15
  csa3d/config/settings.py
@@ -5,10 +5,12 @@ scipy>=1.7.0
5
5
  scikit-learn>=0.24.0
6
6
  rdkit>=2021.09.0
7
7
  natsort>=8.0.0
8
- dendrogram-cut>=0.1.0
9
- parity-core>=0.5.0
10
8
  rmsd>=1.4.0
11
9
 
10
+ [clustering]
11
+ matplotlib>=3.5
12
+ plotly>=5.0
13
+
12
14
  [dev]
13
15
  pytest>=7.0
14
16
  pytest-cov>=4.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "csa3d"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "Extract and analyze enzyme catalytic sites in 3D from the Mechanism and Catalytic Site Atlas (M-CSA)"
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}
@@ -34,12 +34,14 @@ dependencies = [
34
34
  "scikit-learn>=0.24.0",
35
35
  "rdkit>=2021.09.0",
36
36
  "natsort>=8.0.0",
37
- "dendrogram-cut>=0.1.0",
38
- "parity-core>=0.5.0",
39
37
  "rmsd>=1.4.0",
40
38
  ]
41
39
 
42
40
  [project.optional-dependencies]
41
+ clustering = [
42
+ "matplotlib>=3.5",
43
+ "plotly>=5.0",
44
+ ]
43
45
  dev = [
44
46
  "pytest>=7.0",
45
47
  "pytest-cov>=4.0",
@@ -68,7 +70,7 @@ Repository = "https://github.com/your-username/csa3d"
68
70
  "Issue Tracker" = "https://github.com/your-username/csa3d/issues"
69
71
 
70
72
  [tool.setuptools]
71
- packages = ["csa3d", "csa3d.config"]
73
+ packages = ["csa3d", "csa3d.config", "csa3d._vendor"]
72
74
 
73
75
  [tool.black]
74
76
  line-length = 100
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes