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,295 @@
1
+ """Isolated MACE-POLAR-1 inference worker invoked through the external runner.
2
+
3
+ This module deliberately has no imports from mdprep. It is executed by the
4
+ Python interpreter configured for MACE-POLAR-1, which may be a dedicated
5
+ environment separate from mdprep's own Python environment.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import hashlib
12
+ import importlib.metadata
13
+ import json
14
+ from pathlib import Path
15
+ import re
16
+ import sys
17
+ from time import perf_counter
18
+ from typing import Any
19
+
20
+
21
+ EV_PER_HARTREE = 27.211386245988
22
+ ANGSTROM_PER_BOHR = 0.529177210903
23
+ MINIMUM_MACE_VERSION = (0, 3, 16)
24
+
25
+
26
+ def _version(distribution: str) -> str | None:
27
+ try:
28
+ return importlib.metadata.version(distribution)
29
+ except importlib.metadata.PackageNotFoundError:
30
+ return None
31
+
32
+
33
+ def _versions() -> dict[str, str | None]:
34
+ return {
35
+ "python": sys.version.splitlines()[0],
36
+ "mace_torch": _version("mace-torch"),
37
+ "torch": _version("torch"),
38
+ "ase": _version("ase"),
39
+ "graph_longrange": _version("graph-longrange"),
40
+ }
41
+
42
+
43
+ def _require_supported_mace_version() -> str:
44
+ version = _version("mace-torch")
45
+ if version is None:
46
+ raise RuntimeError("mace-torch distribution metadata is unavailable")
47
+ match = re.match(r"^(\d+)\.(\d+)\.(\d+)", version)
48
+ parsed = tuple(int(value) for value in match.groups()) if match else None
49
+ if parsed is None or parsed < MINIMUM_MACE_VERSION:
50
+ raise RuntimeError(
51
+ "MACE-POLAR-1 Hessians require mace-torch 0.3.16 or newer; "
52
+ f"found {version}"
53
+ )
54
+ return version
55
+
56
+
57
+ def _sha256(path: Path) -> str:
58
+ digest = hashlib.sha256()
59
+ with path.open("rb") as handle:
60
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
61
+ digest.update(chunk)
62
+ return digest.hexdigest()
63
+
64
+
65
+ def _cartesian_matrix(raw: object, atom_count: int) -> Any:
66
+ import numpy as np
67
+
68
+ values = np.asarray(raw, dtype=float)
69
+ dimension = atom_count * 3
70
+ if values.shape == (dimension, dimension):
71
+ matrix = values
72
+ elif values.shape == (dimension, atom_count, 3):
73
+ matrix = values.reshape((dimension, dimension))
74
+ elif values.shape == (atom_count, 3, atom_count, 3):
75
+ matrix = values.reshape((dimension, dimension))
76
+ elif values.shape == (atom_count, atom_count, 3, 3):
77
+ matrix = values.transpose(0, 2, 1, 3).reshape((dimension, dimension))
78
+ else:
79
+ raise ValueError(
80
+ f"MACE Hessian shape {values.shape} is incompatible with "
81
+ f"{atom_count} atoms"
82
+ )
83
+ if not np.all(np.isfinite(matrix)):
84
+ raise ValueError("MACE-POLAR-1 Hessian contains non-finite values")
85
+ return matrix
86
+
87
+
88
+ def _deterministic_direction(dimension: int) -> Any:
89
+ import numpy as np
90
+
91
+ indices = np.arange(1, dimension + 1, dtype=float)
92
+ direction = np.sin(indices) + 0.5 * np.cos(indices * 0.37)
93
+ direction /= np.linalg.norm(direction)
94
+ return direction
95
+
96
+
97
+ def _finite_difference_check(
98
+ atoms: Any,
99
+ hessian_ev_per_angstrom2: object,
100
+ *,
101
+ step_angstrom: float,
102
+ ) -> dict[str, float]:
103
+ import numpy as np
104
+
105
+ atom_count = len(atoms)
106
+ direction = _deterministic_direction(atom_count * 3)
107
+ original = np.asarray(atoms.get_positions(), dtype=float)
108
+ displaced = direction.reshape((atom_count, 3)) * step_angstrom
109
+ try:
110
+ atoms.set_positions(original + displaced)
111
+ force_plus = np.asarray(atoms.get_forces(), dtype=float).reshape(-1)
112
+ atoms.set_positions(original - displaced)
113
+ force_minus = np.asarray(atoms.get_forces(), dtype=float).reshape(-1)
114
+ finally:
115
+ atoms.set_positions(original)
116
+ finite_difference = -(force_plus - force_minus) / (2.0 * step_angstrom)
117
+ analytic = np.asarray(hessian_ev_per_angstrom2, dtype=float) @ direction
118
+ difference = analytic - finite_difference
119
+ denominator = max(
120
+ float(np.linalg.norm(analytic)),
121
+ float(np.linalg.norm(finite_difference)),
122
+ 1.0e-12,
123
+ )
124
+ return {
125
+ "step_angstrom": step_angstrom,
126
+ "analytic_hessian_vector_norm_ev_per_angstrom2": float(
127
+ np.linalg.norm(analytic)
128
+ ),
129
+ "finite_difference_hessian_vector_norm_ev_per_angstrom2": float(
130
+ np.linalg.norm(finite_difference)
131
+ ),
132
+ "difference_norm_ev_per_angstrom2": float(np.linalg.norm(difference)),
133
+ "relative_error": float(np.linalg.norm(difference) / denominator),
134
+ }
135
+
136
+
137
+ def _run(args: argparse.Namespace) -> None:
138
+ import numpy as np
139
+ from ase.io import read
140
+ from mace.calculators import mace_polar
141
+ from mace.calculators.foundations_models import download_mace_polar_checkpoint
142
+
143
+ _require_supported_mace_version()
144
+
145
+ input_path = Path(args.xyz).resolve()
146
+ output_hessian = Path(args.output_hessian).resolve()
147
+ output_report = Path(args.output_report).resolve()
148
+ checkpoint = Path(download_mace_polar_checkpoint(args.model)).resolve()
149
+ model_sha256 = _sha256(checkpoint)
150
+ if args.expected_model_sha256 is not None and (
151
+ model_sha256.lower() != args.expected_model_sha256.lower()
152
+ ):
153
+ raise ValueError(
154
+ "MACE-POLAR-1 checkpoint checksum mismatch: expected "
155
+ f"{args.expected_model_sha256}, found {model_sha256}"
156
+ )
157
+
158
+ atoms = read(input_path, format="xyz")
159
+ if len(atoms) != args.atom_count:
160
+ raise ValueError(
161
+ f"XYZ contains {len(atoms)} atoms; expected {args.atom_count}"
162
+ )
163
+ atoms.info["charge"] = args.charge
164
+ # MACE-POLAR-1 defines spin as the spin multiplicity (unpaired electrons + 1).
165
+ atoms.info["spin"] = args.multiplicity
166
+ atoms.info["external_field"] = [0.0, 0.0, 0.0]
167
+
168
+ started = perf_counter()
169
+ calculator = mace_polar(
170
+ model=str(checkpoint),
171
+ device=args.device,
172
+ default_dtype="float64",
173
+ )
174
+ supported_atomic_numbers = sorted(
175
+ int(value) for value in calculator.z_table.zs
176
+ )
177
+ unsupported = sorted(
178
+ set(int(value) for value in atoms.numbers) - set(supported_atomic_numbers)
179
+ )
180
+ if unsupported:
181
+ raise ValueError(
182
+ f"MACE-POLAR-1 checkpoint does not support atomic numbers {unsupported}"
183
+ )
184
+ atoms.calc = calculator
185
+ energy_ev = float(atoms.get_potential_energy())
186
+ forces_ev_per_angstrom = np.asarray(atoms.get_forces(), dtype=float)
187
+ raw_hessian = calculator.get_hessian(atoms=atoms)
188
+ hessian_ev_per_angstrom2 = _cartesian_matrix(raw_hessian, len(atoms))
189
+ finite_difference = _finite_difference_check(
190
+ atoms,
191
+ hessian_ev_per_angstrom2,
192
+ step_angstrom=args.finite_difference_step_angstrom,
193
+ )
194
+ if finite_difference["relative_error"] > args.max_hessian_vector_relative_error:
195
+ raise ValueError(
196
+ "MACE-POLAR-1 analytic Hessian failed its deterministic central-force "
197
+ "difference check: relative error "
198
+ f"{finite_difference['relative_error']:.6g} exceeds "
199
+ f"{args.max_hessian_vector_relative_error:.6g}"
200
+ )
201
+
202
+ hessian_hartree_per_bohr2 = (
203
+ hessian_ev_per_angstrom2 * ANGSTROM_PER_BOHR**2 / EV_PER_HARTREE
204
+ )
205
+ antisymmetry = float(
206
+ np.max(np.abs(hessian_hartree_per_bohr2 - hessian_hartree_per_bohr2.T))
207
+ )
208
+ np.save(output_hessian, hessian_hartree_per_bohr2)
209
+ report = {
210
+ "status": "complete",
211
+ "backend": "MACE-POLAR-1",
212
+ "model": args.model,
213
+ "model_path": str(checkpoint),
214
+ "model_sha256": model_sha256,
215
+ "versions": _versions(),
216
+ "device": args.device,
217
+ "default_dtype": "float64",
218
+ "atom_count": len(atoms),
219
+ "atomic_numbers": [int(value) for value in atoms.numbers],
220
+ "supported_atomic_numbers": supported_atomic_numbers,
221
+ "molecular_charge": args.charge,
222
+ "multiplicity": args.multiplicity,
223
+ "spin_input_semantics": "spin multiplicity (unpaired electrons + 1)",
224
+ "external_field": [0.0, 0.0, 0.0],
225
+ "energy_ev": energy_ev,
226
+ "energy_hartree": energy_ev / EV_PER_HARTREE,
227
+ "force_norm_ev_per_angstrom": float(np.linalg.norm(forces_ev_per_angstrom)),
228
+ "gradient_norm_hartree_per_bohr": float(
229
+ np.linalg.norm(forces_ev_per_angstrom)
230
+ * ANGSTROM_PER_BOHR
231
+ / EV_PER_HARTREE
232
+ ),
233
+ "raw_hessian_shape": list(np.asarray(raw_hessian).shape),
234
+ "hessian_shape": list(hessian_hartree_per_bohr2.shape),
235
+ "source_hessian_units": "eV/angstrom^2",
236
+ "output_hessian_units": "hartree/bohr^2",
237
+ "hessian_antisymmetry_max_hartree_per_bohr2": antisymmetry,
238
+ "hessian_sha256": _sha256(output_hessian),
239
+ "finite_difference_validation": finite_difference,
240
+ "runtime_seconds": perf_counter() - started,
241
+ "geometry_optimization_performed": False,
242
+ }
243
+ output_report.write_text(
244
+ json.dumps(report, indent=2, sort_keys=True) + "\n",
245
+ encoding="utf-8",
246
+ )
247
+ print(json.dumps(report, sort_keys=True))
248
+
249
+
250
+ def main() -> None:
251
+ parser = argparse.ArgumentParser()
252
+ parser.add_argument("--probe", action="store_true")
253
+ parser.add_argument("--xyz")
254
+ parser.add_argument("--atom-count", type=int)
255
+ parser.add_argument("--charge", type=int)
256
+ parser.add_argument("--multiplicity", type=int)
257
+ parser.add_argument("--model")
258
+ parser.add_argument("--device")
259
+ parser.add_argument("--expected-model-sha256")
260
+ parser.add_argument("--finite-difference-step-angstrom", type=float)
261
+ parser.add_argument("--max-hessian-vector-relative-error", type=float)
262
+ parser.add_argument("--output-hessian")
263
+ parser.add_argument("--output-report")
264
+ args = parser.parse_args()
265
+ if args.probe:
266
+ from mace.calculators import mace_polar # noqa: F401
267
+
268
+ _require_supported_mace_version()
269
+ print(
270
+ json.dumps(
271
+ {"status": "available", "versions": _versions()},
272
+ sort_keys=True,
273
+ )
274
+ )
275
+ return
276
+ required = (
277
+ "xyz",
278
+ "atom_count",
279
+ "charge",
280
+ "multiplicity",
281
+ "model",
282
+ "device",
283
+ "finite_difference_step_angstrom",
284
+ "max_hessian_vector_relative_error",
285
+ "output_hessian",
286
+ "output_report",
287
+ )
288
+ missing = [name for name in required if getattr(args, name) is None]
289
+ if missing:
290
+ parser.error(f"missing required arguments: {', '.join(missing)}")
291
+ _run(args)
292
+
293
+
294
+ if __name__ == "__main__":
295
+ main()