rcsb.workflow 0.65__tar.gz → 0.67__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.
Files changed (40) hide show
  1. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/HISTORY.txt +3 -0
  2. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/PKG-INFO +8 -4
  3. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/pyproject.toml +11 -6
  4. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/cli/ExDbExec.py +6 -1
  5. rcsb_workflow-0.67/rcsb/workflow/refstats/LigandQualityReferenceGenerator.py +342 -0
  6. rcsb_workflow-0.67/rcsb/workflow/refstats/ResidueRsccReferenceGenerator.py +860 -0
  7. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/tests/fixturePdbxLoader.py +9 -1
  8. rcsb_workflow-0.67/rcsb/workflow/tests/testLigandQualityReferenceGenerator.py +174 -0
  9. rcsb_workflow-0.67/rcsb/workflow/tests/testResidueRsccReferenceGenerator.py +260 -0
  10. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/wuw/ExDbWorkflow.py +21 -0
  11. rcsb_workflow-0.67/rcsb/workflow/wuw/__init__.py +0 -0
  12. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/.gitignore +0 -0
  13. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/LICENSE +0 -0
  14. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/README.md +0 -0
  15. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/__init__.py +0 -0
  16. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/__init__.py +0 -0
  17. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/bcif/__init__.py +0 -0
  18. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/bcif/task_functions.py +0 -0
  19. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/chem/ChemCompFileWorkflow.py +0 -0
  20. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/chem/ChemCompImageWorkflow.py +0 -0
  21. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/chem/ChemCompSearchIndexWorkflow.py +0 -0
  22. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/chem/__init__.py +0 -0
  23. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/cli/BcifExec.py +0 -0
  24. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/cli/ChemSearchIndexExec.py +0 -0
  25. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/cli/ImgExec.py +0 -0
  26. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/cli/__init__.py +0 -0
  27. {rcsb_workflow-0.65/rcsb/workflow/targets → rcsb_workflow-0.67/rcsb/workflow/refstats}/__init__.py +0 -0
  28. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/targets/ProteinTargetSequenceExecutionWorkflow.py +0 -0
  29. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/targets/ProteinTargetSequenceWorkflow.py +0 -0
  30. {rcsb_workflow-0.65/rcsb/workflow/tests → rcsb_workflow-0.67/rcsb/workflow/targets}/__init__.py +0 -0
  31. {rcsb_workflow-0.65/rcsb/workflow/wuw → rcsb_workflow-0.67/rcsb/workflow/tests}/__init__.py +0 -0
  32. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/tests/testBcifWorkflow.py +0 -0
  33. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/tests/testChemCompFileWorkflow.py +0 -0
  34. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/tests/testChemCompImageWorkflow.py +0 -0
  35. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/tests/testChemCompSearchIndexWorkflow.py +0 -0
  36. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/tests/testExDbWorkflow.py +0 -0
  37. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/tests/testProteinTargetSequenceWorkflow.py +0 -0
  38. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/wuw/BcifWorkflow.py +0 -0
  39. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/wuw/PdbCsmImageWorkflow.py +0 -0
  40. {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/wuw/WuwUtils.py +0 -0
@@ -59,3 +59,6 @@
59
59
  15-Oct-2025 - V0.63 Add ChemSearchIndex from weekly-update-workflow so it can be a cli call instead
60
60
  8-Dec-2025 - V0.64 Switch to 'pyproject.toml' file and update test pipelines to python 3.12
61
61
  6-Jan-2026 - V0.65 Reduce resource utilization for test suites
62
+ 2-Mar-2026 - V0.66 Add refstats subpackage for ligand and RSCC reference generation;
63
+ Update test pipelines to python 3.13
64
+ 7-May-2026 - V0.67 Configure logging at start of CLI executable scripts
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rcsb.workflow
3
- Version: 0.65
3
+ Version: 0.67
4
4
  Summary: RCSB Python data processing and ETL/ELT workflow entry points
5
5
  Project-URL: Homepage, https://github.com/rcsb/py-rcsb_workflow
6
6
  Author-email: John Westbrook <john.westbrook@rcsb.org>
@@ -12,18 +12,22 @@ Classifier: Intended Audience :: Developers
12
12
  Classifier: Natural Language :: English
13
13
  Classifier: Programming Language :: Python
14
14
  Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
16
  Requires-Python: >=3.9
17
17
  Requires-Dist: mmcif>=1.0.0
18
+ Requires-Dist: numpy>=2.1.3
18
19
  Requires-Dist: openeye-toolkits>=2024.1.1
19
- Requires-Dist: rcsb-db>=1.812
20
- Requires-Dist: rcsb-exdb>=1.33
20
+ Requires-Dist: pymongo>=4.8.0
21
+ Requires-Dist: rcsb-db>=1.814
22
+ Requires-Dist: rcsb-exdb>=1.35
21
23
  Requires-Dist: rcsb-utils-chem>=0.84
24
+ Requires-Dist: rcsb-utils-chemref>=0.98
22
25
  Requires-Dist: rcsb-utils-io>=1.49
23
26
  Requires-Dist: rcsb-utils-seq>=0.84
24
27
  Requires-Dist: rcsb-utils-seqalign>=0.32
25
28
  Requires-Dist: rcsb-utils-targets>=0.83
26
29
  Requires-Dist: requests>=2.32.3
30
+ Requires-Dist: scikit-learn>=1.8.0
27
31
  Provides-Extra: tests
28
32
  Requires-Dist: black>=21.5b1; extra == 'tests'
29
33
  Requires-Dist: check-manifest; extra == 'tests'
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
5
5
  [project]
6
6
  name = "rcsb.workflow"
7
7
  description = "RCSB Python data processing and ETL/ELT workflow entry points"
8
- version = "0.65"
8
+ version = "0.67"
9
9
  readme = "README.md"
10
10
  authors = [
11
11
  { name="John Westbrook", email="john.westbrook@rcsb.org" }
@@ -17,29 +17,34 @@ maintainers = [
17
17
  ]
18
18
  license = "Apache-2.0"
19
19
  license-files = ["LICENSE"]
20
- requires-python = ">=3.9" # Latest OpenEye (2025.1.1) only supports up to 3.12 (as of Nov. 2025)
20
+ requires-python = ">=3.9"
21
21
  classifiers = [
22
22
  "Development Status :: 5 - Production/Stable",
23
23
  "Intended Audience :: Developers",
24
24
  "Natural Language :: English",
25
25
  "Programming Language :: Python",
26
26
  "Programming Language :: Python :: 3",
27
- "Programming Language :: Python :: 3.12", # Latest OpenEye (2025.1.1) only supports up to 3.12 (as of Nov. 2025)
27
+ "Programming Language :: Python :: 3.13", # Latest OpenEye (2025.2) supports up to 3.13
28
28
  ]
29
29
 
30
30
  dependencies = [
31
31
  # OpenEye is needed here despite it not being a direct requirement of this package (it's used by rcsb.utils.chem)
32
32
  "OpenEye-toolkits >= 2024.1.1",
33
33
  #
34
+ "requests >= 2.32.3",
35
+ "scikit-learn >= 1.8.0",
36
+ "numpy >= 2.1.3",
37
+ "pymongo >= 4.8.0",
38
+ #
34
39
  "mmcif >= 1.0.0",
35
40
  "rcsb.utils.io >= 1.49",
36
- "rcsb.db >= 1.812",
41
+ "rcsb.db >= 1.814",
37
42
  "rcsb.utils.chem >= 0.84",
38
43
  "rcsb.utils.seq >= 0.84",
39
44
  "rcsb.utils.targets >= 0.83",
45
+ "rcsb.utils.chemref >= 0.98",
40
46
  "rcsb.utils.seqalign >= 0.32",
41
- "rcsb.exdb >= 1.33",
42
- "requests >= 2.32.3",
47
+ "rcsb.exdb >= 1.35",
43
48
  ]
44
49
 
45
50
  [project.optional-dependencies]
@@ -14,6 +14,7 @@
14
14
  # 22-Oct-2024 dwp Add ccd_img_gen and ccd_file_gen operations
15
15
  # (latter will only be used briefly, as will stop generating SDF and Mol2 files in Dec 2024)
16
16
  # 31-Jan-2025 mjt Moved this script from rcsb.exdb (to remove circluar dependencies)
17
+ # 10-Jan-2025 cs Add ligand_quality_ref_gen and residue_rscc_ref_gen operations
17
18
  ##
18
19
  __docformat__ = "google en"
19
20
  __author__ = "John Westbrook"
@@ -31,7 +32,7 @@ from rcsb.workflow.wuw.ExDbWorkflow import ExDbWorkflow
31
32
  HERE = os.path.abspath(os.path.dirname(__file__))
32
33
  TOPDIR = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
33
34
 
34
- # logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s]-%(module)s.%(funcName)s: %(message)s", stream=sys.stdout)
35
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s]-%(module)s.%(funcName)s: %(message)s")
35
36
  logger = logging.getLogger()
36
37
 
37
38
 
@@ -58,6 +59,8 @@ def main():
58
59
  "upd_resource_stash",
59
60
  "ccd_img_gen",
60
61
  "ccd_file_gen",
62
+ "ligand_quality_ref_gen",
63
+ "residue_rscc_ref_gen",
61
64
  ]
62
65
  )
63
66
  parser.add_argument(
@@ -129,6 +132,8 @@ def main():
129
132
  "upd_entry_info",
130
133
  "upd_glycan_idx",
131
134
  "upd_resource_stash",
135
+ "ligand_quality_ref_gen",
136
+ "residue_rscc_ref_gen",
132
137
  ]:
133
138
  okR = exWf.buildExdbResource(op, **loadD)
134
139
  elif op in [
@@ -0,0 +1,342 @@
1
+ ##
2
+ # File: LigandQualityReferenceGenerator.py
3
+ # Date: 2025-12-25 Chenghua Shao
4
+ #
5
+ # Update:
6
+ ##
7
+ """
8
+ Generate ligand quality reference data that is used for ligand quality score computation.
9
+
10
+ """
11
+ import os
12
+ import csv
13
+ import logging
14
+ import time
15
+ import numpy as np
16
+ from sklearn.decomposition import PCA
17
+ from sklearn.preprocessing import StandardScaler
18
+
19
+ from rcsb.utils.io.FileUtil import FileUtil
20
+ from rcsb.db.mongo.Connection import Connection
21
+ from rcsb.utils.chemref.RcsbLigandScoreProvider import RcsbLigandScoreProvider
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class DatabaseError(Exception):
27
+ """Custom exception class for errors in MongoDB connection and query execution."""
28
+
29
+
30
+ class AnalysisError(Exception):
31
+ """Custom exception class for errors during data analysis."""
32
+
33
+
34
+ class OutputError(Exception):
35
+ """Custom exception class for errors during output file writing."""
36
+
37
+
38
+ class LigandQualityReferenceGenerator:
39
+ """
40
+ This class generates ligand quality reference data by performing the following steps:
41
+ 1. Query ligand quality metrics from RCSB MongoDB resource;
42
+ 2. Process the ligand quality data by filtering, aggregating, and formatting;
43
+ 3. Generate ligand reference data through PCA;
44
+ Attributes:
45
+ data: final output of each pdb_ligand combination and their quality scores
46
+ qdata: direct data from MongoDB query
47
+ Methods:
48
+ fetchLigand(pdb_id): Fetch, filter, reduce, and reformat ligand quality metrics
49
+ analyze(): Run PCA on the filtered and reduced ligand quality scores to generate reference data.
50
+ generate(pdb_ids): Full pipeline to generate ligand quality reference data by running the steps of
51
+ fetch -> analyze.
52
+ writeReference(output_file): Write the generated ligand quality reference data to a csv file.
53
+ """
54
+ def __init__(self, cfgOb, cachePath, **kwargs):
55
+ """Initiate the class and the MongoDB connection"""
56
+ self.__cfgOb = cfgOb
57
+ self.__configName = kwargs.get("configName", "site_info_configuration")
58
+ self.__cachePath = cachePath
59
+ self.__dirName = "rcsb-ligand-score"
60
+ self.__dirPath = os.path.join(self.__cachePath, self.__dirName)
61
+ #
62
+ self.__resourceName = "MONGO_DB"
63
+ self.__databaseName = kwargs.get("databaseName", "pdbx_core")
64
+ self.__collectionName = kwargs.get("collectionName", "pdbx_core_nonpolymer_entity_instance")
65
+ #
66
+ # initilize output data of each pdb_ligand combination and their quality scores, which is a list of
67
+ # dictionaries with keys of pdb_ligand, mogul_bonds_RMSZ, mogul_angles_RMSZ, RSR, RSCC, fit_pc1, geo_pc1
68
+ self.refDataL = []
69
+ #
70
+ # initialize query data record from MongoDB, which is a list of dictionaries with keys of pdb_ligand, mogul_bonds_RMSZ,
71
+ # mogul_angles_RMSZ, RSR, RSCC, and count (number of instances for the same ligand in the same PDB entry)
72
+ self.qDataL = None
73
+
74
+ def generate(self, pdb_ids: list[str] = None, backup: bool = False) -> bool:
75
+ """
76
+ Full pipeline to generate ligand quality reference data by running the steps of
77
+ query -> filter -> reduce -> analyze.
78
+
79
+ Args:
80
+ pdb_ids (list): List of specified PDB IDs, default to [] which leads to all PDB structures being queried.
81
+ backup (bool): Whether to backup generated file to stash (default False).
82
+
83
+ Returns:
84
+ (bool): True for success or False otherwise
85
+ Also updates/modifies the self.qDataL and self.refDataL attributes.
86
+ """
87
+ #
88
+ # fetch ligand quality data from MongoDB, and process the data
89
+ try:
90
+ self.qDataL = self.fetchLigand(pdb_ids=pdb_ids)
91
+ except DatabaseError as e:
92
+ logger.error("Failed to fetch ligand quality data, STOP. ERROR: %s", e)
93
+ return False
94
+ # verify data is not empty
95
+ if not self.qDataL:
96
+ logger.error("no data fetched from MongoDB, cannot analyze, STOP")
97
+ return False
98
+ # analyze the data to generate reference scores
99
+ try:
100
+ self.refDataL = self.analyzeLigand(self.qDataL)
101
+ except AnalysisError as e:
102
+ logger.error("Failed to analyze ligand quality data, STOP. ERROR: %s", e)
103
+ return False
104
+ # output the reference data to file
105
+ try:
106
+ rlsP = RcsbLigandScoreProvider(cachePath=self.__cachePath, useCache=True)
107
+ output_file = rlsP.getLigandScoreDataPath()
108
+ self.writeReference(output_file=output_file)
109
+ except OutputError as e:
110
+ logger.error("Failed to write ligand quality reference data to file, STOP. ERROR: %s", e)
111
+ return False
112
+ #
113
+ logger.info("Finished generation workflow for ligand quality reference data.")
114
+ ok = True
115
+ #
116
+ logger.info("Refreshing RcsbLigandScoreProvider instance...")
117
+ rlsP.reload(useCache=True)
118
+ if backup and rlsP.testCache():
119
+ logger.info("Backing up data to stash...")
120
+ okB = rlsP.backup(self.__cfgOb, self.__configName, useStash=True, useGit=False)
121
+ logger.info("%r ligand score backup status (%r)", self.__dirName, okB)
122
+ ok = ok and okB
123
+ #
124
+ return ok
125
+
126
+ def fetchLigand(self, pdb_ids: list[str] = None):
127
+ """
128
+ Fetch, filter, reduce, and reformat ligand quality metrics for given PDB IDs, defaulting to all structures.
129
+ Fetch ligand quality metrics from MongoDB;
130
+ Filter the queried ligand quality scores based on defined criteria;
131
+ Reduce the data by combining multiple instances of the same ligand in the same PDB entry;
132
+ Reformat through projection.
133
+
134
+ :param pdb_ids: List of specified PDB IDs, default to [] which leads to all PDB structures being queried.
135
+ """
136
+ qdata = None
137
+
138
+ if pdb_ids:
139
+ # Match the PDB entries by provided IDs
140
+ pipeline = [{
141
+ "$match": {
142
+ "rcsb_nonpolymer_entity_instance_container_identifiers.entry_id": {
143
+ "$in": pdb_ids
144
+ }
145
+ }
146
+ }]
147
+ else:
148
+ # With no matching PDB IDs provided, All PDB entries are chosen
149
+ pipeline = []
150
+
151
+ pipeline.extend([
152
+ # Project combined ID and filter scores
153
+ {
154
+ "$project": {
155
+ "_id": 0,
156
+ "id": {
157
+ "$concat": [
158
+ "$rcsb_nonpolymer_entity_instance_container_identifiers.entry_id",
159
+ "-",
160
+ "$rcsb_nonpolymer_entity_instance_container_identifiers.comp_id"
161
+ ]
162
+ },
163
+ "scores": {
164
+ "$filter": {
165
+ "input": "$rcsb_nonpolymer_instance_validation_score",
166
+ "as": "s",
167
+ "cond": {
168
+ "$and": [
169
+ {"$gte": ["$$s.average_occupancy", 0.9]},
170
+ {"$lte": ["$$s.average_occupancy", 1]},
171
+ {"$gte": ["$$s.completeness", 0.9]},
172
+ {"$lte": ["$$s.completeness", 1]},
173
+ {"$gt": ["$$s.mogul_bonds_RMSZ", 0]},
174
+ {"$lte": ["$$s.mogul_bonds_RMSZ", 10]},
175
+ {"$gt": ["$$s.mogul_angles_RMSZ", 0]},
176
+ {"$lte": ["$$s.mogul_angles_RMSZ", 10]},
177
+ {"$gt": ["$$s.RSR", 0]},
178
+ {"$lte": ["$$s.RSR", 1]},
179
+ {"$gt": ["$$s.RSCC", 0]},
180
+ {"$lte": ["$$s.RSCC", 1]}
181
+ ]
182
+ }
183
+ }
184
+ }
185
+ }
186
+ },
187
+
188
+ # Remove documents with no valid scores
189
+ {"$match": {"scores.0": {"$exists": True}}},
190
+
191
+ # Unwind and group by ID to calculate averages
192
+ {"$unwind": "$scores"},
193
+ {
194
+ "$group": {
195
+ "_id": "$id",
196
+ "mogul_bonds_RMSZ": {"$avg": "$scores.mogul_bonds_RMSZ"},
197
+ "mogul_angles_RMSZ": {"$avg": "$scores.mogul_angles_RMSZ"},
198
+ "RSR": {"$avg": "$scores.RSR"},
199
+ "RSCC": {"$avg": "$scores.RSCC"},
200
+ "count": {"$sum": 1}
201
+ }
202
+ },
203
+
204
+ # Final projection
205
+ {
206
+ "$project": {
207
+ "_id": 0,
208
+ "pdb_ligand": "$_id",
209
+ "mogul_bonds_RMSZ": 1,
210
+ "mogul_angles_RMSZ": 1,
211
+ "RSR": 1,
212
+ "RSCC": 1,
213
+ "count": 1
214
+ }
215
+ },
216
+
217
+ # Optional limit
218
+ # {"$limit": 10}
219
+ ])
220
+
221
+ # Run aggregation
222
+ with Connection(cfgOb=self.__cfgOb, resourceName=self.__resourceName) as client:
223
+ try:
224
+ db = client[self.__databaseName]
225
+ collection = db[self.__collectionName]
226
+ startTime = time.time()
227
+ qdata = list(collection.aggregate(pipeline))
228
+ logger.info(
229
+ "Fetched %s unique pdb_ligand combination records from MongoDB: %s, collectionName: %s at %s (%.4f seconds)",
230
+ len(qdata),
231
+ self.__databaseName,
232
+ self.__collectionName,
233
+ time.strftime("%Y %m %d %H:%M:%S", time.localtime()),
234
+ time.time() - startTime,
235
+ )
236
+ except Exception as e:
237
+ qdata = None
238
+ raise DatabaseError(f"Failed to run aggregation to fetch MongoDB ligand quality data. ERROR {e}") from e
239
+
240
+ return qdata
241
+
242
+ def analyzeLigand(self, qDataL: list):
243
+ """
244
+ Run PCA on the filtered and reduced ligand quality scores to generate reference data.
245
+ The first principal component (PC1) is used as the ligand quality reference score.
246
+
247
+ """
248
+ refDataL = []
249
+ # Prepare data for PCA, convert each score type into a separate list for matrix construction
250
+ l_pdb_ligand = []
251
+ l_mogul_bonds_RMSZ = []
252
+ l_mogul_angles_RMSZ = []
253
+ l_RSR = []
254
+ l_RSCC = []
255
+ for d_lig in qDataL:
256
+ l_pdb_ligand.append(d_lig["pdb_ligand"])
257
+ l_mogul_bonds_RMSZ.append(d_lig["mogul_bonds_RMSZ"])
258
+ l_mogul_angles_RMSZ.append(d_lig["mogul_angles_RMSZ"])
259
+ l_RSR.append(d_lig["RSR"])
260
+ l_RSCC.append(d_lig["RSCC"])
261
+ # Construct data matrices for geometry and fit scores
262
+ matrix_geo = np.column_stack([l_mogul_bonds_RMSZ, l_mogul_angles_RMSZ])
263
+ matrix_fit = np.column_stack([l_RSR, l_RSCC])
264
+ # Run PCA separately on geometry and fit scores
265
+ try:
266
+ logging.info("To Run PCA on geometry scores")
267
+ l_geo_pc1 = self.runPca(matrix_geo)
268
+ logging.info("To Run PCA on fit scores")
269
+ l_fit_pc1 = self.runPca(matrix_fit)
270
+ except Exception as e:
271
+ raise AnalysisError(f"Failed to run PCA on ligand quality metrics. ERROR {e}") from e
272
+ # Combine the two PC1 scores into a final reference score
273
+ for i, pdb_ligand in enumerate(l_pdb_ligand):
274
+ ref_score = {
275
+ "pdb_ligand": pdb_ligand,
276
+ "rsr": round(l_RSR[i], 4),
277
+ "rscc": round(l_RSCC[i], 4),
278
+ "mogul_bonds_rmsz": round(l_mogul_bonds_RMSZ[i], 4),
279
+ "mogul_angles_rmsz": round(l_mogul_angles_RMSZ[i], 4),
280
+ "fit_pc1": round(l_fit_pc1[i], 5),
281
+ "geo_pc1": round(l_geo_pc1[i], 5)
282
+ }
283
+ refDataL.append(ref_score)
284
+ logger.info("%s unique pdb-ligand pairs analzed by PCA", len(refDataL))
285
+
286
+ return refDataL
287
+
288
+ def runPca(self, xarr: np.ndarray) -> list:
289
+ """
290
+ Run PCA on numpy matrix and output the first principal components.
291
+ Loadings and explained variance can be examined in log but not in outcome.
292
+ Ensure lower scores correspond to better quality by adjusting the sign of PC1 if needed.
293
+
294
+ :param xarr: numpy matrix of any dimension
295
+ :return: first principal components as a list
296
+ """
297
+ # Scale the data
298
+ scaler = StandardScaler()
299
+ xstd = scaler.fit_transform(xarr)
300
+ # Perform PCA
301
+ pca = PCA(n_components=1)
302
+ principal_components = pca.fit_transform(xstd)
303
+ logger.info("Explained variance ratios: %s", pca.explained_variance_ratio_) # should be > 0.7(70%) for ligand geo and fit PC1
304
+ loadings = pca.components_[0]
305
+ logger.info("Resulting PCA loadings: %s", loadings) # should be [sqrt(0.5), sqrt(0.5)] for scaled PCA or with minus sign on one component
306
+ # Code below ensures loading on the first variable (RSR for fitting, and mogul_bonds_RMSZ for geometry) to be positive,
307
+ # This is because PCA direction is arbitrary, i.e. 0.707,-0.707 and -0.707,0.707 are equivalent, but we want to ensure
308
+ # lower scores correspond to better quality, which gives a correct direction for subsequent pecentile calculation.
309
+ if loadings[0] < 0:
310
+ loadings = -loadings
311
+ logger.info("Adjusted PCA loadings: %s", loadings)
312
+ principal_components = -principal_components
313
+ logger.info("Change sign of the first principal components to ensure correct directionality.")
314
+ return principal_components[:, 0].tolist()
315
+
316
+ def writeReference(self, output_file: str):
317
+ """Write the generated ligand quality reference data to a csv file.
318
+
319
+ Args:
320
+ output_file (str): Path to the output csv file.
321
+ """
322
+ fU = FileUtil()
323
+ fU.mkdir(self.__dirPath)
324
+ #
325
+ if not self.refDataL:
326
+ raise OutputError("No data to write. Please run generate() first.")
327
+ if type(self.refDataL) is not list:
328
+ raise OutputError("Data format incorrect. Expected a list of dictionaries after generate().")
329
+ if type(self.refDataL[0]) is not dict:
330
+ raise OutputError("Data format incorrect. Expected a list of dictionaries after generate().")
331
+ #
332
+ fieldnames = self.refDataL[0].keys()
333
+ #
334
+ try:
335
+ with open(output_file, mode="w", newline="", encoding="utf-8") as file:
336
+ writer = csv.DictWriter(file, fieldnames=fieldnames)
337
+ writer.writeheader() # write column headers
338
+ writer.writerows(self.refDataL) # write all rows
339
+ except Exception as e:
340
+ raise OutputError(f"Failed to write ligand quality reference data to file: {e}") from e
341
+ #
342
+ logger.info("Finished generating ligand quality reference data file at %s", output_file)