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,92 @@
|
|
|
1
|
+
"""Structure-stage report writers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from mdprep.structure.normalize import StructureNormalizationResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def write_structure_reports(
|
|
13
|
+
result: StructureNormalizationResult,
|
|
14
|
+
*,
|
|
15
|
+
json_path: str | Path,
|
|
16
|
+
markdown_path: str | Path,
|
|
17
|
+
) -> dict[str, Any]:
|
|
18
|
+
report = result.to_report_dict()
|
|
19
|
+
json_output = Path(json_path)
|
|
20
|
+
markdown_output = Path(markdown_path)
|
|
21
|
+
json_output.parent.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
markdown_output.parent.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
json_output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
24
|
+
markdown_output.write_text(_render_markdown(report), encoding="utf-8")
|
|
25
|
+
return report
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _render_markdown(report: dict[str, Any]) -> str:
|
|
29
|
+
lines = [
|
|
30
|
+
"# Structure Normalization Report",
|
|
31
|
+
"",
|
|
32
|
+
f"- Input: `{report['input_path']}`",
|
|
33
|
+
f"- Normalized PDB: `{report['output_normalized_pdb_path']}`",
|
|
34
|
+
f"- Atoms: {report['atom_count_before']} -> {report['atom_count_after']}",
|
|
35
|
+
f"- Residues: {report['residue_count_before']} -> {report['residue_count_after']}",
|
|
36
|
+
"",
|
|
37
|
+
"## Waters",
|
|
38
|
+
"",
|
|
39
|
+
f"- Kept: {len(report['waters_kept'])}",
|
|
40
|
+
f"- Removed: {len(report['waters_removed'])}",
|
|
41
|
+
"",
|
|
42
|
+
"## Configured Ligands",
|
|
43
|
+
"",
|
|
44
|
+
]
|
|
45
|
+
lines.extend(_residue_lines(report["configured_ligands_kept"]))
|
|
46
|
+
lines.extend(["", "## Configured Metal Ions", ""])
|
|
47
|
+
if report["configured_metal_ions_kept"]:
|
|
48
|
+
for item in report["configured_metal_ions_kept"]:
|
|
49
|
+
lines.append(
|
|
50
|
+
f"- {item['site_id']}: {_format_residue(item)}@{item['atom_name']} "
|
|
51
|
+
f"({item['element']}{int(item['charge']):+d})"
|
|
52
|
+
)
|
|
53
|
+
else:
|
|
54
|
+
lines.append("- None")
|
|
55
|
+
lines.extend(["", "## Unknown Heterogens Removed", ""])
|
|
56
|
+
lines.extend(_residue_lines(report["unknown_heterogens_removed"]))
|
|
57
|
+
lines.extend(["", "## Histidines", ""])
|
|
58
|
+
lines.extend(_residue_lines(report["histidines"]))
|
|
59
|
+
lines.extend(["", "## Titratable Residues", ""])
|
|
60
|
+
lines.extend(_residue_lines(report["titratable_residues"]))
|
|
61
|
+
lines.extend(["", "## Possible Disulfides", ""])
|
|
62
|
+
if report["possible_disulfides"]:
|
|
63
|
+
for item in report["possible_disulfides"]:
|
|
64
|
+
a = item["a"]
|
|
65
|
+
b = item["b"]
|
|
66
|
+
lines.append(
|
|
67
|
+
f"- {_format_residue(a)} -- {_format_residue(b)}: {item['distance_angstrom']:.3f} A"
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
lines.append("- None")
|
|
71
|
+
lines.extend(["", "## Warnings", ""])
|
|
72
|
+
if report["warnings"]:
|
|
73
|
+
lines.extend(f"- {warning}" for warning in report["warnings"])
|
|
74
|
+
else:
|
|
75
|
+
lines.append("- None")
|
|
76
|
+
lines.append("")
|
|
77
|
+
return "\n".join(lines)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _residue_lines(items: list[dict[str, Any]]) -> list[str]:
|
|
81
|
+
if not items:
|
|
82
|
+
return ["- None"]
|
|
83
|
+
lines = []
|
|
84
|
+
for item in items:
|
|
85
|
+
prefix = f"{item['id']}: " if "id" in item else ""
|
|
86
|
+
lines.append(f"- {prefix}{_format_residue(item)} ({item['atom_count']} atoms)")
|
|
87
|
+
return lines
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _format_residue(item: dict[str, Any]) -> str:
|
|
91
|
+
chain = item["chain_id"] or "<blank>"
|
|
92
|
+
return f"{chain}:{item['resname']}{item['resid']}{item.get('icode') or ''}"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""PDB structure parsing, classification, inspection, and selectors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from mdprep.structure.inspect import InspectionSummary, inspect_pdb_structure
|
|
6
|
+
from mdprep.structure.pdb import PdbParseError, read_pdb
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"InspectionSummary",
|
|
10
|
+
"PdbParseError",
|
|
11
|
+
"inspect_pdb_structure",
|
|
12
|
+
"read_pdb",
|
|
13
|
+
]
|
|
14
|
+
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Residue classification helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from mdprep.structure.models import ResidueRecord
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
STANDARD_PROTEIN_RESIDUES = {
|
|
9
|
+
"ALA",
|
|
10
|
+
"ARG",
|
|
11
|
+
"ASN",
|
|
12
|
+
"ASP",
|
|
13
|
+
"ASH",
|
|
14
|
+
"CYS",
|
|
15
|
+
"CYM",
|
|
16
|
+
"CYX",
|
|
17
|
+
"GLN",
|
|
18
|
+
"GLU",
|
|
19
|
+
"GLH",
|
|
20
|
+
"GLY",
|
|
21
|
+
"HIS",
|
|
22
|
+
"HID",
|
|
23
|
+
"HIE",
|
|
24
|
+
"HIP",
|
|
25
|
+
"ILE",
|
|
26
|
+
"LEU",
|
|
27
|
+
"LYS",
|
|
28
|
+
"LYN",
|
|
29
|
+
"MET",
|
|
30
|
+
"PHE",
|
|
31
|
+
"PRO",
|
|
32
|
+
"SER",
|
|
33
|
+
"THR",
|
|
34
|
+
"TRP",
|
|
35
|
+
"TYR",
|
|
36
|
+
"VAL",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
WATER_RESIDUES = {"HOH", "WAT", "H2O", "TIP3", "OPC"}
|
|
40
|
+
|
|
41
|
+
TITRATABLE_RESIDUES = {
|
|
42
|
+
"ASP",
|
|
43
|
+
"ASH",
|
|
44
|
+
"GLU",
|
|
45
|
+
"GLH",
|
|
46
|
+
"HIS",
|
|
47
|
+
"HID",
|
|
48
|
+
"HIE",
|
|
49
|
+
"HIP",
|
|
50
|
+
"LYS",
|
|
51
|
+
"LYN",
|
|
52
|
+
"ARG",
|
|
53
|
+
"CYS",
|
|
54
|
+
"CYM",
|
|
55
|
+
"CYX",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
HISTIDINE_RESIDUES = {"HIS", "HID", "HIE", "HIP"}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def is_standard_protein_residue(residue: ResidueRecord) -> bool:
|
|
62
|
+
# Residue names alone are insufficient: free proline substrates and other
|
|
63
|
+
# amino-acid-like ligands are valid HETATM residues. Polymer identity is
|
|
64
|
+
# carried by ATOM records (and preserved TER boundaries).
|
|
65
|
+
return (
|
|
66
|
+
residue.id.resname in STANDARD_PROTEIN_RESIDUES
|
|
67
|
+
and "ATOM" in residue.record_names
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def is_water_residue(residue: ResidueRecord) -> bool:
|
|
72
|
+
return residue.id.resname in WATER_RESIDUES
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def is_histidine(residue: ResidueRecord) -> bool:
|
|
76
|
+
return is_standard_protein_residue(residue) and residue.id.resname in HISTIDINE_RESIDUES
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def is_titratable_residue(residue: ResidueRecord) -> bool:
|
|
80
|
+
return is_standard_protein_residue(residue) and residue.id.resname in TITRATABLE_RESIDUES
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def is_nonstandard_nonwater_residue(residue: ResidueRecord) -> bool:
|
|
84
|
+
return not is_water_residue(residue) and not is_standard_protein_residue(residue)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def is_likely_ligand_or_cofactor(residue: ResidueRecord) -> bool:
|
|
88
|
+
return is_nonstandard_nonwater_residue(residue)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def likely_ligands_or_cofactors(residues: list[ResidueRecord]) -> list[ResidueRecord]:
|
|
92
|
+
return [residue for residue in residues if is_likely_ligand_or_cofactor(residue)]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Disulfide candidate detection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from itertools import combinations
|
|
7
|
+
from math import dist
|
|
8
|
+
|
|
9
|
+
from mdprep.structure.models import AtomRecord, ResidueId, ResidueRecord
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class DisulfideCandidate:
|
|
14
|
+
a: ResidueId
|
|
15
|
+
b: ResidueId
|
|
16
|
+
distance_angstrom: float
|
|
17
|
+
|
|
18
|
+
def to_dict(self) -> dict[str, object]:
|
|
19
|
+
return {
|
|
20
|
+
"a": self.a.to_dict(),
|
|
21
|
+
"b": self.b.to_dict(),
|
|
22
|
+
"distance_angstrom": self.distance_angstrom,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def detect_possible_disulfides(
|
|
27
|
+
residues: list[ResidueRecord],
|
|
28
|
+
*,
|
|
29
|
+
cutoff_angstrom: float = 2.2,
|
|
30
|
+
) -> list[DisulfideCandidate]:
|
|
31
|
+
cys_sg_atoms: list[tuple[ResidueRecord, AtomRecord]] = []
|
|
32
|
+
for residue in residues:
|
|
33
|
+
if residue.id.resname not in {"CYS", "CYX"}:
|
|
34
|
+
continue
|
|
35
|
+
sg = next((atom for atom in residue.atoms if atom.name == "SG"), None)
|
|
36
|
+
if sg is not None:
|
|
37
|
+
cys_sg_atoms.append((residue, sg))
|
|
38
|
+
|
|
39
|
+
candidates: list[DisulfideCandidate] = []
|
|
40
|
+
for (residue_a, atom_a), (residue_b, atom_b) in combinations(cys_sg_atoms, 2):
|
|
41
|
+
distance_angstrom = dist((atom_a.x, atom_a.y, atom_a.z), (atom_b.x, atom_b.y, atom_b.z))
|
|
42
|
+
if distance_angstrom <= cutoff_angstrom:
|
|
43
|
+
candidates.append(
|
|
44
|
+
DisulfideCandidate(
|
|
45
|
+
a=residue_a.id,
|
|
46
|
+
b=residue_b.id,
|
|
47
|
+
distance_angstrom=distance_angstrom,
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
return candidates
|
|
51
|
+
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Structure inspection summaries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from mdprep.structure.classify import (
|
|
9
|
+
is_histidine,
|
|
10
|
+
is_likely_ligand_or_cofactor,
|
|
11
|
+
is_nonstandard_nonwater_residue,
|
|
12
|
+
is_standard_protein_residue,
|
|
13
|
+
is_titratable_residue,
|
|
14
|
+
is_water_residue,
|
|
15
|
+
)
|
|
16
|
+
from mdprep.structure.disulfides import DisulfideCandidate, detect_possible_disulfides
|
|
17
|
+
from mdprep.structure.models import PdbStructure, ResidueRecord
|
|
18
|
+
from mdprep.structure.pdb import AltlocPolicy, read_pdb
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class InspectionSummary:
|
|
23
|
+
structure: PdbStructure
|
|
24
|
+
protein_residues: list[ResidueRecord]
|
|
25
|
+
water_residues: list[ResidueRecord]
|
|
26
|
+
heterogen_residues: list[ResidueRecord]
|
|
27
|
+
likely_ligands: list[ResidueRecord]
|
|
28
|
+
histidines: list[ResidueRecord]
|
|
29
|
+
titratable_residues: list[ResidueRecord]
|
|
30
|
+
possible_disulfides: list[DisulfideCandidate]
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def chain_ids(self) -> list[str]:
|
|
34
|
+
seen: list[str] = []
|
|
35
|
+
for residue in self.structure.residues:
|
|
36
|
+
if residue.id.chain_id not in seen:
|
|
37
|
+
seen.append(residue.id.chain_id)
|
|
38
|
+
return seen
|
|
39
|
+
|
|
40
|
+
def to_dict(self) -> dict[str, object]:
|
|
41
|
+
return {
|
|
42
|
+
"path": str(self.structure.path),
|
|
43
|
+
"total_atoms": len(self.structure.atoms),
|
|
44
|
+
"total_residues": len(self.structure.residues),
|
|
45
|
+
"model_count": self.structure.model_count,
|
|
46
|
+
"used_model": self.structure.used_model,
|
|
47
|
+
"warnings": list(self.structure.warnings),
|
|
48
|
+
"chains": [{"chain_id": chain_id, "display": chain_id or "<blank>"} for chain_id in self.chain_ids],
|
|
49
|
+
"counts": {
|
|
50
|
+
"protein_residues": len(self.protein_residues),
|
|
51
|
+
"water_residues": len(self.water_residues),
|
|
52
|
+
"heterogen_residues": len(self.heterogen_residues),
|
|
53
|
+
"likely_ligands": len(self.likely_ligands),
|
|
54
|
+
"histidines": len(self.histidines),
|
|
55
|
+
"titratable_residues": len(self.titratable_residues),
|
|
56
|
+
"possible_disulfides": len(self.possible_disulfides),
|
|
57
|
+
},
|
|
58
|
+
"likely_ligands": [_residue_to_dict(residue) for residue in self.likely_ligands],
|
|
59
|
+
"histidines": [_residue_to_dict(residue) for residue in self.histidines],
|
|
60
|
+
"titratable_residues": [_residue_to_dict(residue) for residue in self.titratable_residues],
|
|
61
|
+
"possible_disulfides": [candidate.to_dict() for candidate in self.possible_disulfides],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def inspect_pdb_structure(
|
|
66
|
+
path: str | Path,
|
|
67
|
+
*,
|
|
68
|
+
altloc_policy: AltlocPolicy = "highest_occupancy",
|
|
69
|
+
disulfide_cutoff_angstrom: float = 2.2,
|
|
70
|
+
) -> InspectionSummary:
|
|
71
|
+
structure = read_pdb(path, altloc_policy=altloc_policy)
|
|
72
|
+
residues = structure.residues
|
|
73
|
+
protein_residues = [residue for residue in residues if is_standard_protein_residue(residue)]
|
|
74
|
+
water_residues = [residue for residue in residues if is_water_residue(residue)]
|
|
75
|
+
heterogen_residues = [
|
|
76
|
+
residue
|
|
77
|
+
for residue in residues
|
|
78
|
+
if is_nonstandard_nonwater_residue(residue)
|
|
79
|
+
]
|
|
80
|
+
likely_ligands = [residue for residue in residues if is_likely_ligand_or_cofactor(residue)]
|
|
81
|
+
histidines = [residue for residue in residues if is_histidine(residue)]
|
|
82
|
+
titratable_residues = [residue for residue in residues if is_titratable_residue(residue)]
|
|
83
|
+
possible_disulfides = detect_possible_disulfides(
|
|
84
|
+
residues,
|
|
85
|
+
cutoff_angstrom=disulfide_cutoff_angstrom,
|
|
86
|
+
)
|
|
87
|
+
return InspectionSummary(
|
|
88
|
+
structure=structure,
|
|
89
|
+
protein_residues=protein_residues,
|
|
90
|
+
water_residues=water_residues,
|
|
91
|
+
heterogen_residues=heterogen_residues,
|
|
92
|
+
likely_ligands=likely_ligands,
|
|
93
|
+
histidines=histidines,
|
|
94
|
+
titratable_residues=titratable_residues,
|
|
95
|
+
possible_disulfides=possible_disulfides,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _residue_to_dict(residue: ResidueRecord) -> dict[str, object]:
|
|
100
|
+
return {
|
|
101
|
+
**residue.id.to_dict(),
|
|
102
|
+
"atom_count": len(residue.atoms),
|
|
103
|
+
"record_names": sorted(residue.record_names),
|
|
104
|
+
"original_index": residue.original_index,
|
|
105
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Internal structure data models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
RecordName = Literal["ATOM", "HETATM"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class AtomRecord:
|
|
15
|
+
serial: int | None
|
|
16
|
+
name: str
|
|
17
|
+
altloc: str | None
|
|
18
|
+
resname: str
|
|
19
|
+
chain_id: str
|
|
20
|
+
resid: int
|
|
21
|
+
icode: str | None
|
|
22
|
+
x: float
|
|
23
|
+
y: float
|
|
24
|
+
z: float
|
|
25
|
+
occupancy: float | None
|
|
26
|
+
bfactor: float | None
|
|
27
|
+
element: str | None
|
|
28
|
+
record_name: RecordName
|
|
29
|
+
original_line: str
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def residue_key(self) -> tuple[str, str, int, str | None]:
|
|
33
|
+
return (self.chain_id, self.resname, self.resid, self.icode)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def atom_identity(self) -> tuple[str, str, int, str | None, str]:
|
|
37
|
+
return (self.chain_id, self.resname, self.resid, self.icode, self.name)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class ResidueId:
|
|
42
|
+
chain_id: str
|
|
43
|
+
resname: str
|
|
44
|
+
resid: int
|
|
45
|
+
icode: str | None = None
|
|
46
|
+
|
|
47
|
+
def to_dict(self) -> dict[str, str | int | None]:
|
|
48
|
+
return {
|
|
49
|
+
"chain_id": self.chain_id,
|
|
50
|
+
"resname": self.resname,
|
|
51
|
+
"resid": self.resid,
|
|
52
|
+
"icode": self.icode,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
def display(self) -> str:
|
|
56
|
+
chain = self.chain_id if self.chain_id else "<blank>"
|
|
57
|
+
icode = self.icode or ""
|
|
58
|
+
return f"{chain}:{self.resname}{self.resid}{icode}"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class ResidueRecord:
|
|
63
|
+
id: ResidueId
|
|
64
|
+
atoms: list[AtomRecord]
|
|
65
|
+
record_names: set[str]
|
|
66
|
+
original_index: int
|
|
67
|
+
|
|
68
|
+
def atom_names(self) -> list[str]:
|
|
69
|
+
return [atom.name for atom in self.atoms]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class PdbStructure:
|
|
74
|
+
path: Path
|
|
75
|
+
atoms: list[AtomRecord]
|
|
76
|
+
residues: list[ResidueRecord]
|
|
77
|
+
model_count: int
|
|
78
|
+
used_model: int = 1
|
|
79
|
+
warnings: list[str] = field(default_factory=list)
|
|
80
|
+
# Connectivity is stored by PDB atom serial so it survives residue
|
|
81
|
+
# renaming and filtering. Each pair is canonicalized as (min, max).
|
|
82
|
+
conect_bonds: set[tuple[int, int]] = field(default_factory=set)
|
|
83
|
+
# TER is a polymer/chain boundary, not decoration. Store the serial of
|
|
84
|
+
# the last atom before each explicit input TER so writers cannot
|
|
85
|
+
# accidentally reconnect an unresolved chain break or a heterogen.
|
|
86
|
+
ter_after_serials: set[int] = field(default_factory=set)
|