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
mdprep/qm/pyscf_esp.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""ESP evaluation from a PySCF density matrix."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
BOHR_PER_ANGSTROM = 1.8897261254578281
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PySCFEspError(ValueError):
|
|
12
|
+
"""Raised when ESP evaluation fails."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def evaluate_ligand_esp(
|
|
16
|
+
*,
|
|
17
|
+
mol: object,
|
|
18
|
+
mf: object,
|
|
19
|
+
grid_coordinates_angstrom: np.ndarray,
|
|
20
|
+
batch_size: int = 256,
|
|
21
|
+
) -> np.ndarray:
|
|
22
|
+
points = np.asarray(grid_coordinates_angstrom, dtype=float)
|
|
23
|
+
if points.ndim != 2 or points.shape[1] != 3:
|
|
24
|
+
raise PySCFEspError("Grid coordinates must have shape (n_points, 3).")
|
|
25
|
+
if len(points) == 0:
|
|
26
|
+
raise PySCFEspError("At least one ESP grid point is required.")
|
|
27
|
+
if batch_size < 1:
|
|
28
|
+
raise PySCFEspError("ESP evaluation batch_size must be positive.")
|
|
29
|
+
atom_coords_bohr = np.asarray(mol.atom_coords(), dtype=float)
|
|
30
|
+
atom_charges = np.asarray(mol.atom_charges(), dtype=float)
|
|
31
|
+
dm = mf.make_rdm1()
|
|
32
|
+
if isinstance(dm, tuple) or (hasattr(dm, "ndim") and dm.ndim == 3):
|
|
33
|
+
dm_total = np.asarray(dm[0]) + np.asarray(dm[1])
|
|
34
|
+
else:
|
|
35
|
+
dm_total = np.asarray(dm)
|
|
36
|
+
|
|
37
|
+
points_bohr = points * BOHR_PER_ANGSTROM
|
|
38
|
+
distances = np.linalg.norm(points_bohr[:, None, :] - atom_coords_bohr[None, :, :], axis=2)
|
|
39
|
+
if np.any(distances < 1.0e-10):
|
|
40
|
+
raise PySCFEspError("ESP grid point is on a nucleus.")
|
|
41
|
+
nuclear = np.sum(atom_charges[None, :] / distances, axis=1)
|
|
42
|
+
electronic = np.empty(len(points_bohr), dtype=float)
|
|
43
|
+
for start in range(0, len(points_bohr), batch_size):
|
|
44
|
+
stop = min(start + batch_size, len(points_bohr))
|
|
45
|
+
batch = points_bohr[start:stop]
|
|
46
|
+
try:
|
|
47
|
+
# PySCF's batched 1/r integral avoids one integral-engine setup per
|
|
48
|
+
# ESP point and returns (n_grid, n_ao, n_ao).
|
|
49
|
+
rinv = np.asarray(mol.intor("int1e_grids", grids=batch), dtype=float)
|
|
50
|
+
if rinv.shape[0] != len(batch):
|
|
51
|
+
raise ValueError("unexpected int1e_grids result shape")
|
|
52
|
+
electronic[start:stop] = -np.einsum("pij,ij->p", rinv, dm_total, optimize=True)
|
|
53
|
+
except (TypeError, ValueError, RuntimeError):
|
|
54
|
+
# Older supported PySCF builds may not expose int1e_grids. This is
|
|
55
|
+
# the same integral evaluated point-by-point, not a charge-model
|
|
56
|
+
# fallback.
|
|
57
|
+
for offset, point_bohr in enumerate(batch, start=start):
|
|
58
|
+
mol.set_rinv_origin(point_bohr)
|
|
59
|
+
rinv_point = mol.intor("int1e_rinv")
|
|
60
|
+
electronic[offset] = -float(np.einsum("ij,ij->", dm_total, rinv_point))
|
|
61
|
+
values = np.asarray(nuclear + electronic, dtype=float)
|
|
62
|
+
if not np.all(np.isfinite(values)):
|
|
63
|
+
raise PySCFEspError("PySCF ESP evaluation produced non-finite values.")
|
|
64
|
+
return values
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def write_esp_values(values: np.ndarray, path: str) -> None:
|
|
68
|
+
with open(path, "w", encoding="utf-8") as handle:
|
|
69
|
+
for value in values:
|
|
70
|
+
handle.write(f"{float(value):.12e}\n")
|
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
"""Lazy PySCF runner for ligand RESP/QMMESP charge derivation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import hashlib
|
|
7
|
+
import io
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PySCFRunnerError(RuntimeError):
|
|
15
|
+
"""Raised when PySCF cannot run the requested QM calculation."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class PySCFResult:
|
|
20
|
+
mol: object
|
|
21
|
+
mf: object
|
|
22
|
+
method: str
|
|
23
|
+
basis: str
|
|
24
|
+
dft_grid_level: int | None
|
|
25
|
+
charge: int
|
|
26
|
+
spin: int
|
|
27
|
+
multiplicity: int
|
|
28
|
+
electron_count: int
|
|
29
|
+
energy_hartree: float
|
|
30
|
+
converged: bool
|
|
31
|
+
cycles: int | None
|
|
32
|
+
scf_algorithm: str
|
|
33
|
+
initial_guess: str
|
|
34
|
+
initial_density_checkpoint_path: str | None
|
|
35
|
+
initial_density_checkpoint_sha256: str | None
|
|
36
|
+
initial_density_checkpoint_max_displacement_angstrom: float | None
|
|
37
|
+
initial_density_checkpoint_rms_displacement_angstrom: float | None
|
|
38
|
+
initial_density_checkpoint_projected: bool
|
|
39
|
+
adiis_precondition_cycles: int | None
|
|
40
|
+
adiis_precondition_dft_grid_level: int | None
|
|
41
|
+
precondition_cycles_completed: int
|
|
42
|
+
level_shift_mode: str
|
|
43
|
+
level_shift_hartree: float
|
|
44
|
+
damping_factor: float
|
|
45
|
+
diis_space: int
|
|
46
|
+
density_fitting: bool
|
|
47
|
+
auxiliary_basis: str | None
|
|
48
|
+
spin_square: float | None
|
|
49
|
+
effective_multiplicity: float | None
|
|
50
|
+
electrostatic_embedding_applied: bool
|
|
51
|
+
mm_point_charge_count: int
|
|
52
|
+
embedding_operator: str | None
|
|
53
|
+
stdout: str
|
|
54
|
+
stderr: str
|
|
55
|
+
warnings: list[str]
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict[str, object]:
|
|
58
|
+
return {
|
|
59
|
+
"method": self.method,
|
|
60
|
+
"basis": self.basis,
|
|
61
|
+
"dft_grid_level": self.dft_grid_level,
|
|
62
|
+
"charge": self.charge,
|
|
63
|
+
"spin": self.spin,
|
|
64
|
+
"multiplicity": self.multiplicity,
|
|
65
|
+
"electron_count": self.electron_count,
|
|
66
|
+
"energy_hartree": self.energy_hartree,
|
|
67
|
+
"converged": self.converged,
|
|
68
|
+
"cycles": self.cycles,
|
|
69
|
+
"scf_algorithm": self.scf_algorithm,
|
|
70
|
+
"initial_guess": self.initial_guess,
|
|
71
|
+
"initial_density_checkpoint_path": self.initial_density_checkpoint_path,
|
|
72
|
+
"initial_density_checkpoint_sha256": (
|
|
73
|
+
self.initial_density_checkpoint_sha256
|
|
74
|
+
),
|
|
75
|
+
"initial_density_checkpoint_max_displacement_angstrom": (
|
|
76
|
+
self.initial_density_checkpoint_max_displacement_angstrom
|
|
77
|
+
),
|
|
78
|
+
"initial_density_checkpoint_rms_displacement_angstrom": (
|
|
79
|
+
self.initial_density_checkpoint_rms_displacement_angstrom
|
|
80
|
+
),
|
|
81
|
+
"initial_density_checkpoint_projected": (
|
|
82
|
+
self.initial_density_checkpoint_projected
|
|
83
|
+
),
|
|
84
|
+
"adiis_precondition_cycles": self.adiis_precondition_cycles,
|
|
85
|
+
"adiis_precondition_dft_grid_level": (
|
|
86
|
+
self.adiis_precondition_dft_grid_level
|
|
87
|
+
),
|
|
88
|
+
"precondition_cycles_completed": self.precondition_cycles_completed,
|
|
89
|
+
"level_shift_mode": self.level_shift_mode,
|
|
90
|
+
"level_shift_hartree": self.level_shift_hartree,
|
|
91
|
+
"damping_factor": self.damping_factor,
|
|
92
|
+
"diis_space": self.diis_space,
|
|
93
|
+
"density_fitting": self.density_fitting,
|
|
94
|
+
"auxiliary_basis": self.auxiliary_basis,
|
|
95
|
+
"spin_square": self.spin_square,
|
|
96
|
+
"effective_multiplicity": self.effective_multiplicity,
|
|
97
|
+
"electrostatic_embedding_applied": self.electrostatic_embedding_applied,
|
|
98
|
+
"mm_point_charge_count": self.mm_point_charge_count,
|
|
99
|
+
"embedding_operator": self.embedding_operator,
|
|
100
|
+
"warnings": self.warnings,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def pyscf_available() -> bool:
|
|
105
|
+
try:
|
|
106
|
+
import pyscf # noqa: F401
|
|
107
|
+
except Exception:
|
|
108
|
+
return False
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def pyscf_version() -> str:
|
|
113
|
+
try:
|
|
114
|
+
import pyscf
|
|
115
|
+
except Exception:
|
|
116
|
+
return "not available"
|
|
117
|
+
return getattr(pyscf, "__version__", "unknown")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def scf_class_name(method: str, spin: int) -> str:
|
|
121
|
+
if method.upper() == "HF":
|
|
122
|
+
return "UHF" if spin > 0 else "RHF"
|
|
123
|
+
return "UKS" if spin > 0 else "RKS"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def run_pyscf_scf(
|
|
127
|
+
*,
|
|
128
|
+
elements: list[str],
|
|
129
|
+
coordinates: np.ndarray,
|
|
130
|
+
charge: int,
|
|
131
|
+
spin: int,
|
|
132
|
+
method: str,
|
|
133
|
+
basis: str,
|
|
134
|
+
max_cycle: int,
|
|
135
|
+
conv_tol: float,
|
|
136
|
+
mm_charges: np.ndarray | None = None,
|
|
137
|
+
mm_coordinates: np.ndarray | None = None,
|
|
138
|
+
work_dir: str | Path | None = None,
|
|
139
|
+
checkpoint_path: str | Path | None = None,
|
|
140
|
+
initial_density_checkpoint_path: str | Path | None = None,
|
|
141
|
+
expected_initial_density_checkpoint_sha256: str | None = None,
|
|
142
|
+
initial_density_checkpoint_max_displacement_angstrom: float = 0.0,
|
|
143
|
+
num_threads: int = 1,
|
|
144
|
+
max_memory_mb: int = 4000,
|
|
145
|
+
dft_grid_level: int = 3,
|
|
146
|
+
scf_algorithm: str = "diis",
|
|
147
|
+
initial_guess: str = "minao",
|
|
148
|
+
adiis_precondition_cycles: int = 15,
|
|
149
|
+
adiis_precondition_dft_grid_level: int | None = None,
|
|
150
|
+
level_shift_mode: str = "static",
|
|
151
|
+
level_shift_hartree: float = 0.0,
|
|
152
|
+
damping_factor: float = 0.0,
|
|
153
|
+
diis_space: int = 8,
|
|
154
|
+
density_fitting: bool = False,
|
|
155
|
+
auxiliary_basis: str | None = None,
|
|
156
|
+
) -> PySCFResult:
|
|
157
|
+
try:
|
|
158
|
+
from pyscf import dft, gto, lib, qmmm, scf
|
|
159
|
+
except Exception as exc:
|
|
160
|
+
raise PySCFRunnerError(
|
|
161
|
+
"PySCF is required for gas_resp_pyscf, qmmesp_pyscf, and "
|
|
162
|
+
"mcpb_resp_pyscf workflows."
|
|
163
|
+
) from exc
|
|
164
|
+
|
|
165
|
+
coords = np.asarray(coordinates, dtype=float)
|
|
166
|
+
if coords.shape != (len(elements), 3):
|
|
167
|
+
raise PySCFRunnerError("QM coordinate array shape does not match element list.")
|
|
168
|
+
if not elements or not np.all(np.isfinite(coords)):
|
|
169
|
+
raise PySCFRunnerError("QM elements and finite coordinates are required.")
|
|
170
|
+
if spin < 0:
|
|
171
|
+
raise PySCFRunnerError("PySCF spin must be non-negative.")
|
|
172
|
+
if max_cycle < 1 or not np.isfinite(conv_tol) or conv_tol <= 0:
|
|
173
|
+
raise PySCFRunnerError("PySCF max_cycle and conv_tol must be positive.")
|
|
174
|
+
if num_threads < 1:
|
|
175
|
+
raise PySCFRunnerError("PySCF num_threads must be at least 1.")
|
|
176
|
+
if max_memory_mb < 1:
|
|
177
|
+
raise PySCFRunnerError("PySCF max_memory_mb must be positive.")
|
|
178
|
+
if dft_grid_level < 0 or dft_grid_level > 9:
|
|
179
|
+
raise PySCFRunnerError("PySCF dft_grid_level must be between 0 and 9.")
|
|
180
|
+
if scf_algorithm not in {
|
|
181
|
+
"diis",
|
|
182
|
+
"newton",
|
|
183
|
+
"adiis_then_diis",
|
|
184
|
+
"adiis_then_newton",
|
|
185
|
+
}:
|
|
186
|
+
raise PySCFRunnerError(
|
|
187
|
+
"PySCF scf_algorithm must be 'diis', 'newton', "
|
|
188
|
+
"'adiis_then_diis', or 'adiis_then_newton'."
|
|
189
|
+
)
|
|
190
|
+
if initial_guess not in {"minao", "atom", "huckel"}:
|
|
191
|
+
raise PySCFRunnerError(
|
|
192
|
+
"PySCF initial_guess must be 'minao', 'atom', or 'huckel'."
|
|
193
|
+
)
|
|
194
|
+
if level_shift_mode not in {"static", "dynamic"}:
|
|
195
|
+
raise PySCFRunnerError(
|
|
196
|
+
"PySCF level_shift_mode must be 'static' or 'dynamic'."
|
|
197
|
+
)
|
|
198
|
+
if scf_algorithm != "diis" and level_shift_mode == "dynamic":
|
|
199
|
+
raise PySCFRunnerError(
|
|
200
|
+
"PySCF dynamic level shifting is available only with DIIS."
|
|
201
|
+
)
|
|
202
|
+
if adiis_precondition_cycles < 1:
|
|
203
|
+
raise PySCFRunnerError(
|
|
204
|
+
"PySCF adiis_precondition_cycles must be at least 1."
|
|
205
|
+
)
|
|
206
|
+
if adiis_precondition_dft_grid_level is not None:
|
|
207
|
+
if scf_algorithm not in {"adiis_then_diis", "adiis_then_newton"}:
|
|
208
|
+
raise PySCFRunnerError(
|
|
209
|
+
"PySCF adiis_precondition_dft_grid_level requires "
|
|
210
|
+
"scf_algorithm='adiis_then_diis' or 'adiis_then_newton'."
|
|
211
|
+
)
|
|
212
|
+
if not 0 <= adiis_precondition_dft_grid_level <= 9:
|
|
213
|
+
raise PySCFRunnerError(
|
|
214
|
+
"PySCF adiis_precondition_dft_grid_level must be between 0 and 9."
|
|
215
|
+
)
|
|
216
|
+
if not np.isfinite(level_shift_hartree) or level_shift_hartree < 0:
|
|
217
|
+
raise PySCFRunnerError("PySCF level_shift_hartree must be non-negative.")
|
|
218
|
+
if not np.isfinite(damping_factor) or not 0 <= damping_factor < 1:
|
|
219
|
+
raise PySCFRunnerError("PySCF damping_factor must be in [0, 1).")
|
|
220
|
+
if diis_space < 2:
|
|
221
|
+
raise PySCFRunnerError("PySCF diis_space must be at least 2.")
|
|
222
|
+
if auxiliary_basis is not None and not density_fitting:
|
|
223
|
+
raise PySCFRunnerError(
|
|
224
|
+
"PySCF auxiliary_basis requires density_fitting=True."
|
|
225
|
+
)
|
|
226
|
+
restart_values = (
|
|
227
|
+
initial_density_checkpoint_path,
|
|
228
|
+
expected_initial_density_checkpoint_sha256,
|
|
229
|
+
)
|
|
230
|
+
if any(value is not None for value in restart_values) and not all(
|
|
231
|
+
value is not None for value in restart_values
|
|
232
|
+
):
|
|
233
|
+
raise PySCFRunnerError(
|
|
234
|
+
"PySCF initial density checkpoint path and expected SHA-256 must "
|
|
235
|
+
"be provided together."
|
|
236
|
+
)
|
|
237
|
+
if initial_density_checkpoint_path is not None and scf_algorithm != "newton":
|
|
238
|
+
raise PySCFRunnerError(
|
|
239
|
+
"PySCF initial density checkpoints require scf_algorithm='newton'."
|
|
240
|
+
)
|
|
241
|
+
if (
|
|
242
|
+
not np.isfinite(initial_density_checkpoint_max_displacement_angstrom)
|
|
243
|
+
or initial_density_checkpoint_max_displacement_angstrom < 0
|
|
244
|
+
):
|
|
245
|
+
raise PySCFRunnerError(
|
|
246
|
+
"PySCF initial density checkpoint maximum displacement must be "
|
|
247
|
+
"finite and non-negative."
|
|
248
|
+
)
|
|
249
|
+
mm_charge_values = None if mm_charges is None else np.asarray(mm_charges, dtype=float)
|
|
250
|
+
mm_coordinate_values = None if mm_coordinates is None else np.asarray(mm_coordinates, dtype=float)
|
|
251
|
+
if mm_charge_values is None and mm_coordinate_values is not None:
|
|
252
|
+
raise PySCFRunnerError("MM charges are required when MM coordinates are provided.")
|
|
253
|
+
if mm_charge_values is not None:
|
|
254
|
+
if mm_coordinate_values is None:
|
|
255
|
+
raise PySCFRunnerError("MM coordinates are required when MM charges are provided.")
|
|
256
|
+
if mm_charge_values.ndim != 1 or mm_coordinate_values.shape != (len(mm_charge_values), 3):
|
|
257
|
+
raise PySCFRunnerError("MM point-charge arrays must have shapes (n,) and (n, 3).")
|
|
258
|
+
if not np.all(np.isfinite(mm_charge_values)) or not np.all(np.isfinite(mm_coordinate_values)):
|
|
259
|
+
raise PySCFRunnerError("MM point-charge arrays contain non-finite values.")
|
|
260
|
+
embedding_applied = mm_charge_values is not None and len(mm_charge_values) > 0
|
|
261
|
+
atom_spec = [(element, tuple(coord)) for element, coord in zip(elements, coords, strict=True)]
|
|
262
|
+
stdout = io.StringIO()
|
|
263
|
+
stderr = io.StringIO()
|
|
264
|
+
mol = gto.Mole()
|
|
265
|
+
mol.atom = atom_spec
|
|
266
|
+
mol.unit = "Angstrom"
|
|
267
|
+
mol.charge = int(charge)
|
|
268
|
+
mol.spin = int(spin)
|
|
269
|
+
mol.basis = basis
|
|
270
|
+
mol.max_memory = int(max_memory_mb)
|
|
271
|
+
mol.verbose = 4
|
|
272
|
+
mol.stdout = stdout
|
|
273
|
+
if work_dir is not None:
|
|
274
|
+
Path(work_dir).mkdir(parents=True, exist_ok=True)
|
|
275
|
+
checkpoint = Path(checkpoint_path).resolve() if checkpoint_path is not None else None
|
|
276
|
+
if checkpoint is not None:
|
|
277
|
+
checkpoint.parent.mkdir(parents=True, exist_ok=True)
|
|
278
|
+
restart_checkpoint = (
|
|
279
|
+
Path(initial_density_checkpoint_path).resolve()
|
|
280
|
+
if initial_density_checkpoint_path is not None
|
|
281
|
+
else None
|
|
282
|
+
)
|
|
283
|
+
restart_checkpoint_sha256: str | None = None
|
|
284
|
+
restart_max_displacement: float | None = None
|
|
285
|
+
restart_rms_displacement: float | None = None
|
|
286
|
+
restart_projected = False
|
|
287
|
+
restart_mol = None
|
|
288
|
+
if restart_checkpoint is not None:
|
|
289
|
+
if not restart_checkpoint.is_file():
|
|
290
|
+
raise PySCFRunnerError(
|
|
291
|
+
f"PySCF initial density checkpoint does not exist: {restart_checkpoint}"
|
|
292
|
+
)
|
|
293
|
+
with restart_checkpoint.open("rb") as handle:
|
|
294
|
+
restart_checkpoint_sha256 = hashlib.file_digest(handle, "sha256").hexdigest()
|
|
295
|
+
if restart_checkpoint_sha256.lower() != str(
|
|
296
|
+
expected_initial_density_checkpoint_sha256
|
|
297
|
+
).lower():
|
|
298
|
+
raise PySCFRunnerError(
|
|
299
|
+
"PySCF initial density checkpoint SHA-256 mismatch: expected "
|
|
300
|
+
f"{expected_initial_density_checkpoint_sha256}, found "
|
|
301
|
+
f"{restart_checkpoint_sha256}."
|
|
302
|
+
)
|
|
303
|
+
try:
|
|
304
|
+
restart_mol = lib.chkfile.load_mol(str(restart_checkpoint))
|
|
305
|
+
except Exception as exc:
|
|
306
|
+
raise PySCFRunnerError(
|
|
307
|
+
f"PySCF initial density checkpoint molecule could not be read: {exc}"
|
|
308
|
+
) from exc
|
|
309
|
+
previous_threads = int(lib.num_threads())
|
|
310
|
+
lib.num_threads(int(num_threads))
|
|
311
|
+
try:
|
|
312
|
+
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
|
|
313
|
+
try:
|
|
314
|
+
mol.build()
|
|
315
|
+
except Exception as exc:
|
|
316
|
+
raise PySCFRunnerError(f"PySCF molecule build failed: {exc}") from exc
|
|
317
|
+
if restart_mol is not None:
|
|
318
|
+
identity_matches = (
|
|
319
|
+
int(restart_mol.natm) == int(mol.natm)
|
|
320
|
+
and int(restart_mol.charge) == int(mol.charge)
|
|
321
|
+
and int(restart_mol.spin) == int(mol.spin)
|
|
322
|
+
and int(restart_mol.nao_nr()) == int(mol.nao_nr())
|
|
323
|
+
and np.array_equal(restart_mol.atom_charges(), mol.atom_charges())
|
|
324
|
+
)
|
|
325
|
+
if not identity_matches:
|
|
326
|
+
raise PySCFRunnerError(
|
|
327
|
+
"PySCF initial density checkpoint molecule does not exactly match "
|
|
328
|
+
"the current atom order/elements, charge, spin, and basis dimensions."
|
|
329
|
+
)
|
|
330
|
+
displacement = np.linalg.norm(
|
|
331
|
+
restart_mol.atom_coords(unit="Angstrom")
|
|
332
|
+
- mol.atom_coords(unit="Angstrom"),
|
|
333
|
+
axis=1,
|
|
334
|
+
)
|
|
335
|
+
restart_max_displacement = float(np.max(displacement, initial=0.0))
|
|
336
|
+
restart_rms_displacement = float(
|
|
337
|
+
np.sqrt(np.mean(np.square(displacement)))
|
|
338
|
+
)
|
|
339
|
+
if (
|
|
340
|
+
restart_max_displacement
|
|
341
|
+
> initial_density_checkpoint_max_displacement_angstrom + 1.0e-12
|
|
342
|
+
):
|
|
343
|
+
raise PySCFRunnerError(
|
|
344
|
+
"PySCF initial density checkpoint maximum coordinate displacement "
|
|
345
|
+
f"{restart_max_displacement:.6f} A exceeds the explicit limit "
|
|
346
|
+
f"{initial_density_checkpoint_max_displacement_angstrom:.6f} A."
|
|
347
|
+
)
|
|
348
|
+
restart_projected = restart_max_displacement > 1.0e-10
|
|
349
|
+
|
|
350
|
+
if method.upper() == "HF":
|
|
351
|
+
mf = scf.UHF(mol) if spin > 0 else scf.RHF(mol)
|
|
352
|
+
else:
|
|
353
|
+
try:
|
|
354
|
+
mf = dft.UKS(mol) if spin > 0 else dft.RKS(mol)
|
|
355
|
+
mf.xc = method
|
|
356
|
+
mf.grids.level = int(dft_grid_level)
|
|
357
|
+
except Exception as exc:
|
|
358
|
+
raise PySCFRunnerError(f"PySCF DFT setup failed for method {method!r}; use method: HF.") from exc
|
|
359
|
+
if density_fitting:
|
|
360
|
+
try:
|
|
361
|
+
mf = mf.density_fit(auxbasis=auxiliary_basis)
|
|
362
|
+
except Exception as exc:
|
|
363
|
+
raise PySCFRunnerError(
|
|
364
|
+
f"PySCF density-fitting setup failed: {exc}"
|
|
365
|
+
) from exc
|
|
366
|
+
mf.stdout = stdout
|
|
367
|
+
mf.max_cycle = int(max_cycle)
|
|
368
|
+
mf.conv_tol = float(conv_tol)
|
|
369
|
+
mf.max_memory = int(max_memory_mb)
|
|
370
|
+
mf.init_guess = initial_guess
|
|
371
|
+
final_static_level_shift = (
|
|
372
|
+
float(level_shift_hartree) if level_shift_mode == "static" else 0.0
|
|
373
|
+
)
|
|
374
|
+
mf.level_shift = (
|
|
375
|
+
0.0
|
|
376
|
+
if scf_algorithm in {"adiis_then_diis", "adiis_then_newton"}
|
|
377
|
+
else final_static_level_shift
|
|
378
|
+
)
|
|
379
|
+
mf.damp = float(damping_factor)
|
|
380
|
+
mf.diis_space = int(diis_space)
|
|
381
|
+
if checkpoint is not None:
|
|
382
|
+
mf.chkfile = str(checkpoint)
|
|
383
|
+
if embedding_applied:
|
|
384
|
+
try:
|
|
385
|
+
mf = qmmm.mm_charge(
|
|
386
|
+
mf,
|
|
387
|
+
mm_coordinate_values,
|
|
388
|
+
mm_charge_values,
|
|
389
|
+
unit="Angstrom",
|
|
390
|
+
)
|
|
391
|
+
mf.stdout = stdout
|
|
392
|
+
if checkpoint is not None:
|
|
393
|
+
mf.chkfile = str(checkpoint)
|
|
394
|
+
except Exception as exc:
|
|
395
|
+
raise PySCFRunnerError(f"PySCF point-charge embedding could not be applied: {exc}") from exc
|
|
396
|
+
|
|
397
|
+
if level_shift_mode == "dynamic" and level_shift_hartree > 0:
|
|
398
|
+
try:
|
|
399
|
+
mf = scf.addons.dynamic_level_shift_(
|
|
400
|
+
mf,
|
|
401
|
+
factor=float(level_shift_hartree),
|
|
402
|
+
)
|
|
403
|
+
mf.stdout = stdout
|
|
404
|
+
if checkpoint is not None:
|
|
405
|
+
mf.chkfile = str(checkpoint)
|
|
406
|
+
except Exception as exc:
|
|
407
|
+
raise PySCFRunnerError(
|
|
408
|
+
f"PySCF dynamic level-shift setup failed: {exc}"
|
|
409
|
+
) from exc
|
|
410
|
+
|
|
411
|
+
if scf_algorithm == "newton":
|
|
412
|
+
try:
|
|
413
|
+
mf = mf.newton()
|
|
414
|
+
mf.stdout = stdout
|
|
415
|
+
mf.max_cycle = int(max_cycle)
|
|
416
|
+
mf.conv_tol = float(conv_tol)
|
|
417
|
+
mf.max_memory = int(max_memory_mb)
|
|
418
|
+
if checkpoint is not None:
|
|
419
|
+
mf.chkfile = str(checkpoint)
|
|
420
|
+
except Exception as exc:
|
|
421
|
+
raise PySCFRunnerError(
|
|
422
|
+
f"PySCF second-order Newton SCF setup failed: {exc}"
|
|
423
|
+
) from exc
|
|
424
|
+
|
|
425
|
+
restart_density = None
|
|
426
|
+
if restart_checkpoint is not None:
|
|
427
|
+
try:
|
|
428
|
+
restart_density = mf.init_guess_by_chkfile(
|
|
429
|
+
str(restart_checkpoint),
|
|
430
|
+
project=restart_projected,
|
|
431
|
+
)
|
|
432
|
+
except Exception as exc:
|
|
433
|
+
raise PySCFRunnerError(
|
|
434
|
+
f"PySCF initial density checkpoint could not be loaded: {exc}"
|
|
435
|
+
) from exc
|
|
436
|
+
|
|
437
|
+
precondition_cycles_completed = 0
|
|
438
|
+
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
|
|
439
|
+
try:
|
|
440
|
+
if scf_algorithm in {"adiis_then_diis", "adiis_then_newton"}:
|
|
441
|
+
if method.upper() != "HF" and adiis_precondition_dft_grid_level is not None:
|
|
442
|
+
mf.grids.level = int(adiis_precondition_dft_grid_level)
|
|
443
|
+
mf.DIIS = scf.ADIIS
|
|
444
|
+
mf.diis = True
|
|
445
|
+
mf.max_cycle = int(adiis_precondition_cycles)
|
|
446
|
+
mf.kernel()
|
|
447
|
+
precondition_cycles_completed = int(
|
|
448
|
+
getattr(mf, "cycles", adiis_precondition_cycles)
|
|
449
|
+
)
|
|
450
|
+
precondition_density = mf.make_rdm1()
|
|
451
|
+
mf.converged = False
|
|
452
|
+
mf.DIIS = scf.CDIIS
|
|
453
|
+
mf.diis = True
|
|
454
|
+
mf.level_shift = final_static_level_shift
|
|
455
|
+
mf.max_cycle = int(max_cycle)
|
|
456
|
+
if method.upper() != "HF" and adiis_precondition_dft_grid_level is not None:
|
|
457
|
+
mf.grids.level = int(dft_grid_level)
|
|
458
|
+
mf.grids.reset(mol)
|
|
459
|
+
if scf_algorithm == "adiis_then_newton":
|
|
460
|
+
mf = mf.newton()
|
|
461
|
+
mf.stdout = stdout
|
|
462
|
+
mf.max_cycle = int(max_cycle)
|
|
463
|
+
mf.conv_tol = float(conv_tol)
|
|
464
|
+
mf.max_memory = int(max_memory_mb)
|
|
465
|
+
if checkpoint is not None:
|
|
466
|
+
mf.chkfile = str(checkpoint)
|
|
467
|
+
energy = mf.kernel(dm0=precondition_density)
|
|
468
|
+
else:
|
|
469
|
+
energy = mf.kernel(dm0=restart_density)
|
|
470
|
+
except Exception as exc:
|
|
471
|
+
raise PySCFRunnerError(f"PySCF SCF calculation failed: {exc}") from exc
|
|
472
|
+
finally:
|
|
473
|
+
lib.num_threads(previous_threads)
|
|
474
|
+
converged = bool(getattr(mf, "converged", False))
|
|
475
|
+
if not converged:
|
|
476
|
+
raise PySCFRunnerError("PySCF SCF did not converge.")
|
|
477
|
+
cycles_value = getattr(mf, "cycles", None)
|
|
478
|
+
cycles = (
|
|
479
|
+
precondition_cycles_completed + int(cycles_value)
|
|
480
|
+
if cycles_value is not None
|
|
481
|
+
else None
|
|
482
|
+
)
|
|
483
|
+
spin_square: float | None = None
|
|
484
|
+
effective_multiplicity: float | None = None
|
|
485
|
+
if spin > 0 and hasattr(mf, "spin_square"):
|
|
486
|
+
try:
|
|
487
|
+
spin_values = mf.spin_square()
|
|
488
|
+
spin_square = float(spin_values[0])
|
|
489
|
+
effective_multiplicity = float(spin_values[1])
|
|
490
|
+
except Exception:
|
|
491
|
+
spin_square = None
|
|
492
|
+
effective_multiplicity = None
|
|
493
|
+
return PySCFResult(
|
|
494
|
+
mol=mol,
|
|
495
|
+
mf=mf,
|
|
496
|
+
method=method,
|
|
497
|
+
basis=basis,
|
|
498
|
+
dft_grid_level=None if method.upper() == "HF" else int(dft_grid_level),
|
|
499
|
+
charge=int(charge),
|
|
500
|
+
spin=int(spin),
|
|
501
|
+
multiplicity=int(spin) + 1,
|
|
502
|
+
electron_count=int(mol.nelectron),
|
|
503
|
+
energy_hartree=float(energy),
|
|
504
|
+
converged=converged,
|
|
505
|
+
cycles=cycles,
|
|
506
|
+
scf_algorithm=scf_algorithm,
|
|
507
|
+
initial_guess=initial_guess,
|
|
508
|
+
initial_density_checkpoint_path=(
|
|
509
|
+
str(restart_checkpoint) if restart_checkpoint is not None else None
|
|
510
|
+
),
|
|
511
|
+
initial_density_checkpoint_sha256=restart_checkpoint_sha256,
|
|
512
|
+
initial_density_checkpoint_max_displacement_angstrom=(
|
|
513
|
+
restart_max_displacement
|
|
514
|
+
),
|
|
515
|
+
initial_density_checkpoint_rms_displacement_angstrom=(
|
|
516
|
+
restart_rms_displacement
|
|
517
|
+
),
|
|
518
|
+
initial_density_checkpoint_projected=restart_projected,
|
|
519
|
+
adiis_precondition_cycles=(
|
|
520
|
+
int(adiis_precondition_cycles)
|
|
521
|
+
if scf_algorithm in {"adiis_then_diis", "adiis_then_newton"}
|
|
522
|
+
else None
|
|
523
|
+
),
|
|
524
|
+
adiis_precondition_dft_grid_level=(
|
|
525
|
+
int(adiis_precondition_dft_grid_level)
|
|
526
|
+
if adiis_precondition_dft_grid_level is not None
|
|
527
|
+
else None
|
|
528
|
+
),
|
|
529
|
+
precondition_cycles_completed=precondition_cycles_completed,
|
|
530
|
+
level_shift_mode=level_shift_mode,
|
|
531
|
+
level_shift_hartree=float(level_shift_hartree),
|
|
532
|
+
damping_factor=float(damping_factor),
|
|
533
|
+
diis_space=int(diis_space),
|
|
534
|
+
density_fitting=bool(density_fitting),
|
|
535
|
+
auxiliary_basis=auxiliary_basis,
|
|
536
|
+
spin_square=spin_square,
|
|
537
|
+
effective_multiplicity=effective_multiplicity,
|
|
538
|
+
electrostatic_embedding_applied=embedding_applied,
|
|
539
|
+
mm_point_charge_count=0 if mm_charge_values is None else len(mm_charge_values),
|
|
540
|
+
embedding_operator="pyscf.qmmm.mm_charge" if embedding_applied else None,
|
|
541
|
+
stdout=stdout.getvalue(),
|
|
542
|
+
stderr=stderr.getvalue(),
|
|
543
|
+
warnings=[],
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def run_pyscf_analytic_hessian(
|
|
548
|
+
mf: object,
|
|
549
|
+
*,
|
|
550
|
+
num_threads: int = 1,
|
|
551
|
+
) -> np.ndarray:
|
|
552
|
+
"""Evaluate one fixed-geometry analytic Hessian with bounded PySCF threads."""
|
|
553
|
+
|
|
554
|
+
if num_threads < 1:
|
|
555
|
+
raise PySCFRunnerError("PySCF num_threads must be at least 1.")
|
|
556
|
+
try:
|
|
557
|
+
from pyscf import lib
|
|
558
|
+
except Exception as exc:
|
|
559
|
+
raise PySCFRunnerError(
|
|
560
|
+
"PySCF is required for the MCPB analytic Hessian."
|
|
561
|
+
) from exc
|
|
562
|
+
previous_threads = int(lib.num_threads())
|
|
563
|
+
lib.num_threads(int(num_threads))
|
|
564
|
+
try:
|
|
565
|
+
try:
|
|
566
|
+
return np.asarray(mf.Hessian().kernel(), dtype=float)
|
|
567
|
+
except Exception as exc:
|
|
568
|
+
raise PySCFRunnerError(f"PySCF analytic Hessian calculation failed: {exc}") from exc
|
|
569
|
+
finally:
|
|
570
|
+
lib.num_threads(previous_threads)
|