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/leap/ions.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Ion and salt command helpers for tleap scripts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from math import cos, radians, sqrt
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class IonPlanError(ValueError):
|
|
10
|
+
"""Raised when ion placement cannot be planned safely."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class IonPlan:
|
|
15
|
+
neutralizing_ion: str | None
|
|
16
|
+
neutralizing_count: int
|
|
17
|
+
salt_pairs: int
|
|
18
|
+
commands: list[str]
|
|
19
|
+
|
|
20
|
+
def to_dict(self) -> dict[str, object]:
|
|
21
|
+
return {
|
|
22
|
+
"neutralizing_ion": self.neutralizing_ion,
|
|
23
|
+
"neutralizing_count": self.neutralizing_count,
|
|
24
|
+
"salt_pairs": self.salt_pairs,
|
|
25
|
+
"commands": self.commands,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def neutralizing_ion_count(
|
|
30
|
+
total_charge: float,
|
|
31
|
+
*,
|
|
32
|
+
positive_ion: str,
|
|
33
|
+
negative_ion: str,
|
|
34
|
+
tolerance: float = 0.05,
|
|
35
|
+
) -> tuple[str | None, int]:
|
|
36
|
+
rounded = round(total_charge)
|
|
37
|
+
if abs(total_charge - rounded) > tolerance:
|
|
38
|
+
raise IonPlanError(
|
|
39
|
+
f"System charge {total_charge:.6f} is not within {tolerance:.2f} e of an integer."
|
|
40
|
+
)
|
|
41
|
+
if rounded < 0:
|
|
42
|
+
return positive_ion, abs(rounded)
|
|
43
|
+
if rounded > 0:
|
|
44
|
+
return negative_ion, rounded
|
|
45
|
+
return None, 0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def salt_pair_count(molarity: float, volume_a3: float) -> int:
|
|
49
|
+
if molarity < 0:
|
|
50
|
+
raise IonPlanError("Salt concentration cannot be negative.")
|
|
51
|
+
if volume_a3 <= 0:
|
|
52
|
+
raise IonPlanError("Periodic box volume must be positive to add salt pairs.")
|
|
53
|
+
return round(molarity * volume_a3 * 0.000602214076)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def build_ion_plan(
|
|
57
|
+
*,
|
|
58
|
+
total_charge: float,
|
|
59
|
+
neutralize: bool,
|
|
60
|
+
positive_ion: str,
|
|
61
|
+
negative_ion: str,
|
|
62
|
+
salt_concentration_molar: float,
|
|
63
|
+
volume_a3: float | None,
|
|
64
|
+
) -> IonPlan:
|
|
65
|
+
commands: list[str] = []
|
|
66
|
+
neutralizing_ion: str | None = None
|
|
67
|
+
neutralizing_count = 0
|
|
68
|
+
if neutralize:
|
|
69
|
+
neutralizing_ion, neutralizing_count = neutralizing_ion_count(
|
|
70
|
+
total_charge,
|
|
71
|
+
positive_ion=positive_ion,
|
|
72
|
+
negative_ion=negative_ion,
|
|
73
|
+
)
|
|
74
|
+
if neutralizing_ion is not None and neutralizing_count > 0:
|
|
75
|
+
commands.append(f"addionsrand system {neutralizing_ion} {neutralizing_count}")
|
|
76
|
+
|
|
77
|
+
salt_pairs = 0
|
|
78
|
+
if salt_concentration_molar > 0:
|
|
79
|
+
if volume_a3 is None:
|
|
80
|
+
raise IonPlanError("Cannot add salt pairs because solvated box volume was not determined.")
|
|
81
|
+
salt_pairs = salt_pair_count(salt_concentration_molar, volume_a3)
|
|
82
|
+
if salt_pairs > 0:
|
|
83
|
+
commands.append(f"addionsrand system {positive_ion} {salt_pairs}")
|
|
84
|
+
commands.append(f"addionsrand system {negative_ion} {salt_pairs}")
|
|
85
|
+
return IonPlan(
|
|
86
|
+
neutralizing_ion=neutralizing_ion,
|
|
87
|
+
neutralizing_count=neutralizing_count,
|
|
88
|
+
salt_pairs=salt_pairs,
|
|
89
|
+
commands=commands,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def amber_inpcrd_box_volume(path: str) -> float | None:
|
|
94
|
+
"""Return periodic box volume from an Amber restart/inpcrd file, if present."""
|
|
95
|
+
|
|
96
|
+
lines = [line.strip() for line in open(path, encoding="utf-8").read().splitlines() if line.strip()]
|
|
97
|
+
if len(lines) < 3:
|
|
98
|
+
return None
|
|
99
|
+
fields = lines[-1].split()
|
|
100
|
+
if len(fields) < 6:
|
|
101
|
+
return None
|
|
102
|
+
try:
|
|
103
|
+
a, b, c, alpha, beta, gamma = (float(value) for value in fields[:6])
|
|
104
|
+
except ValueError:
|
|
105
|
+
return None
|
|
106
|
+
cos_a = cos(radians(alpha))
|
|
107
|
+
cos_b = cos(radians(beta))
|
|
108
|
+
cos_g = cos(radians(gamma))
|
|
109
|
+
factor = 1.0 + 2.0 * cos_a * cos_b * cos_g - cos_a * cos_a - cos_b * cos_b - cos_g * cos_g
|
|
110
|
+
if factor <= 0:
|
|
111
|
+
return None
|
|
112
|
+
return a * b * c * sqrt(factor)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Parse tleap logs into validation-friendly summaries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LeapLogError(ValueError):
|
|
11
|
+
"""Raised when a tleap log indicates an unsafe build."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class LeapLogSummary:
|
|
16
|
+
returncode: int
|
|
17
|
+
warnings: list[str] = field(default_factory=list)
|
|
18
|
+
errors: list[str] = field(default_factory=list)
|
|
19
|
+
unknown_residues: list[str] = field(default_factory=list)
|
|
20
|
+
missing_atom_types: list[str] = field(default_factory=list)
|
|
21
|
+
missing_parameters: list[str] = field(default_factory=list)
|
|
22
|
+
atoms_created: list[str] = field(default_factory=list)
|
|
23
|
+
total_charge: float | None = None
|
|
24
|
+
check_output: list[str] = field(default_factory=list)
|
|
25
|
+
log_tail: list[str] = field(default_factory=list)
|
|
26
|
+
|
|
27
|
+
def to_dict(self) -> dict[str, object]:
|
|
28
|
+
return {
|
|
29
|
+
"returncode": self.returncode,
|
|
30
|
+
"warnings": self.warnings,
|
|
31
|
+
"errors": self.errors,
|
|
32
|
+
"unknown_residues": self.unknown_residues,
|
|
33
|
+
"missing_atom_types": self.missing_atom_types,
|
|
34
|
+
"missing_parameters": self.missing_parameters,
|
|
35
|
+
"atoms_created": self.atoms_created,
|
|
36
|
+
"total_charge": self.total_charge,
|
|
37
|
+
"check_output": self.check_output,
|
|
38
|
+
"log_tail": self.log_tail,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_CHARGE_PATTERNS = [
|
|
43
|
+
re.compile(r"Total unperturbed charge:\s*([-+]?\d+(?:\.\d+)?)", re.IGNORECASE),
|
|
44
|
+
re.compile(r"unperturbed charge of the unit:\s*([-+]?\d+(?:\.\d+)?)", re.IGNORECASE),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def parse_tleap_log(path: str | Path, *, returncode: int = 0) -> LeapLogSummary:
|
|
49
|
+
log_path = Path(path)
|
|
50
|
+
text = log_path.read_text(encoding="utf-8", errors="replace") if log_path.exists() else ""
|
|
51
|
+
return parse_tleap_log_text(text, returncode=returncode)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_tleap_log_text(text: str, *, returncode: int = 0) -> LeapLogSummary:
|
|
55
|
+
warnings: list[str] = []
|
|
56
|
+
errors: list[str] = []
|
|
57
|
+
unknown_residues: list[str] = []
|
|
58
|
+
missing_atom_types: list[str] = []
|
|
59
|
+
missing_parameters: list[str] = []
|
|
60
|
+
atoms_created: list[str] = []
|
|
61
|
+
check_output: list[str] = []
|
|
62
|
+
total_charge: float | None = None
|
|
63
|
+
|
|
64
|
+
for raw_line in text.splitlines():
|
|
65
|
+
line = raw_line.strip()
|
|
66
|
+
lower = line.lower()
|
|
67
|
+
if not line:
|
|
68
|
+
continue
|
|
69
|
+
if "warning:" in lower:
|
|
70
|
+
warnings.append(line)
|
|
71
|
+
if "fatal error" in lower or lower.startswith("error:") or " error!" in lower:
|
|
72
|
+
errors.append(line)
|
|
73
|
+
if "unknown residue" in lower:
|
|
74
|
+
unknown_residues.append(line)
|
|
75
|
+
errors.append(line)
|
|
76
|
+
if "does not have a type" in lower or "could not find type" in lower:
|
|
77
|
+
missing_atom_types.append(line)
|
|
78
|
+
errors.append(line)
|
|
79
|
+
if (
|
|
80
|
+
"could not find bond parameter" in lower
|
|
81
|
+
or "could not find angle parameter" in lower
|
|
82
|
+
or "could not find torsion parameter" in lower
|
|
83
|
+
or "missing parameters" in lower
|
|
84
|
+
):
|
|
85
|
+
missing_parameters.append(line)
|
|
86
|
+
errors.append(line)
|
|
87
|
+
if "created a new atom named" in lower:
|
|
88
|
+
atoms_created.append(line)
|
|
89
|
+
if lower.startswith("checking") or "unit is ok" in lower:
|
|
90
|
+
check_output.append(line)
|
|
91
|
+
for pattern in _CHARGE_PATTERNS:
|
|
92
|
+
match = pattern.search(line)
|
|
93
|
+
if match:
|
|
94
|
+
total_charge = float(match.group(1))
|
|
95
|
+
|
|
96
|
+
return LeapLogSummary(
|
|
97
|
+
returncode=returncode,
|
|
98
|
+
warnings=warnings,
|
|
99
|
+
errors=_dedupe(errors),
|
|
100
|
+
unknown_residues=_dedupe(unknown_residues),
|
|
101
|
+
missing_atom_types=_dedupe(missing_atom_types),
|
|
102
|
+
missing_parameters=_dedupe(missing_parameters),
|
|
103
|
+
atoms_created=atoms_created,
|
|
104
|
+
total_charge=total_charge,
|
|
105
|
+
check_output=check_output,
|
|
106
|
+
log_tail=_tail_lines(text),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def assert_tleap_success(
|
|
111
|
+
summary: LeapLogSummary,
|
|
112
|
+
*,
|
|
113
|
+
fail_on_warnings: bool,
|
|
114
|
+
context: str,
|
|
115
|
+
) -> None:
|
|
116
|
+
if summary.returncode != 0:
|
|
117
|
+
raise LeapLogError(
|
|
118
|
+
"\n".join(
|
|
119
|
+
[
|
|
120
|
+
f"{context} tleap exited with code {summary.returncode}.",
|
|
121
|
+
*_summary_details(summary),
|
|
122
|
+
]
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
if summary.unknown_residues:
|
|
126
|
+
raise LeapLogError(f"{context} tleap reported unknown residues: {summary.unknown_residues}")
|
|
127
|
+
if summary.missing_atom_types:
|
|
128
|
+
raise LeapLogError(f"{context} tleap reported missing atom types: {summary.missing_atom_types}")
|
|
129
|
+
if summary.missing_parameters:
|
|
130
|
+
raise LeapLogError(f"{context} tleap reported missing parameters: {summary.missing_parameters}")
|
|
131
|
+
if summary.errors:
|
|
132
|
+
raise LeapLogError(f"{context} tleap reported errors: {summary.errors}")
|
|
133
|
+
if fail_on_warnings and summary.warnings:
|
|
134
|
+
raise LeapLogError(f"{context} tleap warnings are fatal by validation config: {summary.warnings}")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _dedupe(values: list[str]) -> list[str]:
|
|
138
|
+
seen: set[str] = set()
|
|
139
|
+
deduped: list[str] = []
|
|
140
|
+
for value in values:
|
|
141
|
+
if value in seen:
|
|
142
|
+
continue
|
|
143
|
+
seen.add(value)
|
|
144
|
+
deduped.append(value)
|
|
145
|
+
return deduped
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _summary_details(summary: LeapLogSummary) -> list[str]:
|
|
149
|
+
details: list[str] = []
|
|
150
|
+
if summary.errors:
|
|
151
|
+
details.append(f"errors: {summary.errors}")
|
|
152
|
+
if summary.unknown_residues:
|
|
153
|
+
details.append(f"unknown residues: {summary.unknown_residues}")
|
|
154
|
+
if summary.missing_atom_types:
|
|
155
|
+
details.append(f"missing atom types: {summary.missing_atom_types}")
|
|
156
|
+
if summary.missing_parameters:
|
|
157
|
+
details.append(f"missing parameters: {summary.missing_parameters}")
|
|
158
|
+
if summary.warnings:
|
|
159
|
+
details.append(f"warnings: {summary.warnings}")
|
|
160
|
+
if summary.log_tail:
|
|
161
|
+
details.append("tleap log tail:")
|
|
162
|
+
details.extend(summary.log_tail)
|
|
163
|
+
return details
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _tail_lines(text: str, *, lines: int = 30) -> list[str]:
|
|
167
|
+
stripped = text.strip()
|
|
168
|
+
if not stripped:
|
|
169
|
+
return []
|
|
170
|
+
return stripped.splitlines()[-lines:]
|
mdprep/leap/report.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""tleap 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.leap.builder import TLeapStageResult
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def write_tleap_reports(
|
|
13
|
+
result: TLeapStageResult,
|
|
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
|
+
json_output.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
23
|
+
markdown_output.write_text(_render_markdown(report), encoding="utf-8")
|
|
24
|
+
return report
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _render_markdown(report: dict[str, Any]) -> str:
|
|
28
|
+
lines = [
|
|
29
|
+
"# tleap Report",
|
|
30
|
+
"",
|
|
31
|
+
"## Force Fields",
|
|
32
|
+
"",
|
|
33
|
+
]
|
|
34
|
+
lines.extend(f"- `{source}`" for source in report["force_fields_sourced"])
|
|
35
|
+
lines.extend(
|
|
36
|
+
[
|
|
37
|
+
"",
|
|
38
|
+
"## Ligands",
|
|
39
|
+
"",
|
|
40
|
+
]
|
|
41
|
+
)
|
|
42
|
+
ligands = report.get("ligands", [])
|
|
43
|
+
if ligands:
|
|
44
|
+
for ligand in ligands:
|
|
45
|
+
lines.append(
|
|
46
|
+
f"- `{ligand['ligand_id']}`: mol2 `{ligand['final_mol2_path']}`, "
|
|
47
|
+
f"frcmod `{ligand['final_frcmod_path']}`"
|
|
48
|
+
)
|
|
49
|
+
else:
|
|
50
|
+
lines.append("- None")
|
|
51
|
+
lines.extend(["", "### MCPB joint-RESP ligands", ""])
|
|
52
|
+
mcpb_resp_ligands = report.get("mcpb_resp_ligands", [])
|
|
53
|
+
if mcpb_resp_ligands:
|
|
54
|
+
for ligand in mcpb_resp_ligands:
|
|
55
|
+
lines.append(
|
|
56
|
+
f"- `{ligand['ligand_id']}`: final template "
|
|
57
|
+
f"`{ligand['final_mol2_path']}` (provisional mol2 excluded from "
|
|
58
|
+
"the final tLEAP ligand list)"
|
|
59
|
+
)
|
|
60
|
+
else:
|
|
61
|
+
lines.append("- None")
|
|
62
|
+
lines.extend(["", "## Disulfide Bonds", ""])
|
|
63
|
+
bonds = report.get("disulfide_bond_commands", [])
|
|
64
|
+
if bonds:
|
|
65
|
+
lines.extend(f"- `{bond['command']}`" for bond in bonds)
|
|
66
|
+
else:
|
|
67
|
+
lines.append("- None")
|
|
68
|
+
lines.extend(["", "## 12-6-4 Post-processing", ""])
|
|
69
|
+
c4_runs = report.get("c4_postprocessing", [])
|
|
70
|
+
if c4_runs:
|
|
71
|
+
for run in c4_runs:
|
|
72
|
+
lines.append(
|
|
73
|
+
f"- `{run['output_prmtop']}`: atom types "
|
|
74
|
+
+ ", ".join(f"`{atom_type}`" for atom_type in run["atom_types"])
|
|
75
|
+
)
|
|
76
|
+
else:
|
|
77
|
+
lines.append("- None")
|
|
78
|
+
lines.extend(
|
|
79
|
+
[
|
|
80
|
+
"",
|
|
81
|
+
"## Outputs",
|
|
82
|
+
"",
|
|
83
|
+
f"- Dry prmtop: `{report['dry_outputs']['prmtop']}`",
|
|
84
|
+
f"- Dry inpcrd: `{report['dry_outputs']['inpcrd']}`",
|
|
85
|
+
f"- Final prmtop: `{report['final_outputs']['prmtop']}`",
|
|
86
|
+
f"- Final inpcrd: `{report['final_outputs']['inpcrd']}`",
|
|
87
|
+
f"- Final PDB: `{report['final_outputs']['pdb']}`",
|
|
88
|
+
"",
|
|
89
|
+
"## Warnings",
|
|
90
|
+
"",
|
|
91
|
+
]
|
|
92
|
+
)
|
|
93
|
+
warnings = report.get("warnings") or []
|
|
94
|
+
if warnings:
|
|
95
|
+
lines.extend(f"- {warning}" for warning in warnings)
|
|
96
|
+
else:
|
|
97
|
+
lines.append("- None")
|
|
98
|
+
lines.append("")
|
|
99
|
+
return "\n".join(lines)
|