"""oxDNA input file parser."""
from __future__ import annotations
import typing
from dataclasses import dataclass
from pathlib import Path
import jax.numpy as jnp
from mythos.input import topology as _topology
from mythos.utils.units import get_kt_from_string
if typing.TYPE_CHECKING:
import io
INVALID_DICT_LINE = "Invalid dictionary line: {}"
[docs]
def _parse_numeric(value: str) -> tuple[float | int, bool]:
is_successful = False
parsed = 0
for t in (int, float):
try:
parsed = t(value)
except ValueError:
continue
else:
is_successful = True
break
return parsed, is_successful
[docs]
def _parse_boolean(value: str) -> tuple[bool, bool]:
lowered = value.lower()
return (
lowered == "true",
lowered in ("true", "false"),
)
[docs]
def _parse_value(value: str) -> str | float | int | bool:
# remove potential comment from end of line
value = value.split("#", maxsplit=1)[0].strip()
parsed, is_numeric = _parse_numeric(value)
if not is_numeric:
parsed, is_boolean = _parse_boolean(value)
if not is_boolean:
parsed = value
return parsed
[docs]
def _parse_dict(
in_line: str, lines: typing.Iterable[str]
) -> tuple[dict[str, str | float | int | bool], typing.Iterable[str]]:
if "=" not in in_line and "{" not in in_line:
raise ValueError(INVALID_DICT_LINE.format(in_line))
var_name = in_line.split("=", maxsplit=1)[0].strip()
parsed = {}
for line in lines:
if "{" not in line and "}" not in line:
key, value = (v.strip() for v in line.split("="))
parsed[key] = _parse_value(value)
elif "{" in line:
(key, value), lines = _parse_dict(line, lines)
parsed[key] = value
elif "}":
break
return (var_name, parsed), lines
[docs]
def read(input_file: Path) -> dict[str, str | float | int | bool]:
"""Read an oxDNA input file."""
with input_file.open("r") as f:
lines = filter(lambda line: (len(line.strip()) > 0) and (not line.strip().startswith("#")), f.readlines())
parsed = {}
for line in lines:
if "{" in line:
(key, value), lines = _parse_dict(line, lines)
else:
key, str_value = (v.strip() for v in line.split("="))
value = _parse_value(str_value)
parsed[key] = value
return parsed
[docs]
def write_to(input_config: dict, f: io.TextIOWrapper) -> None:
"""Write an oxDNA input file."""
for key, value in input_config.items():
if isinstance(value, dict):
f.write(f"{key} = {{\n")
write_to(value, f)
f.write("}\n")
else:
if key == "T" and isinstance(value, float):
parsed_value = str(value) + "K"
elif isinstance(value, bool):
parsed_value = str(value).lower()
else:
parsed_value = str(value)
f.write(f"{key} = {parsed_value}\n")
[docs]
def read_box_size(conf_file: Path) -> jnp.ndarray:
"""Read the box size from an oxDNA configuration file.
Parses the ``b = ...`` line from the configuration file header.
Args:
conf_file: Path to the oxDNA configuration (``.conf`` / ``.dat``) file.
Returns:
A JAX array of shape ``(3,)`` with the box dimensions.
Raises:
ValueError: If no ``b = ...`` line is found in the file.
"""
with conf_file.open("r") as f:
for line in f:
if line.startswith("b ="):
return jnp.array([float(v) for v in line.split("=")[1].strip().split()])
raise ValueError(f"No 'b = ...' line found in {conf_file}")
[docs]
def write(input_config: dict, input_file: Path) -> None:
"""Write an oxDNA input file."""
with input_file.open("w") as f:
write_to(input_config, f)