Skip to content

GROMACS engine support to a3fe - #76

Open
Roy-Haolin-Du wants to merge 12 commits into
michellab:feature-gromacs-supportfrom
Roy-Haolin-Du:feature-gromacs-support
Open

Roy-Haolin-Du wants to merge 12 commits into
michellab:feature-gromacs-supportfrom
Roy-Haolin-Du:feature-gromacs-support

Conversation

@Roy-Haolin-Du

@Roy-Haolin-Du Roy-Haolin-Du commented Dec 8, 2025

Copy link
Copy Markdown
Member

Description

Adds GROMACS engine support to a3fe, enabling non-adaptive ABFE calculations using GROMACS alongside the existing SOMD support. SOMD remains the default engine and continues to provide the adaptive workflow.

Key Changes

  • Configuration: Adds GromacsConfig for MDP parameter management.
  • Simulation: Adds the GROMACS EM-to-production workflow and SLURM job submission.
  • Charged ligands: Alchemically transforms a monovalent counterion together with the ligand to keep the system charge neutral.
  • Analysis: Adds MBAR and convergence analysis for GROMACS XVG output, with BAR initialization for robust MBAR convergence.
  • Testing: Adds real GROMACS regression data and a minimal independent SLURM integration test.
  • Documentation: Documents engine selection, current limitations, charged-ligand handling, and API entries.

Validation

  • Completed real neutral and charged-ligand non-adaptive GROMACS calculations, including setup, execution, analysis, and convergence analysis.
  • Full non-integration test suite passed: 88 passed, 10 skipped.
  • The minimal GROMACS SLURM integration test passed using four 20 ps jobs, covering EM, production, XVG reading, MBAR, and stage analysis.
  • Repository pre-commit checks, Ruff formatting and linting, and the Sphinx HTML build passed.

Current Scope

GROMACS supports non-adaptive production calculations. Adaptive GROMACS execution is not implemented; the existing SOMD adaptive workflow is unchanged.

@Roy-Haolin-Du

Copy link
Copy Markdown
Member Author

Hi @fjclark , this PR #76 is now ready for review.

It adds non-adaptive GROMACS ABFE support, including charged ligands, analysis, regression tests, documentation, and a minimal SLURM integration test.
As discussed with @jmichel80 , this non-adaptive GROMACS ABFE support first, enabling users who prefer GROMACS to run ABFE with a3fe. Adaptive GROMACS a3fe will be addressed separately because requiring checkpoint-based simulation extension and dynamic redistribution of sampling across lambda windows, which have not yet been implemented for the GROMACS backend.

I also validated the implementation (gmx engine) locally using four ligands (two neutral and two charged, 0.1ns);
all completed successfully through setup, execution, and analysis.
image

So, when you have a chance, would you mind taking a look?
There is no rush at all. If you are happy for me to merge it into the feature branch without a detailed review, please just let me know.
I would really appreciate your help, and many thanks, finaly!

Best wishes,
Roy

Comment on lines +261 to +278
_StageType.VANISH: [
0.0,
0.1,
0.2,
0.3,
0.4,
0.5,
0.55,
0.6,
0.65,
0.7,
0.75,
0.8,
0.85,
0.9,
0.95,
1.0,
],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More science than software question, but it would be nice to document (even if that's just adding to the description) where the lambda values came from (my fault for not doing this originally).

The most consistent way to do this would be to space the default windows with the same thermodynamic speed for the same system for GROMACs and SOMD (but feel free to leave for now if that's a pain).

ligand_charge: int = _Field(
0,
description="Net charge of the ligand. If non-zero, must use PME for electrostatics.",
description="Charge change for the alchemical transformation. If non-zero, PME must be used.",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is "Net charge of the ligand" less ambiguous? It's not defined which direction the charge change is in so e.g. could be plus or minus the ligand charge.

Comment on lines +528 to +530
pcoupl: _Literal["no", "Berendsen", "C-rescale", "Parrinello-Rahman"] = _Field(
"Parrinello-Rahman", description="Pressure coupling"
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a quick skim your defaults look good to me! If you've taken them from anywhere in particular e.g. BioSimSpace or https://github.com/bigginlab/fragment-opt-abfe-benchmark/blob/main/simulation_control_files/abfe_mdps/4fs/coul/prod/prod.mdp, would be good to note this in the docstring.

Comment thread a3fe/run/lambda_window.py
Comment on lines +357 to +366
if self.sims[0].engine_type == _EngineType.GROMACS:
# GROMACS: dt in ps, nstdhdl is steps (dH/dlambda output frequency), convert to ns
# First energy is written at time 0, so no offset needed
time_per_energy = config.dt * config.nstdhdl / 1000
equil_index = int(self._equil_time / time_per_energy)
else:
# SOMD: timestep in fs, energy_frequency is steps, convert to ns
# First energy is only written after the first nrg_freq steps, so subtract 1
time_per_energy = config.timestep * config.energy_frequency / 1_000_000
equil_index = max(0, int(self._equil_time / time_per_energy) - 1)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be neater to have this logic inside each engine config, so that Lambda window (and as far as possible, everything in run) doesn't have to know anything about the engines -- it just calls e.g. equil_index = config.get_equil_index(self._equil_time).

Comment thread a3fe/run/lambda_window.py
Comment on lines +371 to +378
if sim.engine_type == _EngineType.GROMACS:
in_file = sim.output_dir + "/prod/prod.xvg"
out_file = sim.output_dir + "/prod/prod_equilibrated.xvg"
header_chars = ("#", "@")
else:
in_file = sim.output_dir + "/simfile.dat"
out_file = sim.output_dir + "/simfile_equilibrated.dat"
header_chars = ("#",)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above, I think it would be neater if engine-specific paths were handled by an engine-specific class so that LamWindow can remain completely blind to what engine is used -- it would be great to avoid engine-specific if else statements in run.

Comment thread a3fe/run/lambda_window.py
lines = ifile.readlines()

# Figure out how many lines come before the data
non_data_lines = 0
for line in lines:
if line.startswith("#"):
if line.startswith(header_chars) or not line.strip():

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is engine-specific, again it would be cool if were in a class specific to an engine.

Comment thread a3fe/run/system_prep.py
@@ -452,7 +452,7 @@ def run_ensemble_equilibration(
print(
f"Running ensemble equilibration simulation with GROMACS for {cfg.ensemble_equilibration_time} ps"
)
if leg_type == _LegType.BOUND:
if leg_type == _LegType.BOUND or engine_type == _EngineType.GROMACS:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and below in this file (and in run in general), it would be really nice and clean if we could move all of the engine-specific logic out of run, as far as possible (if you think it's too much of a pain to be worth it, feel free to ignore). It feels like this decision might be best made in the engine-specific system prep config.

Comment thread a3fe/run/leg.py
Comment on lines +43 to +78
def _add_gromacs_alchemical_ions(
system: _BSS._SireWrappers._system.System, # type: ignore
ligand: _BSS._SireWrappers._molecule.Molecule, # type: ignore
ligand_charge: int,
) -> None:
"""Make existing counterions co-alchemical for a GROMACS decoupling."""
ion_charge = -1 if ligand_charge > 0 else 1
ions = [
mol
for mol in system
if mol.nAtoms() == 1 and round(mol.charge().value()) == ion_charge
]
if len(ions) < abs(ligand_charge):
raise ValueError(
f"Could not find {abs(ligand_charge)} monovalent counterion(s) "
"for the charged ligand."
)

space = system._sire_object.property("space")
ligand_centre = ligand.getAtoms()[ligand.getCOMIdx()]._sire_object.property(
"coordinates"
)
ions.sort(
key=lambda ion: space.calc_dist(
ion.getAtoms()[0]._sire_object.property("coordinates"), ligand_centre
),
reverse=True,
)

for ion in ions[: abs(ligand_charge)]:
perturbed_ion = _BSS.Align.merge(ion, ion, mapping={0: 0})
cursor = perturbed_ion._sire_object.cursor()
charge = perturbed_ion.getAtoms()[0]._sire_object.property("charge1")
cursor[0]["charge1"] = 0 * charge
perturbed_ion._sire_object = cursor.commit()
system.updateMolecule(system.getIndex(ion), _mark_alchemical_ion(perturbed_ion))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saying the same thing again, but would be neat to move all the engine-specific stuff out of run, or at least into something like run/gromacs_helpers.py

Comment thread a3fe/run/leg.py
Comment on lines +509 to +516
def _ensemble_equilibration_output_files(self) -> _List[str]:
"""Return the files expected from ensemble equilibration."""
if self.engine_type == _EngineType.GROMACS:
files = ["gromacs.gro"]
if self.leg_type == _LegType.BOUND:
files.append("gromacs.xtc")
return files
return ["somd.rst7"]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be cool to move this logic inside the EngineType classes so that all the logic in run is engine-independent. This is dependency inversion -- we want to depend on the interfaces (e.g. knowing that EngineType has a get_files function) rather than implementing engine-level logic here. This would make it easier in future to add another engine (not that we're planning that, but it keeps the code nice and separates concerns).

Comment thread a3fe/run/leg.py
Comment on lines +601 to +605
coordinate_prefix, coordinate_extension = (
("gromacs", "gro")
if self.engine_type == _EngineType.GROMACS
else ("somd", "rst7")
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would make coordinate_prefix_and_extension a property of EngineType.

Comment thread a3fe/run/leg.py
@@ -603,7 +656,10 @@ def run_ensemble_equilibration(

# Save the restraints to a text file and store within the Leg object
with open(f"{outdir}/restraint_{i + 1}.txt", "w") as f:
f.write(restraint.toString(engine="SOMD")) # type: ignore
run_engine = (
"GROMACS" if self.engine_type == _EngineType.GROMACS else "SOMD"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would again move this logic inside EngineType so you can go e.g.
run_engine = self.engine_type.run_engine and move out engine-specific logic.

Comment thread a3fe/run/leg.py
for file in _glob.glob(f"{stage_input_dir}/lambda_0.0000/*")
if not file.endswith(".cfg")
for file in _glob.glob(f"{stage_input_dir}/lambda_*/*")
if not file.endswith((".cfg", ".mdp", ".tpr"))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to collect these somewhere else, e.g. in EngineType.config_file_suffixes. Then you could combine them all into a constant in configuration do from ..configuration import CONFIG_FILE_SUFFIXES as _CONFIG_FILE_SUFFIXES, and here to `if not file.endswith(_CONFIG_FILE_SUFFIXES).

Comment thread a3fe/run/stage.py
Comment on lines +803 to +805
self.engine_config.ref_t
if self.engine_type == _EngineType.GROMACS
else 298.15

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be great if this logic was in engine-specific modules/ classes

Comment thread a3fe/run/simulation.py
Comment on lines +30 to +40
required_input_files = {
_EngineType.SOMD: [
"somd.prm7",
"somd.rst7",
"somd.pert",
],
_EngineType.GROMACS: [
"gromacs.top",
"gromacs.gro",
],
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be neater if these were a property of EngineType so you could do e.g. engine_type.required_input_files

Comment thread a3fe/run/simulation.py
Comment on lines +190 to +196
if self.engine_type == _EngineType.GROMACS:
# Move GROMACS checkpoint files
for cpt_file in _glob.glob(
f"{self.output_dir}/**/*.cpt", recursive=True
):
_subprocess.run(["mv", cpt_file, f"{self.output_dir}/failure"])
else: # SOMD

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again would be great to avoid engine-specific behaviour in run

Comment thread a3fe/run/simulation.py
Comment on lines +228 to +234
"""Select the coordinate and restraint files for this run."""
if self.engine_type == _EngineType.SOMD:
file_prefix = "somd"
file_extension = "rst7"
elif self.engine_type == _EngineType.GROMACS:
file_prefix = "gromacs"
file_extension = "gro"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be great for this to go into an engine-specific class or module

assert results_exp.loc["t4l", "calc_dg"] == pytest.approx(5.2622, abs=1e-2)
assert results_exp.loc["t4l", "calc_er"] == pytest.approx(0.1138, abs=1e-2)
assert results_exp.loc["t4l", "calc_dg"] == pytest.approx(5.2850, abs=1e-2)
assert results_exp.loc["t4l", "calc_er"] == pytest.approx(0.0808, abs=1e-2)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any idea why the regression test has broken? Was there a bug in the original?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this was caused by a bug in the original equilibration-index calculation. equil_time is in ns while timestep is in fs.
This was fixed

equil_index = (
int(
self._equil_time
* 1_000_000
/ (
self.sims[0].engine_config.timestep
* self.sims[0].engine_config.energy_frequency
)
)
- 1 # type: ignore
and the updated regression values match main.
Thanks!

Comment on lines +737 to +741
mbar_temperature = (
lambda_windows[0].engine_config.ref_t
if engine_type == _EngineType.GROMACS
else 298.15
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to move engine-specific logic into engine-specific classes or modules

Comment thread a3fe/analyse/mbar.py
Comment on lines +151 to +243
def _run_mbar_gromacs(
output_dir: str,
run_nos: _List[int],
percentage_end: float = 100,
percentage_start: float = 0,
subsampling: bool = False,
delete_outfiles: bool = False,
equilibrated: bool = True,
temperature: float = 298.15,
) -> _Tuple[_np.ndarray, _np.ndarray, _List[str], _Dict[str, _Dict[str, _np.ndarray]]]:
"""
Run MBAR on GROMACS dhdl output files using alchemlyb and pymbar.
"""
if subsampling:
raise NotImplementedError("Subsampling is not implemented for GROMACS MBAR.")

import pandas as _pd
import pymbar as _pymbar
from alchemlyb.parsing.gmx import extract_u_nk as _extract_u_nk

tmp_files = _prepare_xvg_files(
output_dir=output_dir,
run_nos=run_nos,
percentage_end=percentage_end,
percentage_start=percentage_start,
equilibrated=equilibrated,
)

mbar_out_files = []
free_energies = []
errors = []
mbar_grads = {}
kt = _R * temperature / 4184

for run_no in run_nos:
xvg_files = _get_gromacs_xvg_files(
output_dir=output_dir,
run_no=run_no,
percentage_end=percentage_end,
percentage_start=percentage_start,
)
u_nk = _pd.concat(
[_extract_u_nk(xvg_file, temperature) for xvg_file in xvg_files]
)
u_nk = u_nk.sort_index(level=u_nk.index.names[1:])
groups = u_nk.groupby(level=u_nk.index.names[1:])
n_k = [
len(groups.get_group(state)) if state in groups.groups else 0
for state in u_nk.columns
]
mbar = _pymbar.MBAR(u_nk.T, n_k, initialize="BAR")
delta_f, d_delta_f, _ = mbar.getFreeEnergyDifferences(return_theta=True)

outfile = (
f"{output_dir}/freenrg-MBAR-run_{str(run_no).zfill(2)}_"
f"{round(percentage_end, 3)}_end_"
f"{round(percentage_start, 3)}_start.dat"
)
mbar_out_files.append(outfile)

lam_vals = _get_lambda_values_from_xvg_files(xvg_files)
delta_f = _np.array(delta_f) * kt
d_delta_f = _np.array(d_delta_f) * kt
overlap = _get_overlap_matrix(mbar)

free_energies.append(delta_f[0, -1])
errors.append(d_delta_f[0, -1])

_write_gromacs_mbar_output(
outfile=outfile,
xvg_files=xvg_files,
lam_vals=lam_vals,
delta_f=delta_f,
d_delta_f=d_delta_f,
overlap=overlap,
subsampling=subsampling,
)

mbar_grads[f"run_{run_no}"] = {}
lam_vals, grads, grad_errors = _read_mbar_gradients(outfile)
mbar_grads[f"run_{run_no}"]["lam_vals"] = lam_vals
mbar_grads[f"run_{run_no}"]["grads"] = grads
mbar_grads[f"run_{run_no}"]["grad_errors"] = grad_errors

if delete_outfiles:
for ofile in mbar_out_files:
_subprocess.run(["rm", ofile])
mbar_out_files = []

for tmp_file in tmp_files:
_subprocess.run(["rm", tmp_file])

return _np.array(free_energies), _np.array(errors), mbar_out_files, mbar_grads

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be great to test this against MBAR run through e.g. BioSimSpace (if possible) or even just checking it's fairly close to the GROMACS BAR result.

@fjclark fjclark left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Roy-Haolin-Du, thanks and congrats! Great to see this functionality.

I've had a skim over everything and looked at most of run in more detail.

Overall this looks really good! My main comment (reflected in most of my smaller comments) is that it would be amazing if we could separate out all of the engine specific logic as much as possible from other parts of a3fe, for example it would be great if run was almost completely engine-agnostic. In practice, this mainly mains eliminating engine if else statements like "if gromacs do this, else if somd do this", moving the logic into an engine specific module/ function so that the run modules can just depend on an interface rather than implementing engine-specific logic themselves. In practice it might not be practical to 100 % extract this logic but I think even moving towards this would make things a bit cleaner.

Also, I've started getting AI to review all my PRs recently and have found this often catches bugs. As well as the standard /review skills, I've found /validate-science fromhttps://github.com/dotsdl/agents-for-scientific-software useful, and also often use https://github.com/DietrichGebert/ponytail to review for conciseness and suggest where code could be removed.

I've not got time to check these (so ignore if irrelevant) but here's the result of asking codex to review and validate science (mainly issues around statistical inefficiency so not a massive problem since we're using inter-replicate errors, but obviously still worth addressing if correct):

Review:

- [P1] Clean nested GROMACS outputs before rerunning — a3fe/run/simulation.py:310
    SimulationRunner.clean() only performs non-recursive globs, while GROMACS writes checkpoints and results under em/ and prod/. Consequently, clean()
    leaves .cpt, .log, .xvg, and related outputs behind; a subsequent mdrun -deffnm can resume or reuse stale data. GROMACS cleanup must remove those
    nested artifacts or directories.

  - [P2] Convert statistical inefficiency using the output interval — a3fe/run/simulation.py:562
    Gradient samples are spaced by dt * nstdhdl, but GradientData converts sample counts using only engine_config.timestep. Against current upstream
    main, which includes the fs→ns conversion, this still underreports GROMACS inefficiency by nstdhdl—100× with defaults. On the unre-based feature tip,
    the missing fs→ns conversion instead makes it 10,000× too large. Derive the interval from returned sample times or explicitly use dt * nstdhdl / 1000
    ns.

Validate science:

Reviewed feature-gromacs-support against michellab/main at 62cf024. No files changed.

  ## Silently wrong

  - a3fe/analyse/mbar.py:164: GROMACS MBAR rejects subsampling=True, then treats every correlated trajectory frame as independent. On the supplied
    dataset, decorrelation reduced 501 frames/state to 19–27 and increased the two run uncertainties from 0.269/0.282 to 1.196/1.165 kcal/mol. Thus the
    current errors are materially underestimated. Verified numerically; the point estimates’ scientific accuracy still requires domain validation.

  - a3fe/run/stage.py:864: pymbar’s asymptotic 1σ errors are printed immediately before the claim that they are 95% confidence intervals. For supplied
    run 1, 0.269 kcal/mol is reported where a normal 95% conversion would be approximately 0.527 kcal/mol—before correcting for autocorrelation. Verified
    from pymbar semantics and the included data.

  ## Unvalidated

  - a3fe/tests/test_analyse.py:136: the GROMACS regression values and underlying XVG dataset were introduced together, with no recorded independent
    implementation, analytic result, or published reference. These tests would preserve an MBAR bug by updating the expected values. Repository
    provenance verified; scientific correctness needs an independent oracle.

  - a3fe/configuration/engine_config.py:641: only the discharge schedule is scientifically exercised. Restraint and vanish lambda mappings, endpoint
    signs, and the combined ABFE cycle have no analytic or differential tests. The integration test merely requires finite values. Verified coverage gap;
    stage conventions need domain confirmation.

  - a3fe/configuration/engine_config.py:439: numerous physical/protocol defaults are described as matching fragment-opt-abfe-benchmark, but no pinned
    source or comparison test establishes that provenance. Protocol drift would silently change results. Verified documentation gap; constants need a
    domain expert to trace to the intended protocol.

  ## Fragile

  - a3fe/analyse/mbar.py:192: analysis trusts the supplied/configured temperature without checking the temperature embedded in each XVG header. A stale
    or modified Stage.pkl therefore produces plausible results using the wrong β. Verified behavior; actual-temperature policy needs confirmation.

  - a3fe/analyse/mbar.py:195 and a3fe/analyse/mbar.py:596: MBAR states come from XVG column metadata, while reported lambda labels come independently
    from numerically sorted directory names. Descending or non-monotonic custom schedules are accepted but can mislabel PMFs and neighboring gradients.
    Verified by code-path inspection; a synthetic permutation invariant is missing.

  Focused GROMACS analysis/read tests passed: 3 passed, 2 skipped. The full suite showed no failures during roughly three minutes but was manually
  stopped before completion. There is no external ground truth here, so I cannot certify the physical results—only internal consistency and oracle
  quality.

  Before writing tests, the key invariants to confirm are: GROMACS errors should use decorrelated samples; per-run errors should remain explicitly 1σ or
  be converted to 95%; and lambda schedules should be strictly increasing.

@fjclark

fjclark commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Also the EngineType enums might be a nice place for match statements e.g.

class EngineType(_YamlSerialisableEnum):
    """The simulation engine used for production simulations."""

    SOMD = auto()
    GROMACS = auto()

    @property
    def engine_config(self) -> type[_EngineConfig]:
        """Return the configuration class for this engine."""
        match self:
            case EngineType.SOMD:
                return _SomdConfig
            case EngineType.GROMACS:
                return _GromacsConfig

@fjclark

fjclark commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

OK, thinking about this a bit more, I think I've recommended moving too much stuff to EngineType, which should probably remain small as it's purpose is identifying the engine rather than holding extensive information about implementation details.

A solution could be to create new engines/somd.py and engines/gromacs.py classes which contain all the engine-specific stuff which isn't configuration. These could each derive an engine-specific e.g. GromacsBackend(EngineBackend) class from an abstract EngineBackend which specifies the interface (e.g. which functions should be defined) and these could be stored in a registry in engines/__init__.py keyed by EngineType. Then, modules in run could do e.g. coordinate_file_prefix, coordinate_file_extension = engine_backend_registry[self.engine_type].coordinate_file_prefix_and_extension. You could also do things like times_ns, gradients_kcal = backend.read_gradients(...). The engine backends should likely be stateless (state information belongs in the config classes) so for the run classes, you could create a property on SimulationRunner e.g. engine_backend which returns the engine backend based on the engine type but avoids the large stateless class being pickled when you save things.

@Roy-Haolin-Du

Copy link
Copy Markdown
Member Author

Thank you Finlay for all your hard work! I will go through them carefully.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants