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/metals/report.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Metal-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.metals.workflow import MetalStageResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def write_metal_reports(
|
|
13
|
+
result: MetalStageResult,
|
|
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
|
+
"# Metal Preparation Report",
|
|
31
|
+
"",
|
|
32
|
+
f"- Complete: {report['complete']}",
|
|
33
|
+
f"- Configured sites: {len(report['sites'])}",
|
|
34
|
+
"",
|
|
35
|
+
"## Nonbonded Sites",
|
|
36
|
+
"",
|
|
37
|
+
]
|
|
38
|
+
if not report["nonbonded_sites"]:
|
|
39
|
+
lines.append("- None")
|
|
40
|
+
for site in report["nonbonded_sites"]:
|
|
41
|
+
lines.append(f"- `{site['site_id']}`: `{site['parameter_set']}`")
|
|
42
|
+
for ion in site["ions"]:
|
|
43
|
+
parameter = ion["parameter"]
|
|
44
|
+
selector = ion["selector"]
|
|
45
|
+
lines.append(
|
|
46
|
+
f" - {selector['chain'] or '<blank>'}:{selector['resname']}"
|
|
47
|
+
f"{selector['resid']}@{selector['atom_name']}: {parameter['atom_type']}, "
|
|
48
|
+
f"Rmin/2={parameter['rmin_over_2_angstrom']:.6f} A, "
|
|
49
|
+
f"epsilon={parameter['epsilon_kcal_mol']:.8f} kcal/mol"
|
|
50
|
+
)
|
|
51
|
+
lines.extend(["", "## Bonded MCPB.py Site", ""])
|
|
52
|
+
bonded = report["bonded_mcpb_site"]
|
|
53
|
+
if bonded is None:
|
|
54
|
+
lines.append("- None")
|
|
55
|
+
else:
|
|
56
|
+
lines.extend(
|
|
57
|
+
[
|
|
58
|
+
f"- Site: `{bonded['site_id']}`",
|
|
59
|
+
f"- Workflow: `{bonded['workflow']}`",
|
|
60
|
+
f"- Complete: {bonded['complete']}",
|
|
61
|
+
f"- Work directory: `{bonded['work_dir']}`",
|
|
62
|
+
f"- MCPB.py commands run: {len(bonded['runs'])}",
|
|
63
|
+
f"- Residues renamed by MCPB.py: {len(bonded['residue_renames'])}",
|
|
64
|
+
]
|
|
65
|
+
)
|
|
66
|
+
if not bonded["complete"]:
|
|
67
|
+
lines.append(
|
|
68
|
+
"- Required QM artifacts for a `complete` rerun: "
|
|
69
|
+
+ ", ".join(f"`{name}`" for name in bonded["expected_qm_artifacts"])
|
|
70
|
+
)
|
|
71
|
+
refitted_ligands = bonded.get("refitted_ligands", [])
|
|
72
|
+
lines.extend(["", "### Joint RESP-fitted ligands", ""])
|
|
73
|
+
if refitted_ligands:
|
|
74
|
+
for ligand in refitted_ligands:
|
|
75
|
+
lines.append(
|
|
76
|
+
f"- `{ligand['ligand_id']}` -> `{ligand['final_resname']}`: "
|
|
77
|
+
f"mol2 `{ligand['final_mol2_path']}`, fragment charge "
|
|
78
|
+
f"{ligand['fragment_charge']:.6f} e"
|
|
79
|
+
)
|
|
80
|
+
else:
|
|
81
|
+
lines.append("- None")
|
|
82
|
+
comparison = bonded["parameter_comparison"]
|
|
83
|
+
if comparison is not None:
|
|
84
|
+
lines.extend(
|
|
85
|
+
[
|
|
86
|
+
"",
|
|
87
|
+
"### MCPB parameter comparison",
|
|
88
|
+
"",
|
|
89
|
+
f"- Status: `{comparison['status']}`",
|
|
90
|
+
f"- Matched Seminario terms: {comparison['matched_term_count']}",
|
|
91
|
+
"- Non-positive candidate force constants: "
|
|
92
|
+
f"{len(comparison['nonpositive_candidate_force_constants'])}",
|
|
93
|
+
"- Non-positive reference force constants: "
|
|
94
|
+
f"{len(comparison['nonpositive_reference_force_constants'])}",
|
|
95
|
+
f"- Detailed report: `{comparison['json_path']}`",
|
|
96
|
+
]
|
|
97
|
+
)
|
|
98
|
+
lines.extend(["", "## Pre-MCPB Hydrogenation", ""])
|
|
99
|
+
hydrogenation = report["pre_mcpb_hydrogenation"]
|
|
100
|
+
if hydrogenation is None:
|
|
101
|
+
lines.append("- None")
|
|
102
|
+
else:
|
|
103
|
+
lines.extend(
|
|
104
|
+
[
|
|
105
|
+
f"- Hydrogenated PDB: `{hydrogenation['restored_output_pdb_path']}`",
|
|
106
|
+
f"- tleap script: `{hydrogenation['script_path']}`",
|
|
107
|
+
f"- Metal-donor protonation checks: {len(hydrogenation['donor_protonation_checks'])}",
|
|
108
|
+
]
|
|
109
|
+
)
|
|
110
|
+
lines.extend(["", "## 12-6-4 Atom Types", ""])
|
|
111
|
+
lines.extend(f"- `{atom_type}`" for atom_type in report["c4_atom_types"])
|
|
112
|
+
if not report["c4_atom_types"]:
|
|
113
|
+
lines.append("- None")
|
|
114
|
+
lines.extend(["", "## Warnings", ""])
|
|
115
|
+
lines.extend(f"- {warning}" for warning in report["warnings"])
|
|
116
|
+
if not report["warnings"]:
|
|
117
|
+
lines.append("- None")
|
|
118
|
+
lines.append("")
|
|
119
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""Metal preparation stage orchestration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field, replace
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from mdprep.config.models import ManifestConfig, NonbondedMetalConfig
|
|
10
|
+
from mdprep.ligands.workflow import LigandStageResult
|
|
11
|
+
from mdprep.metals.coordination import ResolvedMetalSite, resolve_metal_sites
|
|
12
|
+
from mdprep.metals.hydrogenate import MetalHydrogenationResult, hydrogenate_for_mcpb
|
|
13
|
+
from mdprep.metals.mcpb import McpbSiteResult, run_mcpb_site
|
|
14
|
+
from mdprep.metals.nonbonded import (
|
|
15
|
+
NonbondedIonArtifact,
|
|
16
|
+
NonbondedSiteResult,
|
|
17
|
+
prepare_nonbonded_site,
|
|
18
|
+
)
|
|
19
|
+
from mdprep.protonation.apply import ProtonationResult
|
|
20
|
+
from mdprep.structure.models import PdbStructure
|
|
21
|
+
from mdprep.structure.pdb import read_pdb
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from mdprep.refinement.workflow import RefinementResult
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MetalWorkflowError(ValueError):
|
|
28
|
+
"""Raised when metal preparation is incomplete or internally inconsistent."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class MetalStageResult:
|
|
33
|
+
sites: list[ResolvedMetalSite]
|
|
34
|
+
nonbonded_sites: list[NonbondedSiteResult] = field(default_factory=list)
|
|
35
|
+
mcpb_site: McpbSiteResult | None = None
|
|
36
|
+
pre_mcpb_hydrogenation: MetalHydrogenationResult | None = None
|
|
37
|
+
structure: PdbStructure | None = None
|
|
38
|
+
complete: bool = True
|
|
39
|
+
leap_setup_commands: list[str] = field(default_factory=list)
|
|
40
|
+
leap_bond_commands: list[str] = field(default_factory=list)
|
|
41
|
+
c4_atom_types: list[str] = field(default_factory=list)
|
|
42
|
+
warnings: list[str] = field(default_factory=list)
|
|
43
|
+
|
|
44
|
+
def to_report_dict(self) -> dict[str, object]:
|
|
45
|
+
return {
|
|
46
|
+
"complete": self.complete,
|
|
47
|
+
"sites": [site.to_dict() for site in self.sites],
|
|
48
|
+
"nonbonded_sites": [site.to_dict() for site in self.nonbonded_sites],
|
|
49
|
+
"bonded_mcpb_site": self.mcpb_site.to_dict() if self.mcpb_site else None,
|
|
50
|
+
"pre_mcpb_hydrogenation": (
|
|
51
|
+
self.pre_mcpb_hydrogenation.to_dict()
|
|
52
|
+
if self.pre_mcpb_hydrogenation
|
|
53
|
+
else None
|
|
54
|
+
),
|
|
55
|
+
"leap_setup_commands": self.leap_setup_commands,
|
|
56
|
+
"leap_bond_commands": self.leap_bond_commands,
|
|
57
|
+
"c4_atom_types": self.c4_atom_types,
|
|
58
|
+
"final_metal_stage_pdb": str(self.structure.path) if self.structure else None,
|
|
59
|
+
"warnings": self.warnings,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def run_metal_stage(
|
|
64
|
+
structure: PdbStructure,
|
|
65
|
+
manifest: ManifestConfig,
|
|
66
|
+
*,
|
|
67
|
+
output_dir: str | Path,
|
|
68
|
+
ligand_result: LigandStageResult,
|
|
69
|
+
protonation_result: ProtonationResult,
|
|
70
|
+
refinement_result: "RefinementResult | None" = None,
|
|
71
|
+
) -> MetalStageResult:
|
|
72
|
+
sites = resolve_metal_sites(structure, manifest, validate_mcpb_cutoff=True)
|
|
73
|
+
if not sites:
|
|
74
|
+
return MetalStageResult(sites=[], structure=structure)
|
|
75
|
+
output = Path(output_dir) / "metals"
|
|
76
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
nonbonded_results: list[NonbondedSiteResult] = []
|
|
78
|
+
mcpb_result: McpbSiteResult | None = None
|
|
79
|
+
hydrogenation_result: MetalHydrogenationResult | None = None
|
|
80
|
+
setup_commands: list[str] = []
|
|
81
|
+
bond_commands: list[str] = []
|
|
82
|
+
c4_types: list[str] = []
|
|
83
|
+
warnings: list[str] = []
|
|
84
|
+
final_structure = structure
|
|
85
|
+
parameter_by_type: dict[str, tuple[str, str]] = {}
|
|
86
|
+
provisional_parameter_by_type: dict[str, tuple[str, str]] = {}
|
|
87
|
+
provisional_template_by_resname: dict[str, tuple[str, int, str]] = {}
|
|
88
|
+
|
|
89
|
+
provisional_setup_commands: list[str] = []
|
|
90
|
+
bonded_site: ResolvedMetalSite | None = None
|
|
91
|
+
for site in sites:
|
|
92
|
+
site_dir = output / _safe_dir_name(site.config.id)
|
|
93
|
+
if site.config.model == "nonbonded":
|
|
94
|
+
result = prepare_nonbonded_site(
|
|
95
|
+
site,
|
|
96
|
+
water_model=manifest.protein.water_model,
|
|
97
|
+
output_dir=site_dir,
|
|
98
|
+
)
|
|
99
|
+
for artifact in result.ions:
|
|
100
|
+
_register_ion_artifact(
|
|
101
|
+
artifact,
|
|
102
|
+
parameter_by_type=parameter_by_type,
|
|
103
|
+
template_by_resname=None,
|
|
104
|
+
context="final topology",
|
|
105
|
+
)
|
|
106
|
+
_register_ion_artifact(
|
|
107
|
+
artifact,
|
|
108
|
+
parameter_by_type=provisional_parameter_by_type,
|
|
109
|
+
template_by_resname=provisional_template_by_resname,
|
|
110
|
+
context="pre-MCPB provisional topology",
|
|
111
|
+
)
|
|
112
|
+
if artifact.parameter.parameter_set == "12_6_4":
|
|
113
|
+
c4_types.append(artifact.parameter.atom_type)
|
|
114
|
+
nonbonded_results.append(result)
|
|
115
|
+
setup_commands.extend(result.leap_commands)
|
|
116
|
+
provisional_setup_commands.extend(result.leap_commands)
|
|
117
|
+
else:
|
|
118
|
+
if bonded_site is not None:
|
|
119
|
+
raise MetalWorkflowError("Only one bonded MCPB.py site may be prepared per system.")
|
|
120
|
+
bonded_site = site
|
|
121
|
+
assert site.config.mcpb is not None
|
|
122
|
+
family = site.config.mcpb.provisional_nonbonded_parameter_set
|
|
123
|
+
if family is None:
|
|
124
|
+
raise MetalWorkflowError(
|
|
125
|
+
f"Bonded site {site.config.id!r} has no provisional nonbonded ion model."
|
|
126
|
+
)
|
|
127
|
+
provisional_config = site.config.model_copy(
|
|
128
|
+
update={
|
|
129
|
+
"model": "nonbonded",
|
|
130
|
+
"nonbonded": NonbondedMetalConfig(parameter_set=family),
|
|
131
|
+
"mcpb": None,
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
provisional_site = replace(site, config=provisional_config)
|
|
135
|
+
provisional = prepare_nonbonded_site(
|
|
136
|
+
provisional_site,
|
|
137
|
+
water_model=manifest.protein.water_model,
|
|
138
|
+
output_dir=site_dir / "provisional_nonbonded",
|
|
139
|
+
)
|
|
140
|
+
for artifact in provisional.ions:
|
|
141
|
+
_register_ion_artifact(
|
|
142
|
+
artifact,
|
|
143
|
+
parameter_by_type=provisional_parameter_by_type,
|
|
144
|
+
template_by_resname=provisional_template_by_resname,
|
|
145
|
+
context="pre-MCPB provisional topology",
|
|
146
|
+
)
|
|
147
|
+
provisional_setup_commands.extend(provisional.leap_commands)
|
|
148
|
+
|
|
149
|
+
if bonded_site is not None:
|
|
150
|
+
site_dir = output / _safe_dir_name(bonded_site.config.id)
|
|
151
|
+
hydrogenation_result = hydrogenate_for_mcpb(
|
|
152
|
+
structure,
|
|
153
|
+
manifest,
|
|
154
|
+
ligand_result=ligand_result,
|
|
155
|
+
protonation_result=protonation_result,
|
|
156
|
+
metal_setup_commands=_deduplicate_commands(provisional_setup_commands),
|
|
157
|
+
output_dir=site_dir / "hydrogenation",
|
|
158
|
+
)
|
|
159
|
+
hydrated_sites = resolve_metal_sites(
|
|
160
|
+
hydrogenation_result.structure,
|
|
161
|
+
manifest,
|
|
162
|
+
validate_mcpb_cutoff=True,
|
|
163
|
+
)
|
|
164
|
+
hydrated_bonded = next(
|
|
165
|
+
(site for site in hydrated_sites if site.config.id == bonded_site.config.id),
|
|
166
|
+
None,
|
|
167
|
+
)
|
|
168
|
+
if hydrated_bonded is None:
|
|
169
|
+
raise MetalWorkflowError(
|
|
170
|
+
f"Bonded site {bonded_site.config.id!r} could not be resolved after hydrogenation."
|
|
171
|
+
)
|
|
172
|
+
mcpb_result = run_mcpb_site(
|
|
173
|
+
hydrated_bonded,
|
|
174
|
+
structure=hydrogenation_result.structure,
|
|
175
|
+
manifest=manifest,
|
|
176
|
+
ligand_result=ligand_result,
|
|
177
|
+
output_dir=site_dir / "mcpb",
|
|
178
|
+
provisional_prmtop=hydrogenation_result.prmtop_path,
|
|
179
|
+
provisional_inpcrd=hydrogenation_result.inpcrd_path,
|
|
180
|
+
refinement_result=refinement_result,
|
|
181
|
+
)
|
|
182
|
+
if mcpb_result.complete:
|
|
183
|
+
assert mcpb_result.restored_final_pdb_path is not None
|
|
184
|
+
final_structure = read_pdb(mcpb_result.restored_final_pdb_path)
|
|
185
|
+
setup_commands.extend(mcpb_result.leap_setup_commands)
|
|
186
|
+
bond_commands.extend(mcpb_result.leap_bond_commands)
|
|
187
|
+
if (
|
|
188
|
+
mcpb_result.pyscf_result is not None
|
|
189
|
+
and mcpb_result.pyscf_result.small.scientific_warning is not None
|
|
190
|
+
):
|
|
191
|
+
warnings.append(mcpb_result.pyscf_result.small.scientific_warning)
|
|
192
|
+
|
|
193
|
+
complete = mcpb_result is None or mcpb_result.complete
|
|
194
|
+
return MetalStageResult(
|
|
195
|
+
sites=sites,
|
|
196
|
+
nonbonded_sites=nonbonded_results,
|
|
197
|
+
mcpb_site=mcpb_result,
|
|
198
|
+
pre_mcpb_hydrogenation=hydrogenation_result,
|
|
199
|
+
structure=final_structure,
|
|
200
|
+
complete=complete,
|
|
201
|
+
leap_setup_commands=_deduplicate_commands(setup_commands),
|
|
202
|
+
leap_bond_commands=_deduplicate_commands(bond_commands),
|
|
203
|
+
c4_atom_types=sorted(set(c4_types)),
|
|
204
|
+
warnings=warnings,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def promote_mcpb_resp_ligands(
|
|
209
|
+
ligand_result: LigandStageResult,
|
|
210
|
+
metal_result: MetalStageResult,
|
|
211
|
+
) -> LigandStageResult:
|
|
212
|
+
"""Replace provisional cofactor paths with MCPB's joint-RESP artifacts."""
|
|
213
|
+
|
|
214
|
+
site = metal_result.mcpb_site
|
|
215
|
+
if site is None or not site.complete:
|
|
216
|
+
return ligand_result
|
|
217
|
+
artifacts = {item.ligand_id: item for item in site.refitted_ligands}
|
|
218
|
+
promoted = []
|
|
219
|
+
for item in ligand_result.ligands:
|
|
220
|
+
artifact = artifacts.get(item.ligand_id)
|
|
221
|
+
if artifact is None:
|
|
222
|
+
promoted.append(item)
|
|
223
|
+
continue
|
|
224
|
+
if item.charge_method != "mcpb_resp_pyscf":
|
|
225
|
+
raise MetalWorkflowError(
|
|
226
|
+
f"MCPB returned a refitted mol2 for ligand {item.ligand_id!r}, "
|
|
227
|
+
f"but its configured charge method is {item.charge_method!r}."
|
|
228
|
+
)
|
|
229
|
+
if site.final_frcmod_path is None:
|
|
230
|
+
raise MetalWorkflowError(
|
|
231
|
+
f"MCPB RESP ligand {item.ligand_id!r} has no final shared frcmod."
|
|
232
|
+
)
|
|
233
|
+
promoted.append(
|
|
234
|
+
replace(
|
|
235
|
+
item,
|
|
236
|
+
final_mol2_path=artifact.final_mol2_path,
|
|
237
|
+
final_frcmod_path=site.final_frcmod_path,
|
|
238
|
+
validation=None,
|
|
239
|
+
warnings=[
|
|
240
|
+
*item.warnings,
|
|
241
|
+
"Final mol2 charges come from the joint embedded PySCF/MCPB RESP "
|
|
242
|
+
"fit; the recorded AM1-BCC mol2 remains provisional only.",
|
|
243
|
+
],
|
|
244
|
+
status="ok",
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
expected = {
|
|
248
|
+
item.ligand_id
|
|
249
|
+
for item in ligand_result.ligands
|
|
250
|
+
if item.charge_method == "mcpb_resp_pyscf"
|
|
251
|
+
}
|
|
252
|
+
if set(artifacts) != expected:
|
|
253
|
+
raise MetalWorkflowError(
|
|
254
|
+
"MCPB RESP ligand promotion is incomplete: expected "
|
|
255
|
+
f"{sorted(expected)}, obtained {sorted(artifacts)}."
|
|
256
|
+
)
|
|
257
|
+
return replace(ligand_result, ligands=promoted)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _safe_dir_name(value: str) -> str:
|
|
261
|
+
safe = "".join(character if character.isalnum() or character in "-_" else "_" for character in value)
|
|
262
|
+
return safe or "metal_site"
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _deduplicate_commands(commands: list[str]) -> list[str]:
|
|
266
|
+
"""Remove identical commands while preserving multiline block members."""
|
|
267
|
+
|
|
268
|
+
result: list[str] = []
|
|
269
|
+
seen: set[str] = set()
|
|
270
|
+
seen_blocks: set[tuple[str, ...]] = set()
|
|
271
|
+
index = 0
|
|
272
|
+
while index < len(commands):
|
|
273
|
+
command = commands[index]
|
|
274
|
+
if command == "addAtomTypes {":
|
|
275
|
+
end = index + 1
|
|
276
|
+
while end < len(commands) and commands[end] != "}":
|
|
277
|
+
end += 1
|
|
278
|
+
if end >= len(commands):
|
|
279
|
+
raise MetalWorkflowError("Unterminated addAtomTypes block in generated tleap setup.")
|
|
280
|
+
block = tuple(commands[index : end + 1])
|
|
281
|
+
if block not in seen_blocks:
|
|
282
|
+
seen_blocks.add(block)
|
|
283
|
+
result.extend(block)
|
|
284
|
+
index = end + 1
|
|
285
|
+
continue
|
|
286
|
+
if command not in seen:
|
|
287
|
+
seen.add(command)
|
|
288
|
+
result.append(command)
|
|
289
|
+
index += 1
|
|
290
|
+
return result
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _register_ion_artifact(
|
|
294
|
+
artifact: NonbondedIonArtifact,
|
|
295
|
+
*,
|
|
296
|
+
parameter_by_type: dict[str, tuple[str, str]],
|
|
297
|
+
template_by_resname: dict[str, tuple[str, int, str]] | None,
|
|
298
|
+
context: str,
|
|
299
|
+
) -> None:
|
|
300
|
+
parameter = artifact.parameter
|
|
301
|
+
ion = artifact.ion
|
|
302
|
+
signature = (parameter.parameter_set, parameter.frcmod_name)
|
|
303
|
+
previous = parameter_by_type.get(parameter.atom_type)
|
|
304
|
+
if previous is not None and previous != signature:
|
|
305
|
+
raise MetalWorkflowError(
|
|
306
|
+
f"Amber atom type {parameter.atom_type} is requested with conflicting ion models "
|
|
307
|
+
f"{previous} and {signature} in the {context}. A single topology cannot contain both."
|
|
308
|
+
)
|
|
309
|
+
parameter_by_type[parameter.atom_type] = signature
|
|
310
|
+
if template_by_resname is None:
|
|
311
|
+
return
|
|
312
|
+
residue_name = ion.residue.id.resname
|
|
313
|
+
template = (ion.element, ion.charge, ion.atom.name)
|
|
314
|
+
prior_template = template_by_resname.get(residue_name)
|
|
315
|
+
if prior_template is not None and prior_template != template:
|
|
316
|
+
raise MetalWorkflowError(
|
|
317
|
+
f"Metal residue name {residue_name!r} maps to conflicting templates "
|
|
318
|
+
f"{prior_template} and {template} in the {context}. Use distinct PDB residue names."
|
|
319
|
+
)
|
|
320
|
+
template_by_resname[residue_name] = template
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Manual protonation and residue-state assignment helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from mdprep.protonation.apply import ProtonationApplicationError, ProtonationResult, apply_protonation_stage
|
|
6
|
+
from mdprep.protonation.states import ProtonationStateError, validate_state_transition
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"ProtonationApplicationError",
|
|
10
|
+
"ProtonationResult",
|
|
11
|
+
"ProtonationStateError",
|
|
12
|
+
"apply_protonation_stage",
|
|
13
|
+
"validate_state_transition",
|
|
14
|
+
]
|
|
15
|
+
|