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.
Files changed (100) hide show
  1. mdprep/__init__.py +7 -0
  2. mdprep/_selftest_data/basic_protein.yaml +33 -0
  3. mdprep/_selftest_data/ligand_sub.mol2 +11 -0
  4. mdprep/ambertools/__init__.py +2 -0
  5. mdprep/ambertools/antechamber.py +110 -0
  6. mdprep/ambertools/commands.py +33 -0
  7. mdprep/ambertools/mol2.py +415 -0
  8. mdprep/ambertools/parmchk2.py +89 -0
  9. mdprep/ambertools/resp.py +570 -0
  10. mdprep/charges/__init__.py +1 -0
  11. mdprep/charges/esp_grid.py +190 -0
  12. mdprep/cli.py +451 -0
  13. mdprep/config/__init__.py +9 -0
  14. mdprep/config/loader.py +146 -0
  15. mdprep/config/models.py +1100 -0
  16. mdprep/external/__init__.py +14 -0
  17. mdprep/external/discovery.py +14 -0
  18. mdprep/external/runner.py +55 -0
  19. mdprep/leap/__init__.py +2 -0
  20. mdprep/leap/builder.py +476 -0
  21. mdprep/leap/forcefields.py +93 -0
  22. mdprep/leap/ions.py +112 -0
  23. mdprep/leap/log_parser.py +170 -0
  24. mdprep/leap/report.py +99 -0
  25. mdprep/leap/residues.py +653 -0
  26. mdprep/leap/runner.py +65 -0
  27. mdprep/ligands/__init__.py +2 -0
  28. mdprep/ligands/extract.py +355 -0
  29. mdprep/ligands/identity.py +39 -0
  30. mdprep/ligands/pyscf_charges.py +242 -0
  31. mdprep/ligands/report.py +146 -0
  32. mdprep/ligands/workflow.py +838 -0
  33. mdprep/md/__init__.py +5 -0
  34. mdprep/md/roe_brooks.py +650 -0
  35. mdprep/metals/__init__.py +2 -0
  36. mdprep/metals/c4.py +157 -0
  37. mdprep/metals/coordination.py +313 -0
  38. mdprep/metals/gxtb_hessian.py +467 -0
  39. mdprep/metals/hydrogenate.py +344 -0
  40. mdprep/metals/mace_polar1_hessian.py +357 -0
  41. mdprep/metals/mace_polar1_worker.py +295 -0
  42. mdprep/metals/mcpb.py +949 -0
  43. mdprep/metals/mcpb_compat.py +125 -0
  44. mdprep/metals/nonbonded.py +294 -0
  45. mdprep/metals/parameter_comparison.py +563 -0
  46. mdprep/metals/pyscf_mcpb.py +1388 -0
  47. mdprep/metals/report.py +119 -0
  48. mdprep/metals/workflow.py +320 -0
  49. mdprep/protonation/__init__.py +15 -0
  50. mdprep/protonation/apply.py +968 -0
  51. mdprep/protonation/disulfide_states.py +141 -0
  52. mdprep/protonation/histidine_geometry.py +950 -0
  53. mdprep/protonation/histidine_xtb.py +390 -0
  54. mdprep/protonation/overrides.py +64 -0
  55. mdprep/protonation/pka_rules.py +165 -0
  56. mdprep/protonation/propka.py +174 -0
  57. mdprep/protonation/propka_parser.py +140 -0
  58. mdprep/protonation/report.py +187 -0
  59. mdprep/protonation/states.py +52 -0
  60. mdprep/protonation/temporary_hydrogenation.py +238 -0
  61. mdprep/protonation/xtb_parser.py +79 -0
  62. mdprep/protonation/xtb_runner.py +108 -0
  63. mdprep/qm/__init__.py +2 -0
  64. mdprep/qm/point_charges.py +233 -0
  65. mdprep/qm/pyscf_esp.py +70 -0
  66. mdprep/qm/pyscf_runner.py +570 -0
  67. mdprep/refinement/__init__.py +5 -0
  68. mdprep/refinement/ash.py +541 -0
  69. mdprep/refinement/coordinates.py +210 -0
  70. mdprep/refinement/mapping.py +163 -0
  71. mdprep/refinement/report.py +66 -0
  72. mdprep/refinement/selection.py +436 -0
  73. mdprep/refinement/workflow.py +188 -0
  74. mdprep/reports/__init__.py +8 -0
  75. mdprep/reports/structure_report.py +92 -0
  76. mdprep/structure/__init__.py +14 -0
  77. mdprep/structure/classify.py +92 -0
  78. mdprep/structure/disulfides.py +51 -0
  79. mdprep/structure/inspect.py +105 -0
  80. mdprep/structure/models.py +86 -0
  81. mdprep/structure/normalize.py +363 -0
  82. mdprep/structure/pdb.py +511 -0
  83. mdprep/structure/repair.py +566 -0
  84. mdprep/structure/selectors.py +204 -0
  85. mdprep/structure/writer.py +84 -0
  86. mdprep/validation/__init__.py +2 -0
  87. mdprep/validation/openmm_check.py +86 -0
  88. mdprep/validation/parmed_check.py +43 -0
  89. mdprep/validation/topology.py +239 -0
  90. mdprep/workflows/__init__.py +6 -0
  91. mdprep/workflows/init.py +242 -0
  92. mdprep/workflows/inspect.py +21 -0
  93. mdprep/workflows/prepare.py +763 -0
  94. mdprep/workflows/selftest.py +220 -0
  95. mdprep/workflows/validate.py +30 -0
  96. mdprep-0.2.0.dist-info/METADATA +264 -0
  97. mdprep-0.2.0.dist-info/RECORD +100 -0
  98. mdprep-0.2.0.dist-info/WHEEL +4 -0
  99. mdprep-0.2.0.dist-info/entry_points.txt +2 -0
  100. 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)