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.
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/HISTORY.txt +3 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/PKG-INFO +8 -4
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/pyproject.toml +11 -6
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/cli/ExDbExec.py +6 -1
- rcsb_workflow-0.67/rcsb/workflow/refstats/LigandQualityReferenceGenerator.py +342 -0
- rcsb_workflow-0.67/rcsb/workflow/refstats/ResidueRsccReferenceGenerator.py +860 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/tests/fixturePdbxLoader.py +9 -1
- rcsb_workflow-0.67/rcsb/workflow/tests/testLigandQualityReferenceGenerator.py +174 -0
- rcsb_workflow-0.67/rcsb/workflow/tests/testResidueRsccReferenceGenerator.py +260 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/wuw/ExDbWorkflow.py +21 -0
- rcsb_workflow-0.67/rcsb/workflow/wuw/__init__.py +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/.gitignore +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/LICENSE +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/README.md +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/__init__.py +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/__init__.py +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/bcif/__init__.py +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/bcif/task_functions.py +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/chem/ChemCompFileWorkflow.py +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/chem/ChemCompImageWorkflow.py +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/chem/ChemCompSearchIndexWorkflow.py +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/chem/__init__.py +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/cli/BcifExec.py +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/cli/ChemSearchIndexExec.py +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/cli/ImgExec.py +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/cli/__init__.py +0 -0
- {rcsb_workflow-0.65/rcsb/workflow/targets → rcsb_workflow-0.67/rcsb/workflow/refstats}/__init__.py +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/targets/ProteinTargetSequenceExecutionWorkflow.py +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/targets/ProteinTargetSequenceWorkflow.py +0 -0
- {rcsb_workflow-0.65/rcsb/workflow/tests → rcsb_workflow-0.67/rcsb/workflow/targets}/__init__.py +0 -0
- {rcsb_workflow-0.65/rcsb/workflow/wuw → rcsb_workflow-0.67/rcsb/workflow/tests}/__init__.py +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/tests/testBcifWorkflow.py +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/tests/testChemCompFileWorkflow.py +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/tests/testChemCompImageWorkflow.py +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/tests/testChemCompSearchIndexWorkflow.py +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/tests/testExDbWorkflow.py +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/tests/testProteinTargetSequenceWorkflow.py +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/wuw/BcifWorkflow.py +0 -0
- {rcsb_workflow-0.65 → rcsb_workflow-0.67}/rcsb/workflow/wuw/PdbCsmImageWorkflow.py +0 -0
- {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.
|
|
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.
|
|
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:
|
|
20
|
-
Requires-Dist: rcsb-
|
|
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.
|
|
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"
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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)
|