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,242 @@
|
|
|
1
|
+
"""PySCF-based ligand RESP/QMMESP charge derivation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
import json
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from mdprep.ambertools.mol2 import write_mol2_with_charges
|
|
13
|
+
from mdprep.ambertools.resp import AmberRespError, AmberRespFitResult, run_amber_resp_fit
|
|
14
|
+
from mdprep.charges.esp_grid import EspGridError, generate_merz_kollman_grid, write_grid_xyz
|
|
15
|
+
from mdprep.config.models import LigandConfig
|
|
16
|
+
from mdprep.ligands.extract import ExtractedLigand
|
|
17
|
+
from mdprep.qm.point_charges import PointChargeSelection, write_point_charge_files
|
|
18
|
+
from mdprep.qm.pyscf_esp import BOHR_PER_ANGSTROM, PySCFEspError, evaluate_ligand_esp, write_esp_values
|
|
19
|
+
from mdprep.qm.pyscf_runner import PySCFResult, PySCFRunnerError, run_pyscf_scf
|
|
20
|
+
from mdprep.structure.models import AtomRecord
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LigandPySCFChargeError(ValueError):
|
|
24
|
+
"""Raised when PySCF ligand charge derivation fails."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
QMMESP_CONFIRMATION = (
|
|
28
|
+
"The target-ligand PySCF single point was run as electrostatically embedded QM/MM "
|
|
29
|
+
"using pyscf.qmmm.mm_charge. Fixed MM point charges from the provisional Amber "
|
|
30
|
+
"system polarized the target-ligand QM density. The RESP fitting target contains "
|
|
31
|
+
"only the polarized target-ligand QM electrostatic potential; environment point "
|
|
32
|
+
"charges were not fitted and were not written to the ligand mol2."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
GAS_RESP_CONFIRMATION = "Gas-phase ligand ESP fit; no MM point charges were used."
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class LigandPySCFChargeResult:
|
|
41
|
+
method: str
|
|
42
|
+
qm_dir: Path
|
|
43
|
+
charged_mol2_path: Path
|
|
44
|
+
fitted_charges_csv_path: Path
|
|
45
|
+
fit_report_path: Path
|
|
46
|
+
pyscf_result: dict[str, object]
|
|
47
|
+
fit_result: dict[str, object]
|
|
48
|
+
grid_point_count: int
|
|
49
|
+
embedding_summary: dict[str, object] | None
|
|
50
|
+
warnings: list[str]
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> dict[str, object]:
|
|
53
|
+
return {
|
|
54
|
+
"method": self.method,
|
|
55
|
+
"qm_dir": str(self.qm_dir),
|
|
56
|
+
"charged_mol2_path": str(self.charged_mol2_path),
|
|
57
|
+
"fitted_charges_csv_path": str(self.fitted_charges_csv_path),
|
|
58
|
+
"fit_report_path": str(self.fit_report_path),
|
|
59
|
+
"pyscf_result": self.pyscf_result,
|
|
60
|
+
"fit_result": self.fit_result,
|
|
61
|
+
"grid_point_count": self.grid_point_count,
|
|
62
|
+
"embedding_summary": self.embedding_summary,
|
|
63
|
+
"warnings": self.warnings,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def derive_pyscf_charges(
|
|
68
|
+
*,
|
|
69
|
+
extracted: ExtractedLigand,
|
|
70
|
+
provisional_mol2_path: str | Path,
|
|
71
|
+
output_mol2_path: str | Path,
|
|
72
|
+
output_dir: str | Path,
|
|
73
|
+
method_name: str,
|
|
74
|
+
point_charges: PointChargeSelection | None = None,
|
|
75
|
+
) -> LigandPySCFChargeResult:
|
|
76
|
+
ligand = extracted.config
|
|
77
|
+
config = ligand.qmmesp
|
|
78
|
+
if config is None:
|
|
79
|
+
raise LigandPySCFChargeError(f"Ligand {ligand.id} requires a qmmesp block for {method_name}.")
|
|
80
|
+
if method_name == "qmmesp_pyscf" and point_charges is None:
|
|
81
|
+
raise LigandPySCFChargeError("qmmesp_pyscf requires an explicit provisional-system MM environment selection.")
|
|
82
|
+
if method_name == "qmmesp_pyscf" and point_charges is not None and point_charges.total_after_cutoff == 0:
|
|
83
|
+
raise LigandPySCFChargeError(
|
|
84
|
+
"qmmesp_pyscf selected zero MM point charges; refusing to run a gas-phase SCF "
|
|
85
|
+
"under a QMMESP label. Expand the environment selection or embedding cutoff."
|
|
86
|
+
)
|
|
87
|
+
if method_name == "gas_resp_pyscf" and point_charges is not None:
|
|
88
|
+
raise LigandPySCFChargeError("gas_resp_pyscf cannot accept MM embedding point charges.")
|
|
89
|
+
qm_dir = Path(output_dir) / "ligands" / ligand.id / "qm" / method_name
|
|
90
|
+
qm_dir.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
atoms = extracted.atoms
|
|
92
|
+
elements = [_atom_element(atom) for atom in atoms]
|
|
93
|
+
coords = np.asarray([[atom.x, atom.y, atom.z] for atom in atoms], dtype=float)
|
|
94
|
+
scf_charge = ligand.net_charge if config.scf_charge is None else config.scf_charge
|
|
95
|
+
scf_spin = ligand.multiplicity - 1 if config.scf_spin is None else config.scf_spin
|
|
96
|
+
embedded_qmmm = point_charges is not None
|
|
97
|
+
point_charge_count = 0 if point_charges is None else point_charges.total_after_cutoff
|
|
98
|
+
pyscf_input = {
|
|
99
|
+
"ligand_id": ligand.id,
|
|
100
|
+
"elements": elements,
|
|
101
|
+
"coordinates_angstrom": coords.tolist(),
|
|
102
|
+
"charge": scf_charge,
|
|
103
|
+
"spin": scf_spin,
|
|
104
|
+
"multiplicity": scf_spin + 1,
|
|
105
|
+
"method": config.method,
|
|
106
|
+
"basis": config.basis,
|
|
107
|
+
"max_cycle": config.max_cycle,
|
|
108
|
+
"conv_tol": config.conv_tol,
|
|
109
|
+
"num_threads": config.num_threads,
|
|
110
|
+
"max_memory_mb": config.max_memory_mb,
|
|
111
|
+
"checkpoint_path": str(qm_dir / "pyscf.chk"),
|
|
112
|
+
"calculation_type": (
|
|
113
|
+
"electrostatically_embedded_qm_mm_single_point"
|
|
114
|
+
if embedded_qmmm
|
|
115
|
+
else "gas_phase_qm_single_point"
|
|
116
|
+
),
|
|
117
|
+
"qm_region": "target_ligand_only",
|
|
118
|
+
"qm_atom_count": len(atoms),
|
|
119
|
+
"mm_region": "selected_non_target_atoms_from_provisional_amber_system" if embedded_qmmm else None,
|
|
120
|
+
"mm_point_charge_count": point_charge_count,
|
|
121
|
+
"electrostatic_embedding": embedded_qmmm,
|
|
122
|
+
"embedding_operator": "pyscf.qmmm.mm_charge" if embedded_qmmm else None,
|
|
123
|
+
"mm_point_charge_potential_in_scf_hamiltonian": embedded_qmmm,
|
|
124
|
+
"mm_bonded_and_lennard_jones_terms_in_scf": False,
|
|
125
|
+
"direct_mm_potential_in_resp_target": False,
|
|
126
|
+
"esp_grid": config.grid.model_dump(mode="json"),
|
|
127
|
+
"resp_fitting": config.resp_fitting.model_dump(mode="json"),
|
|
128
|
+
}
|
|
129
|
+
(qm_dir / "pyscf_input.json").write_text(json.dumps(pyscf_input, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
130
|
+
if point_charges is not None:
|
|
131
|
+
write_point_charge_files(
|
|
132
|
+
point_charges,
|
|
133
|
+
csv_path=qm_dir / "mm_point_charges.csv",
|
|
134
|
+
xyz_path=qm_dir / "mm_point_charges.xyz",
|
|
135
|
+
summary_path=qm_dir / "embedding_summary.json",
|
|
136
|
+
)
|
|
137
|
+
try:
|
|
138
|
+
grid = generate_merz_kollman_grid(
|
|
139
|
+
elements=elements,
|
|
140
|
+
coordinates=coords,
|
|
141
|
+
vdw_scale_factors=config.grid.vdw_scale_factors,
|
|
142
|
+
point_density_per_square_angstrom=config.grid.point_density_per_square_angstrom,
|
|
143
|
+
exclude_inside_vdw_scale=config.grid.exclude_inside_vdw_scale,
|
|
144
|
+
max_points=config.grid.max_points,
|
|
145
|
+
)
|
|
146
|
+
mm_charges = None if point_charges is None else point_charges.charge_array
|
|
147
|
+
mm_coords = None if point_charges is None else point_charges.coordinate_array
|
|
148
|
+
pyscf_result = run_pyscf_scf(
|
|
149
|
+
elements=elements,
|
|
150
|
+
coordinates=coords,
|
|
151
|
+
charge=scf_charge,
|
|
152
|
+
spin=scf_spin,
|
|
153
|
+
method=config.method,
|
|
154
|
+
basis=config.basis,
|
|
155
|
+
max_cycle=config.max_cycle,
|
|
156
|
+
conv_tol=config.conv_tol,
|
|
157
|
+
mm_charges=mm_charges,
|
|
158
|
+
mm_coordinates=mm_coords,
|
|
159
|
+
work_dir=qm_dir,
|
|
160
|
+
checkpoint_path=qm_dir / "pyscf.chk",
|
|
161
|
+
num_threads=config.num_threads,
|
|
162
|
+
max_memory_mb=config.max_memory_mb,
|
|
163
|
+
)
|
|
164
|
+
(qm_dir / "pyscf_stdout.txt").write_text(pyscf_result.stdout, encoding="utf-8")
|
|
165
|
+
(qm_dir / "pyscf_stderr.txt").write_text(pyscf_result.stderr, encoding="utf-8")
|
|
166
|
+
(qm_dir / "pyscf_result.json").write_text(
|
|
167
|
+
json.dumps(pyscf_result.to_dict(), indent=2, sort_keys=True) + "\n",
|
|
168
|
+
encoding="utf-8",
|
|
169
|
+
)
|
|
170
|
+
esp = evaluate_ligand_esp(
|
|
171
|
+
mol=pyscf_result.mol,
|
|
172
|
+
mf=pyscf_result.mf,
|
|
173
|
+
grid_coordinates_angstrom=grid.points,
|
|
174
|
+
)
|
|
175
|
+
write_grid_xyz(grid, qm_dir / "esp_grid.xyz")
|
|
176
|
+
write_esp_values(esp, str(qm_dir / "esp_values.dat"))
|
|
177
|
+
fit = run_amber_resp_fit(
|
|
178
|
+
provisional_mol2_path=provisional_mol2_path,
|
|
179
|
+
atom_coordinates_bohr=coords * BOHR_PER_ANGSTROM,
|
|
180
|
+
grid_coordinates_bohr=grid.points * BOHR_PER_ANGSTROM,
|
|
181
|
+
esp_values_au=esp,
|
|
182
|
+
total_charge=ligand.net_charge,
|
|
183
|
+
multiplicity=ligand.multiplicity,
|
|
184
|
+
atom_types=ligand.atom_types,
|
|
185
|
+
work_dir=qm_dir / "resp",
|
|
186
|
+
)
|
|
187
|
+
except (EspGridError, PySCFRunnerError, PySCFEspError, AmberRespError) as exc:
|
|
188
|
+
raise LigandPySCFChargeError(str(exc)) from exc
|
|
189
|
+
|
|
190
|
+
charged_mol2 = Path(output_mol2_path)
|
|
191
|
+
write_mol2_with_charges(provisional_mol2_path, [float(charge) for charge in fit.charges], charged_mol2)
|
|
192
|
+
charges_csv = qm_dir / "fitted_charges.csv"
|
|
193
|
+
_write_fitted_charges(charges_csv, atoms, fit)
|
|
194
|
+
fit_report = {
|
|
195
|
+
**fit.to_dict(),
|
|
196
|
+
"grid_point_count": int(len(grid.points)),
|
|
197
|
+
"fitted_charge_center_count": len(atoms),
|
|
198
|
+
"fitted_charge_centers": [atom.name for atom in atoms],
|
|
199
|
+
"calculation_type": pyscf_input["calculation_type"],
|
|
200
|
+
"electrostatic_embedding_applied": embedded_qmmm,
|
|
201
|
+
"embedding_operator": pyscf_input["embedding_operator"],
|
|
202
|
+
"mm_point_charge_count": point_charge_count,
|
|
203
|
+
"mm_point_charge_potential_in_scf_hamiltonian": embedded_qmmm,
|
|
204
|
+
"mm_bonded_and_lennard_jones_terms_in_scf": False,
|
|
205
|
+
"external_mm_potential_included_in_fit": False,
|
|
206
|
+
"esp_grid": config.grid.model_dump(mode="json"),
|
|
207
|
+
"confirmation": QMMESP_CONFIRMATION if point_charges is not None else GAS_RESP_CONFIRMATION,
|
|
208
|
+
}
|
|
209
|
+
fit_report_path = qm_dir / "fit_report.json"
|
|
210
|
+
fit_report_path.write_text(json.dumps(fit_report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
211
|
+
embedding_summary = None
|
|
212
|
+
if point_charges is not None:
|
|
213
|
+
embedding_summary = point_charges.to_dict()
|
|
214
|
+
return LigandPySCFChargeResult(
|
|
215
|
+
method=method_name,
|
|
216
|
+
qm_dir=qm_dir,
|
|
217
|
+
charged_mol2_path=charged_mol2,
|
|
218
|
+
fitted_charges_csv_path=charges_csv,
|
|
219
|
+
fit_report_path=fit_report_path,
|
|
220
|
+
pyscf_result=pyscf_result.to_dict(),
|
|
221
|
+
fit_result=fit_report,
|
|
222
|
+
grid_point_count=int(len(grid.points)),
|
|
223
|
+
embedding_summary=embedding_summary,
|
|
224
|
+
warnings=list(fit.warnings) + list(pyscf_result.warnings),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _atom_element(atom: AtomRecord) -> str:
|
|
229
|
+
if atom.element:
|
|
230
|
+
return atom.element
|
|
231
|
+
stripped = atom.name.strip()
|
|
232
|
+
while stripped and stripped[0].isdigit():
|
|
233
|
+
stripped = stripped[1:]
|
|
234
|
+
return stripped[:1].upper()
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _write_fitted_charges(path: Path, atoms: list[AtomRecord], fit: AmberRespFitResult) -> None:
|
|
238
|
+
with path.open("w", encoding="utf-8", newline="") as handle:
|
|
239
|
+
writer = csv.DictWriter(handle, fieldnames=["atom_index", "atom_name", "charge"])
|
|
240
|
+
writer.writeheader()
|
|
241
|
+
for index, (atom, charge) in enumerate(zip(atoms, fit.charges, strict=True), start=1):
|
|
242
|
+
writer.writerow({"atom_index": index, "atom_name": atom.name, "charge": float(charge)})
|
mdprep/ligands/report.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Ligand-stage report writers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from mdprep.ligands.workflow import LigandStageResult, LigandWorkflowItem
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
CSV_COLUMNS = [
|
|
14
|
+
"ligand_id",
|
|
15
|
+
"chain",
|
|
16
|
+
"resname",
|
|
17
|
+
"resid",
|
|
18
|
+
"icode",
|
|
19
|
+
"atom_count",
|
|
20
|
+
"charge_method",
|
|
21
|
+
"atom_types",
|
|
22
|
+
"net_charge",
|
|
23
|
+
"multiplicity",
|
|
24
|
+
"final_mol2",
|
|
25
|
+
"final_frcmod",
|
|
26
|
+
"charge_sum_final",
|
|
27
|
+
"coordinate_max_deviation",
|
|
28
|
+
"status",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def write_ligand_reports(
|
|
33
|
+
result: LigandStageResult,
|
|
34
|
+
*,
|
|
35
|
+
json_path: str | Path,
|
|
36
|
+
csv_path: str | Path,
|
|
37
|
+
markdown_path: str | Path,
|
|
38
|
+
) -> dict[str, Any]:
|
|
39
|
+
report = result.to_report_dict()
|
|
40
|
+
json_output = Path(json_path)
|
|
41
|
+
csv_output = Path(csv_path)
|
|
42
|
+
markdown_output = Path(markdown_path)
|
|
43
|
+
json_output.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
json_output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
45
|
+
_write_csv(result.ligands, csv_output)
|
|
46
|
+
markdown_output.write_text(_render_markdown(report), encoding="utf-8")
|
|
47
|
+
return report
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _write_csv(items: list[LigandWorkflowItem], path: Path) -> None:
|
|
51
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
with path.open("w", encoding="utf-8", newline="") as handle:
|
|
53
|
+
writer = csv.DictWriter(handle, fieldnames=CSV_COLUMNS)
|
|
54
|
+
writer.writeheader()
|
|
55
|
+
for item in items:
|
|
56
|
+
data = item.to_dict()
|
|
57
|
+
residue = data["residue_identity"]
|
|
58
|
+
validation = data.get("validation") or {}
|
|
59
|
+
writer.writerow(
|
|
60
|
+
{
|
|
61
|
+
"ligand_id": data["ligand_id"],
|
|
62
|
+
"chain": residue["chain_id"],
|
|
63
|
+
"resname": residue["resname"],
|
|
64
|
+
"resid": residue["resid"],
|
|
65
|
+
"icode": residue["icode"],
|
|
66
|
+
"atom_count": data["atom_count"],
|
|
67
|
+
"charge_method": data["charge_method"],
|
|
68
|
+
"atom_types": data["atom_types"],
|
|
69
|
+
"net_charge": data["net_charge"],
|
|
70
|
+
"multiplicity": data["multiplicity"],
|
|
71
|
+
"final_mol2": data["final_mol2_path"],
|
|
72
|
+
"final_frcmod": data["final_frcmod_path"],
|
|
73
|
+
"charge_sum_final": validation.get("charge_sum_final"),
|
|
74
|
+
"coordinate_max_deviation": validation.get("coordinate_max_deviation"),
|
|
75
|
+
"status": data["status"],
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _render_markdown(report: dict[str, Any]) -> str:
|
|
81
|
+
lines = ["# Ligand Report", ""]
|
|
82
|
+
provisional_system = report.get("qmmesp_provisional_system")
|
|
83
|
+
if provisional_system:
|
|
84
|
+
lines.extend(
|
|
85
|
+
[
|
|
86
|
+
"## QMMESP provisional system",
|
|
87
|
+
"",
|
|
88
|
+
"- Provisional charge model: AM1-BCC for every configured ligand/cofactor",
|
|
89
|
+
f"- prmtop: `{provisional_system['prmtop_path']}`",
|
|
90
|
+
f"- inpcrd: `{provisional_system['inpcrd_path']}`",
|
|
91
|
+
f"- tleap input: `{provisional_system['script_path']}`",
|
|
92
|
+
f"- tleap log: `{provisional_system['tleap']['log_path']}`",
|
|
93
|
+
"",
|
|
94
|
+
"### Provisional ligands",
|
|
95
|
+
"",
|
|
96
|
+
]
|
|
97
|
+
)
|
|
98
|
+
for provisional in report.get("qmmesp_provisional_ligands", []):
|
|
99
|
+
lines.append(
|
|
100
|
+
f"- `{provisional['ligand_id']}`: `{provisional['final_mol2_path']}` "
|
|
101
|
+
f"(charge method `{provisional['charge_method']}`)"
|
|
102
|
+
)
|
|
103
|
+
lines.append("")
|
|
104
|
+
for item in report["ligands"]:
|
|
105
|
+
lines.extend(
|
|
106
|
+
[
|
|
107
|
+
f"## {item['ligand_id']}",
|
|
108
|
+
"",
|
|
109
|
+
f"- Status: `{item['status']}`",
|
|
110
|
+
f"- Charge method: `{item['charge_method']}`",
|
|
111
|
+
f"- Atom types: `{item['atom_types']}`",
|
|
112
|
+
f"- Net charge: {item['net_charge']}",
|
|
113
|
+
f"- Extracted PDB: `{item['extracted_pdb_path']}`",
|
|
114
|
+
f"- Final mol2: `{item['final_mol2_path']}`",
|
|
115
|
+
f"- Final frcmod: `{item['final_frcmod_path']}`",
|
|
116
|
+
]
|
|
117
|
+
)
|
|
118
|
+
if item.get("antechamber"):
|
|
119
|
+
lines.append(f"- antechamber command: `{' '.join(item['antechamber']['command'])}`")
|
|
120
|
+
if item.get("parmchk2"):
|
|
121
|
+
lines.append(f"- parmchk2 command: `{' '.join(item['parmchk2']['command'])}`")
|
|
122
|
+
if item.get("qm"):
|
|
123
|
+
qm = item["qm"]
|
|
124
|
+
lines.extend(
|
|
125
|
+
[
|
|
126
|
+
"- PySCF charge derivation:",
|
|
127
|
+
f" - Method: `{qm['method']}`",
|
|
128
|
+
f" - Output directory: `{qm['qm_dir']}`",
|
|
129
|
+
f" - Grid points: {qm['grid_point_count']}",
|
|
130
|
+
f" - Fitted charge sum: {qm['fit_result']['charge_sum_final']}",
|
|
131
|
+
]
|
|
132
|
+
)
|
|
133
|
+
if qm["fit_result"].get("calculation_type"):
|
|
134
|
+
lines.append(f" - Calculation type: `{qm['fit_result']['calculation_type']}`")
|
|
135
|
+
if qm["fit_result"].get("embedding_operator"):
|
|
136
|
+
lines.append(f" - Electrostatic embedding: `{qm['fit_result']['embedding_operator']}`")
|
|
137
|
+
if qm["fit_result"].get("confirmation"):
|
|
138
|
+
lines.append(f" - Interpretation: {qm['fit_result']['confirmation']}")
|
|
139
|
+
if qm.get("embedding_summary"):
|
|
140
|
+
embedding = qm["embedding_summary"]
|
|
141
|
+
lines.append(f" - MM point charges: {embedding['point_charge_count_after_cutoff']}")
|
|
142
|
+
lines.append(f" - Target atom count: {embedding['target_atom_count']}")
|
|
143
|
+
warnings = item.get("warnings") or []
|
|
144
|
+
lines.append("- Warnings: " + ("; ".join(warnings) if warnings else "None"))
|
|
145
|
+
lines.append("")
|
|
146
|
+
return "\n".join(lines)
|