mdprep 0.2.0__py3-none-any.whl
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.
- mdprep/__init__.py +7 -0
- mdprep/_selftest_data/basic_protein.yaml +33 -0
- mdprep/_selftest_data/ligand_sub.mol2 +11 -0
- mdprep/ambertools/__init__.py +2 -0
- mdprep/ambertools/antechamber.py +110 -0
- mdprep/ambertools/commands.py +33 -0
- mdprep/ambertools/mol2.py +415 -0
- mdprep/ambertools/parmchk2.py +89 -0
- mdprep/ambertools/resp.py +570 -0
- mdprep/charges/__init__.py +1 -0
- mdprep/charges/esp_grid.py +190 -0
- mdprep/cli.py +451 -0
- mdprep/config/__init__.py +9 -0
- mdprep/config/loader.py +146 -0
- mdprep/config/models.py +1100 -0
- mdprep/external/__init__.py +14 -0
- mdprep/external/discovery.py +14 -0
- mdprep/external/runner.py +55 -0
- mdprep/leap/__init__.py +2 -0
- mdprep/leap/builder.py +476 -0
- mdprep/leap/forcefields.py +93 -0
- mdprep/leap/ions.py +112 -0
- mdprep/leap/log_parser.py +170 -0
- mdprep/leap/report.py +99 -0
- mdprep/leap/residues.py +653 -0
- mdprep/leap/runner.py +65 -0
- mdprep/ligands/__init__.py +2 -0
- mdprep/ligands/extract.py +355 -0
- mdprep/ligands/identity.py +39 -0
- mdprep/ligands/pyscf_charges.py +242 -0
- mdprep/ligands/report.py +146 -0
- mdprep/ligands/workflow.py +838 -0
- mdprep/md/__init__.py +5 -0
- mdprep/md/roe_brooks.py +650 -0
- mdprep/metals/__init__.py +2 -0
- mdprep/metals/c4.py +157 -0
- mdprep/metals/coordination.py +313 -0
- mdprep/metals/gxtb_hessian.py +467 -0
- mdprep/metals/hydrogenate.py +344 -0
- mdprep/metals/mace_polar1_hessian.py +357 -0
- mdprep/metals/mace_polar1_worker.py +295 -0
- mdprep/metals/mcpb.py +949 -0
- mdprep/metals/mcpb_compat.py +125 -0
- mdprep/metals/nonbonded.py +294 -0
- mdprep/metals/parameter_comparison.py +563 -0
- mdprep/metals/pyscf_mcpb.py +1388 -0
- mdprep/metals/report.py +119 -0
- mdprep/metals/workflow.py +320 -0
- mdprep/protonation/__init__.py +15 -0
- mdprep/protonation/apply.py +968 -0
- mdprep/protonation/disulfide_states.py +141 -0
- mdprep/protonation/histidine_geometry.py +950 -0
- mdprep/protonation/histidine_xtb.py +390 -0
- mdprep/protonation/overrides.py +64 -0
- mdprep/protonation/pka_rules.py +165 -0
- mdprep/protonation/propka.py +174 -0
- mdprep/protonation/propka_parser.py +140 -0
- mdprep/protonation/report.py +187 -0
- mdprep/protonation/states.py +52 -0
- mdprep/protonation/temporary_hydrogenation.py +238 -0
- mdprep/protonation/xtb_parser.py +79 -0
- mdprep/protonation/xtb_runner.py +108 -0
- mdprep/qm/__init__.py +2 -0
- mdprep/qm/point_charges.py +233 -0
- mdprep/qm/pyscf_esp.py +70 -0
- mdprep/qm/pyscf_runner.py +570 -0
- mdprep/refinement/__init__.py +5 -0
- mdprep/refinement/ash.py +541 -0
- mdprep/refinement/coordinates.py +210 -0
- mdprep/refinement/mapping.py +163 -0
- mdprep/refinement/report.py +66 -0
- mdprep/refinement/selection.py +436 -0
- mdprep/refinement/workflow.py +188 -0
- mdprep/reports/__init__.py +8 -0
- mdprep/reports/structure_report.py +92 -0
- mdprep/structure/__init__.py +14 -0
- mdprep/structure/classify.py +92 -0
- mdprep/structure/disulfides.py +51 -0
- mdprep/structure/inspect.py +105 -0
- mdprep/structure/models.py +86 -0
- mdprep/structure/normalize.py +363 -0
- mdprep/structure/pdb.py +511 -0
- mdprep/structure/repair.py +566 -0
- mdprep/structure/selectors.py +204 -0
- mdprep/structure/writer.py +84 -0
- mdprep/validation/__init__.py +2 -0
- mdprep/validation/openmm_check.py +86 -0
- mdprep/validation/parmed_check.py +43 -0
- mdprep/validation/topology.py +239 -0
- mdprep/workflows/__init__.py +6 -0
- mdprep/workflows/init.py +242 -0
- mdprep/workflows/inspect.py +21 -0
- mdprep/workflows/prepare.py +763 -0
- mdprep/workflows/selftest.py +220 -0
- mdprep/workflows/validate.py +30 -0
- mdprep-0.2.0.dist-info/METADATA +264 -0
- mdprep-0.2.0.dist-info/RECORD +100 -0
- mdprep-0.2.0.dist-info/WHEEL +4 -0
- mdprep-0.2.0.dist-info/entry_points.txt +2 -0
- mdprep-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""Safe structure-only normalization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from mdprep.config.models import ManifestConfig
|
|
9
|
+
from mdprep.structure.classify import (
|
|
10
|
+
is_likely_ligand_or_cofactor,
|
|
11
|
+
is_nonstandard_nonwater_residue,
|
|
12
|
+
is_standard_protein_residue,
|
|
13
|
+
is_water_residue,
|
|
14
|
+
)
|
|
15
|
+
from mdprep.structure.inspect import InspectionSummary, inspect_pdb_structure
|
|
16
|
+
from mdprep.structure.models import AtomRecord, PdbStructure, ResidueRecord
|
|
17
|
+
from mdprep.structure.pdb import read_pdb
|
|
18
|
+
from mdprep.structure.repair import (
|
|
19
|
+
StructureRepairError,
|
|
20
|
+
StructureRepairResult,
|
|
21
|
+
repair_structure,
|
|
22
|
+
)
|
|
23
|
+
from mdprep.structure.selectors import SelectorError, resolve_atom_selector, resolve_residue_selector
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class StructureNormalizationError(ValueError):
|
|
27
|
+
"""Raised when structure normalization would require an unsafe decision."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class NormalizedLigand:
|
|
32
|
+
ligand_id: str
|
|
33
|
+
residue: dict[str, object]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class NormalizedMetalIon:
|
|
38
|
+
site_id: str
|
|
39
|
+
element: str
|
|
40
|
+
charge: int
|
|
41
|
+
atom: dict[str, object]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class StructureNormalizationResult:
|
|
46
|
+
input_path: Path
|
|
47
|
+
output_path: Path | None
|
|
48
|
+
input_summary: InspectionSummary
|
|
49
|
+
normalized_structure: PdbStructure
|
|
50
|
+
waters_kept: list[dict[str, object]] = field(default_factory=list)
|
|
51
|
+
waters_removed: list[dict[str, object]] = field(default_factory=list)
|
|
52
|
+
configured_ligands_kept: list[NormalizedLigand] = field(default_factory=list)
|
|
53
|
+
configured_metal_ions_kept: list[NormalizedMetalIon] = field(default_factory=list)
|
|
54
|
+
unknown_heterogens_removed: list[dict[str, object]] = field(default_factory=list)
|
|
55
|
+
unknown_heterogens_causing_failure: list[dict[str, object]] = field(default_factory=list)
|
|
56
|
+
warnings: list[str] = field(default_factory=list)
|
|
57
|
+
repair_result: StructureRepairResult | None = None
|
|
58
|
+
|
|
59
|
+
def to_report_dict(self) -> dict[str, object]:
|
|
60
|
+
normalized_summary = inspect_pdb_structure_from_structure(self.normalized_structure)
|
|
61
|
+
return {
|
|
62
|
+
"input_path": str(self.input_path),
|
|
63
|
+
"output_normalized_pdb_path": str(self.output_path) if self.output_path else None,
|
|
64
|
+
"atom_count_before": len(self.input_summary.structure.atoms),
|
|
65
|
+
"atom_count_after": len(self.normalized_structure.atoms),
|
|
66
|
+
"residue_count_before": len(self.input_summary.structure.residues),
|
|
67
|
+
"residue_count_after": len(self.normalized_structure.residues),
|
|
68
|
+
"chains": [{"chain_id": chain, "display": chain or "<blank>"} for chain in normalized_summary.chain_ids],
|
|
69
|
+
"waters_kept": self.waters_kept,
|
|
70
|
+
"waters_removed": self.waters_removed,
|
|
71
|
+
"configured_ligands_kept": [
|
|
72
|
+
{"id": ligand.ligand_id, **ligand.residue} for ligand in self.configured_ligands_kept
|
|
73
|
+
],
|
|
74
|
+
"configured_metal_ions_kept": [
|
|
75
|
+
{
|
|
76
|
+
"site_id": metal.site_id,
|
|
77
|
+
"element": metal.element,
|
|
78
|
+
"charge": metal.charge,
|
|
79
|
+
**metal.atom,
|
|
80
|
+
}
|
|
81
|
+
for metal in self.configured_metal_ions_kept
|
|
82
|
+
],
|
|
83
|
+
"unknown_heterogens_removed": self.unknown_heterogens_removed,
|
|
84
|
+
"unknown_heterogens_causing_failure": self.unknown_heterogens_causing_failure,
|
|
85
|
+
"histidines": _residue_dicts(self.input_summary.histidines),
|
|
86
|
+
"titratable_residues": _residue_dicts(self.input_summary.titratable_residues),
|
|
87
|
+
"possible_disulfides": [
|
|
88
|
+
candidate.to_dict() for candidate in self.input_summary.possible_disulfides
|
|
89
|
+
],
|
|
90
|
+
"structure_repair": (
|
|
91
|
+
self.repair_result.to_dict()
|
|
92
|
+
if self.repair_result is not None
|
|
93
|
+
else None
|
|
94
|
+
),
|
|
95
|
+
"warnings": self.warnings,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def normalize_structure_stage(
|
|
100
|
+
manifest: ManifestConfig,
|
|
101
|
+
*,
|
|
102
|
+
output_path: str | Path | None = None,
|
|
103
|
+
) -> StructureNormalizationResult:
|
|
104
|
+
input_path = Path(manifest.project.input_structure)
|
|
105
|
+
input_summary = inspect_pdb_structure(
|
|
106
|
+
input_path,
|
|
107
|
+
altloc_policy=manifest.structure.altloc_policy,
|
|
108
|
+
disulfide_cutoff_angstrom=manifest.disulfides.detection_cutoff_angstrom,
|
|
109
|
+
)
|
|
110
|
+
repair_result: StructureRepairResult | None = None
|
|
111
|
+
if manifest.structure.repair.backend != "none":
|
|
112
|
+
if output_path is None:
|
|
113
|
+
raise StructureNormalizationError(
|
|
114
|
+
"Structure repair requires an explicit normalization output path."
|
|
115
|
+
)
|
|
116
|
+
normalized_target = Path(output_path)
|
|
117
|
+
repair_path = normalized_target.with_name("00_structure_repaired.pdb")
|
|
118
|
+
try:
|
|
119
|
+
repair_result = repair_structure(manifest, output_path=repair_path)
|
|
120
|
+
except StructureRepairError as exc:
|
|
121
|
+
raise StructureNormalizationError(str(exc)) from exc
|
|
122
|
+
assert repair_result is not None
|
|
123
|
+
structure = repair_result.structure
|
|
124
|
+
else:
|
|
125
|
+
structure = read_pdb(
|
|
126
|
+
input_path,
|
|
127
|
+
altloc_policy=manifest.structure.altloc_policy,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
configured_ligands = _resolve_configured_ligands(structure, manifest)
|
|
131
|
+
configured_metals = _resolve_configured_metals(structure, manifest)
|
|
132
|
+
_validate_disulfide_selectors(structure, manifest)
|
|
133
|
+
configured_ligand_keys = {id(residue) for _, residue in configured_ligands}
|
|
134
|
+
configured_metal_residue_keys = {id(residue) for _, _, residue in configured_metals}
|
|
135
|
+
overlap = configured_ligand_keys & configured_metal_residue_keys
|
|
136
|
+
if overlap:
|
|
137
|
+
residues = [residue for residue in structure.residues if id(residue) in overlap]
|
|
138
|
+
formatted = ", ".join(_format_residue_for_error(residue) for residue in residues)
|
|
139
|
+
raise StructureNormalizationError(
|
|
140
|
+
"A residue cannot be configured as both a ligand and a metal ion: " + formatted
|
|
141
|
+
)
|
|
142
|
+
likely_heterogens = [residue for residue in structure.residues if is_likely_ligand_or_cofactor(residue)]
|
|
143
|
+
unknown_heterogens = [
|
|
144
|
+
residue
|
|
145
|
+
for residue in likely_heterogens
|
|
146
|
+
if id(residue) not in configured_ligand_keys
|
|
147
|
+
and id(residue) not in configured_metal_residue_keys
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
unknown_heterogen_dicts = _residue_dicts(unknown_heterogens)
|
|
151
|
+
if unknown_heterogens and not manifest.structure.remove_unknown_heterogens:
|
|
152
|
+
formatted = ", ".join(_format_residue_for_error(residue) for residue in unknown_heterogens)
|
|
153
|
+
raise StructureNormalizationError(
|
|
154
|
+
"Unknown heterogens are present but structure.remove_unknown_heterogens is false: "
|
|
155
|
+
f"{formatted}. Add them to ligands: for molecular heterogens or metals: for metal ions, "
|
|
156
|
+
"or set structure.remove_unknown_heterogens: true."
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
keep_residue_ids: set[int] = set()
|
|
160
|
+
waters_kept: list[ResidueRecord] = []
|
|
161
|
+
waters_removed: list[ResidueRecord] = []
|
|
162
|
+
unknown_removed: list[ResidueRecord] = []
|
|
163
|
+
|
|
164
|
+
for residue in structure.residues:
|
|
165
|
+
if is_standard_protein_residue(residue):
|
|
166
|
+
keep_residue_ids.add(id(residue))
|
|
167
|
+
elif is_water_residue(residue):
|
|
168
|
+
if manifest.structure.keep_crystal_waters:
|
|
169
|
+
keep_residue_ids.add(id(residue))
|
|
170
|
+
waters_kept.append(residue)
|
|
171
|
+
else:
|
|
172
|
+
waters_removed.append(residue)
|
|
173
|
+
elif id(residue) in configured_ligand_keys:
|
|
174
|
+
keep_residue_ids.add(id(residue))
|
|
175
|
+
elif id(residue) in configured_metal_residue_keys:
|
|
176
|
+
keep_residue_ids.add(id(residue))
|
|
177
|
+
elif residue in unknown_heterogens:
|
|
178
|
+
unknown_removed.append(residue)
|
|
179
|
+
else:
|
|
180
|
+
keep_residue_ids.add(id(residue))
|
|
181
|
+
|
|
182
|
+
filtered_atoms: list[AtomRecord] = []
|
|
183
|
+
for residue in structure.residues:
|
|
184
|
+
if id(residue) in keep_residue_ids:
|
|
185
|
+
filtered_atoms.extend(residue.atoms)
|
|
186
|
+
|
|
187
|
+
normalized_structure = PdbStructure(
|
|
188
|
+
path=Path(output_path) if output_path is not None else structure.path,
|
|
189
|
+
atoms=filtered_atoms,
|
|
190
|
+
residues=_filter_residues_by_ids(structure.residues, keep_residue_ids),
|
|
191
|
+
model_count=structure.model_count,
|
|
192
|
+
used_model=structure.used_model,
|
|
193
|
+
warnings=list(structure.warnings),
|
|
194
|
+
conect_bonds={
|
|
195
|
+
bond
|
|
196
|
+
for bond in structure.conect_bonds
|
|
197
|
+
if bond[0] in {atom.serial for atom in filtered_atoms}
|
|
198
|
+
and bond[1] in {atom.serial for atom in filtered_atoms}
|
|
199
|
+
},
|
|
200
|
+
ter_after_serials={
|
|
201
|
+
serial
|
|
202
|
+
for serial in structure.ter_after_serials
|
|
203
|
+
if serial in {atom.serial for atom in filtered_atoms}
|
|
204
|
+
},
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
warnings = list(structure.warnings)
|
|
208
|
+
if repair_result is not None:
|
|
209
|
+
warnings.extend(repair_result.warnings)
|
|
210
|
+
if manifest.structure.remove_input_hydrogens:
|
|
211
|
+
warnings.append(
|
|
212
|
+
"structure.remove_input_hydrogens is not applied during the structure-only stage."
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return StructureNormalizationResult(
|
|
216
|
+
input_path=input_path,
|
|
217
|
+
output_path=Path(output_path) if output_path is not None else None,
|
|
218
|
+
input_summary=input_summary,
|
|
219
|
+
normalized_structure=normalized_structure,
|
|
220
|
+
waters_kept=_residue_dicts(waters_kept),
|
|
221
|
+
waters_removed=_residue_dicts(waters_removed),
|
|
222
|
+
configured_ligands_kept=[
|
|
223
|
+
NormalizedLigand(ligand_id=ligand_id, residue=_residue_dict(residue))
|
|
224
|
+
for ligand_id, residue in configured_ligands
|
|
225
|
+
],
|
|
226
|
+
configured_metal_ions_kept=[
|
|
227
|
+
NormalizedMetalIon(
|
|
228
|
+
site_id=site_id,
|
|
229
|
+
element=element,
|
|
230
|
+
charge=charge,
|
|
231
|
+
atom={
|
|
232
|
+
**residue.id.to_dict(),
|
|
233
|
+
"atom_name": atom.name,
|
|
234
|
+
"atom_serial": atom.serial,
|
|
235
|
+
},
|
|
236
|
+
)
|
|
237
|
+
for site_id, (element, charge, atom), residue in configured_metals
|
|
238
|
+
],
|
|
239
|
+
unknown_heterogens_removed=_residue_dicts(unknown_removed),
|
|
240
|
+
unknown_heterogens_causing_failure=unknown_heterogen_dicts
|
|
241
|
+
if unknown_heterogens and not manifest.structure.remove_unknown_heterogens
|
|
242
|
+
else [],
|
|
243
|
+
warnings=warnings,
|
|
244
|
+
repair_result=repair_result,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def inspect_pdb_structure_from_structure(structure: PdbStructure) -> InspectionSummary:
|
|
249
|
+
from mdprep.structure.classify import is_histidine, is_titratable_residue
|
|
250
|
+
from mdprep.structure.disulfides import detect_possible_disulfides
|
|
251
|
+
|
|
252
|
+
residues = structure.residues
|
|
253
|
+
protein_residues = [residue for residue in residues if is_standard_protein_residue(residue)]
|
|
254
|
+
water_residues = [residue for residue in residues if is_water_residue(residue)]
|
|
255
|
+
heterogen_residues = [
|
|
256
|
+
residue for residue in residues if is_nonstandard_nonwater_residue(residue)
|
|
257
|
+
]
|
|
258
|
+
likely_ligands = [residue for residue in residues if is_likely_ligand_or_cofactor(residue)]
|
|
259
|
+
histidines = [residue for residue in residues if is_histidine(residue)]
|
|
260
|
+
titratable = [residue for residue in residues if is_titratable_residue(residue)]
|
|
261
|
+
disulfides = detect_possible_disulfides(residues)
|
|
262
|
+
return InspectionSummary(
|
|
263
|
+
structure=structure,
|
|
264
|
+
protein_residues=protein_residues,
|
|
265
|
+
water_residues=water_residues,
|
|
266
|
+
heterogen_residues=heterogen_residues,
|
|
267
|
+
likely_ligands=likely_ligands,
|
|
268
|
+
histidines=histidines,
|
|
269
|
+
titratable_residues=titratable,
|
|
270
|
+
possible_disulfides=disulfides,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _resolve_configured_ligands(
|
|
275
|
+
structure: PdbStructure,
|
|
276
|
+
manifest: ManifestConfig,
|
|
277
|
+
) -> list[tuple[str, ResidueRecord]]:
|
|
278
|
+
resolved: list[tuple[str, ResidueRecord]] = []
|
|
279
|
+
for ligand in manifest.ligands:
|
|
280
|
+
try:
|
|
281
|
+
residue = resolve_residue_selector(structure, ligand.selector.model_dump())
|
|
282
|
+
except SelectorError as exc:
|
|
283
|
+
raise StructureNormalizationError(
|
|
284
|
+
f"Ligand {ligand.id!r} selector did not resolve exactly one residue: {exc}"
|
|
285
|
+
) from exc
|
|
286
|
+
resolved.append((ligand.id, residue))
|
|
287
|
+
return resolved
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _resolve_configured_metals(
|
|
291
|
+
structure: PdbStructure,
|
|
292
|
+
manifest: ManifestConfig,
|
|
293
|
+
) -> list[tuple[str, tuple[str, int, AtomRecord], ResidueRecord]]:
|
|
294
|
+
resolved: list[tuple[str, tuple[str, int, AtomRecord], ResidueRecord]] = []
|
|
295
|
+
seen_atoms: set[int] = set()
|
|
296
|
+
for site in manifest.metals:
|
|
297
|
+
for ion in site.ions:
|
|
298
|
+
try:
|
|
299
|
+
atom = resolve_atom_selector(structure, ion.selector.model_dump())
|
|
300
|
+
residue = resolve_residue_selector(structure, ion.selector.model_dump())
|
|
301
|
+
except SelectorError as exc:
|
|
302
|
+
raise StructureNormalizationError(
|
|
303
|
+
f"Metal site {site.id!r} ion selector did not resolve exactly one atom: {exc}"
|
|
304
|
+
) from exc
|
|
305
|
+
if id(atom) in seen_atoms:
|
|
306
|
+
raise StructureNormalizationError(
|
|
307
|
+
f"Metal atom {atom.atom_identity} is configured more than once."
|
|
308
|
+
)
|
|
309
|
+
seen_atoms.add(id(atom))
|
|
310
|
+
observed_element = (atom.element or "").capitalize()
|
|
311
|
+
if observed_element and observed_element != ion.element:
|
|
312
|
+
raise StructureNormalizationError(
|
|
313
|
+
f"Metal site {site.id!r} declares element {ion.element} for "
|
|
314
|
+
f"{atom.atom_identity}, but the PDB element is {observed_element}."
|
|
315
|
+
)
|
|
316
|
+
if len(residue.atoms) != 1:
|
|
317
|
+
raise StructureNormalizationError(
|
|
318
|
+
f"Metal ion residue {residue.id.display()} contains {len(residue.atoms)} atoms; "
|
|
319
|
+
"each configured metal ion must occupy a one-atom residue."
|
|
320
|
+
)
|
|
321
|
+
resolved.append((site.id, (ion.element, ion.charge, atom), residue))
|
|
322
|
+
return resolved
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _validate_disulfide_selectors(structure: PdbStructure, manifest: ManifestConfig) -> None:
|
|
326
|
+
for group_name, pairs in (
|
|
327
|
+
("force", manifest.disulfides.force),
|
|
328
|
+
("forbid", manifest.disulfides.forbid),
|
|
329
|
+
):
|
|
330
|
+
for index, pair in enumerate(pairs, start=1):
|
|
331
|
+
for side_name, selector in (("a", pair.a), ("b", pair.b)):
|
|
332
|
+
try:
|
|
333
|
+
resolve_residue_selector(structure, selector.model_dump())
|
|
334
|
+
except SelectorError as exc:
|
|
335
|
+
raise StructureNormalizationError(
|
|
336
|
+
f"Disulfide {group_name}[{index}].{side_name} selector did not resolve exactly one residue: {exc}"
|
|
337
|
+
) from exc
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _filter_residues_by_ids(
|
|
341
|
+
residues: list[ResidueRecord],
|
|
342
|
+
keep_residue_ids: set[int],
|
|
343
|
+
) -> list[ResidueRecord]:
|
|
344
|
+
return [residue for residue in residues if id(residue) in keep_residue_ids]
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _residue_dicts(residues: list[ResidueRecord]) -> list[dict[str, object]]:
|
|
348
|
+
return [_residue_dict(residue) for residue in residues]
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _residue_dict(residue: ResidueRecord) -> dict[str, object]:
|
|
352
|
+
return {
|
|
353
|
+
**residue.id.to_dict(),
|
|
354
|
+
"atom_count": len(residue.atoms),
|
|
355
|
+
"record_names": sorted(residue.record_names),
|
|
356
|
+
"original_index": residue.original_index,
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _format_residue_for_error(residue: ResidueRecord) -> str:
|
|
361
|
+
chain = residue.id.chain_id if residue.id.chain_id else "<blank>"
|
|
362
|
+
icode = residue.id.icode or ""
|
|
363
|
+
return f"{chain}:{residue.id.resname}{residue.id.resid}{icode}"
|