"""Helpers for constructing AMBER ``mdin`` control files."""
from __future__ import annotations
from pathlib import Path
from typing import Dict, List, Optional
[docs]
class AmberMdin:
"""Mutable representation of an AMBER mdin file.
Parameters
----------
cut : float, optional
Non-bonded cutoff in Å (default: 9.0).
ioutfm : int, optional
Output format flag (1 → NetCDF).
ntb : int, optional
Periodic boundary condition flag.
ntxo : int, optional
Restart write format.
"""
def __init__(self, *, cut: float = 9.0, ioutfm: int = 1, ntb: int = 1, ntxo: int = 2):
self.blocks: List[Dict[str, object]] = []
self.apply_defaults(cut=cut, ioutfm=ioutfm, ntb=ntb, ntxo=ntxo)
# ----------------------- mutation helpers -----------------------
[docs]
def add_block(self, name: str, params: Optional[Dict[str, object]] = None) -> None:
"""Append a named control block."""
cleaned = name.strip("&")
self.blocks.append({"type": "block", "name": cleaned, "params": params or {}})
[docs]
def add_raw(self, line: str) -> None:
"""Append a raw line verbatim to the output."""
self.blocks.append({"type": "raw", "line": line.strip()})
[docs]
def update_param(self, block_name: str, key: str, value: object) -> None:
"""Update a single parameter within ``block_name``."""
for block in self.blocks:
if block["type"] == "block" and block["name"] == block_name:
block["params"][key] = value
return
raise KeyError(f"Block '{block_name}' not found")
[docs]
def override_block(self, block_name: str, param_dict: Dict[str, object]) -> None:
"""Merge ``param_dict`` into an existing block or create the block."""
for block in self.blocks:
if block["type"] == "block" and block["name"] == block_name:
block["params"].update(param_dict)
return
self.add_block(block_name, param_dict)
# ----------------------- serialisation -----------------------
[docs]
def to_string(self) -> str:
"""Render the mdin contents as text."""
lines: List[str] = []
for block in self.blocks:
if block["type"] == "block":
lines.append(f" &{block['name']}")
for key, value in block["params"].items():
lines.append(f" {key} = {value},")
lines.append(" /")
elif block["type"] == "raw":
lines.append(block["line"])
return "\n".join(lines)
[docs]
def save(self, filename: str | Path) -> None:
"""Write the mdin file to ``filename``."""
path = Path(filename)
path.write_text("# Generated by AmberMdin\n" + self.to_string())
# ----------------------- templates -----------------------
[docs]
def apply_defaults(self, *, cut: float = 9.0, ioutfm: int = 1, ntb: int = 1, ntxo: int = 2) -> None:
"""Initialise with a baseline ``cntrl`` block."""
self.add_block(
"cntrl",
{
"imin": 0,
"irest": 0,
"ntx": 1,
"ntxo": ntxo,
"ntf": 2,
"ntc": 2,
"cut": cut,
"ntpr": 5000,
"ntwr": 5000,
"ntwx": 5000,
"iwrap": 1,
"ioutfm": ioutfm,
"ntb": ntb,
},
)
# ----------------------- convenience builders -----------------------
[docs]
def apply_minimization(mdin: AmberMdin, *, steps: int = 5000) -> None:
"""Enable energy minimisation for ``steps`` iterations."""
mdin.override_block(
"cntrl",
{
"imin": 1,
"maxcyc": steps,
"ncyc": steps // 2,
},
)
[docs]
def apply_npt(mdin: AmberMdin, *, temp: float = 298.15, steps: int = 50000, barostat: int = 2, dt: float = 0.004) -> None:
"""Configure standard NPT dynamics."""
mdin.override_block(
"cntrl",
{
"ntp": 1,
"barostat": barostat,
"ntt": 3,
"temp0": temp,
"gamma_ln": 1.0,
"nstlim": steps,
"dt": dt,
},
)
[docs]
def apply_membrane_npt(
mdin: AmberMdin,
*,
temp: float = 298.15,
steps: int = 50000,
barostat: int = 2,
dt: float = 0.004,
) -> None:
"""Configure semi-isotropic NPT suitable for membranes."""
mdin.override_block(
"cntrl",
{
"ntp": 3,
"barostat": barostat,
"csurften": 3,
"ntt": 3,
"temp0": temp,
"gamma_ln": 1.0,
"nstlim": steps,
"dt": dt,
},
)
[docs]
def apply_ti(
mdin: AmberMdin,
*,
lbd_val: float,
timask1: str,
timask2: str,
scmask1: str,
scmask2: str,
crgmask: str,
) -> None:
"""Configure thermodynamic integration (TI) parameters."""
mdin.override_block(
"cntrl",
{
"icfe": 1,
"clambda": lbd_val,
"timask1": f"'{timask1}'",
"timask2": f"'{timask2}'",
"scmask1": f"'{scmask1}'",
"scmask2": f"'{scmask2}'",
"crgmask": f"'{crgmask}'",
"ifsc": 1,
"ifmbar": 1,
},
)
[docs]
def apply_restraints(mdin: AmberMdin, *, mask: str, weight: float = 50.0) -> None:
"""Add positional restraints."""
mdin.override_block(
"cntrl",
{
"ntr": 1,
"restraintmask": f"'{mask}'",
"restraint_wt": weight,
},
)
[docs]
def apply_wt_end(mdin: AmberMdin) -> None:
"""Append the ``&wt type='END'`` control line."""
mdin.add_raw("&wt type='END', /")
[docs]
def apply_disang(mdin: AmberMdin, *, filename: str = "disang.rest") -> None:
"""Reference a DISANG restraint file."""
mdin.add_raw(f"DISANG={filename}")
mdin.add_raw("LISTOUT=POUT")