Skip to content

Commit 56fa6e1

Browse files
authored
Merge pull request #322 from CCPBioSim/lammps-compatibility
Lammps compatibility
2 parents fb3bba6 + b2efc26 commit 56fa6e1

4 files changed

Lines changed: 157 additions & 3 deletions

File tree

CodeEntropy/config/runtime.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,8 +335,14 @@ def _build_universe(
335335
kcal_units = args.kcal_force_units
336336

337337
if forcefile is None:
338-
logger.debug(f"Loading Universe with {tprfile} and {trrfile}")
339-
return mda.Universe(tprfile, trrfile, format=fileformat)
338+
if fileformat == "LAMMPSDUMP":
339+
logger.debug(
340+
f"Loading Universe with {tprfile} and {trrfile} (LAMMPSDUMP)"
341+
)
342+
return universe_operations.convert_lammps(tprfile, trrfile, fileformat)
343+
else:
344+
logger.debug(f"Loading Universe with {tprfile} and {trrfile}")
345+
return mda.Universe(tprfile, trrfile, format=fileformat)
340346

341347
return universe_operations.merge_forces(
342348
tprfile, trrfile, forcefile, fileformat, kcal_units

CodeEntropy/levels/mda.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,97 @@ def extract_fragment(
121121
selection_string = f"index {frag.indices[0]}:{frag.indices[-1]}"
122122
return self.select_atoms(universe, selection_string)
123123

124+
def convert_lammps(
125+
self,
126+
tprfile: str,
127+
trrfile,
128+
fileformat: str | None = None,
129+
) -> mda.Universe:
130+
"""Update the units produced from the universe produced from LAMMPS
131+
format topology and trajectory files. MDA currently has a bug that
132+
results in forces not being converted to the correct units
133+
(see issue for more details:
134+
https://github.com/MDAnalysis/mdanalysis/issues/5115
135+
)
136+
The method currently expects the following additional columns in the
137+
lammps dump file: fx fy fz c_5 c_7
138+
where c_5 and c_7 are the atom potential and kinetic energies respectively.
139+
140+
This method loads:
141+
142+
- Coordinates and dimensions from the coordinate trajectory
143+
(``tprfile`` + ``trrfile``).
144+
145+
Args:
146+
tprfile: Topology input file.
147+
trrfile: Coordinate trajectory file(s). This can be a single path or a
148+
list, as accepted by MDAnalysis.
149+
fileformat: Optional file format for the coordinate trajectory, as
150+
recognised by MDAnalysis.
151+
152+
Returns:
153+
MDAnalysis.Universe: A new Universe containing coordinates, forces and
154+
dimensions loaded into memory.
155+
156+
Raises:
157+
ValueError: If fileformat is not one of the supported values.
158+
"""
159+
160+
def _convert_lammps_forces_energies(ts):
161+
"""
162+
Convert lammps forces from kcal/mol/Ang to kJ/mol/Ang.
163+
Assumes columns for per-atom potential (c_5) and kinetic energies (c_7)
164+
are provided and converts these too.
165+
166+
Args:
167+
ts: MDAnalysis timeseries from the trajectory.
168+
169+
Returns:
170+
A converted time series.
171+
"""
172+
ts.forces *= 4.184
173+
ts.data["c_5"] *= 4.184
174+
ts.data["c_7"] *= 4.184
175+
return ts
176+
177+
def _convert_lammps_forces(ts):
178+
"""
179+
Convert lammps forces from kcal/mol/Ang to kJ/mol/Ang.
180+
181+
Args:
182+
ts: MDAnalysis timeseries from the trajectory.
183+
184+
Returns:
185+
A converted time series.
186+
"""
187+
ts.forces *= 4.184
188+
return ts
189+
190+
if fileformat == "LAMMPSDUMP":
191+
try:
192+
return mda.Universe(
193+
tprfile,
194+
trrfile,
195+
format=fileformat,
196+
additional_columns=["fx", "fy", "fz", "c_5", "c_7"],
197+
transformations=[_convert_lammps_forces_energies],
198+
)
199+
except KeyError:
200+
logger.debug(
201+
f"Warning: Energy columns not found in LAMMPSDUMP: {trrfile}"
202+
)
203+
return mda.Universe(
204+
tprfile,
205+
trrfile,
206+
format=fileformat,
207+
additional_columns=["fx", "fy", "fz"],
208+
transformations=[_convert_lammps_forces],
209+
)
210+
else:
211+
raise ValueError(
212+
f"Incorrect file format: {fileformat}, LAMMPSDUMP expected"
213+
)
214+
124215
def merge_forces(
125216
self,
126217
tprfile: str,

CodeEntropy/levels/neighbors.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,11 @@ def _get_rdkit_mol(self, universe, mol_id):
167167
rdkit_mol = frag.convert_to("RDKIT", force=True, inferrer=None)
168168
logger.debug("Warning: Dummy atoms found")
169169
else:
170-
rdkit_mol = molecule.convert_to("RDKIT", force=True)
170+
try:
171+
rdkit_mol = molecule.convert_to("RDKIT", force=True)
172+
except Exception:
173+
logger.debug("Warning: Constraint bonds to H atoms found")
174+
rdkit_mol = molecule.convert_to("RDKIT", force=True, inferrer=None)
171175

172176
number_heavy = rdkit_mol.GetNumHeavyAtoms()
173177
number_hydrogen = rdkit_mol.GetNumAtoms() - number_heavy

tests/unit/CodeEntropy/levels/test_mda_universe_operations.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,59 @@ def test_merge_forces_scales_kcal(monkeypatch):
118118
assert np.allclose(forces, np.ones((2, 2, 3)) * 4.184)
119119

120120

121+
def test_convert_lammps_transforms_forces_and_energies(monkeypatch):
122+
ops = UniverseOperations()
123+
124+
mock_universe = MagicMock()
125+
transformations_captured = []
126+
127+
def capture_universe(*args, **kwargs):
128+
if "transformations" in kwargs:
129+
transformations_captured.extend(kwargs["transformations"])
130+
return mock_universe
131+
132+
monkeypatch.setattr("CodeEntropy.levels.mda.mda.Universe", capture_universe)
133+
134+
ops.convert_lammps("tpr", "trr", "LAMMPSDUMP")
135+
136+
ts = MagicMock()
137+
ts.forces = np.array([[1.0, 2.0, 3.0]], dtype=float)
138+
ts.data = {"c_5": np.array([1.0]), "c_7": np.array([2.0])}
139+
140+
transformations_captured[0](ts)
141+
142+
assert np.allclose(ts.forces, np.array([[1.0, 2.0, 3.0]], dtype=float) * 4.184)
143+
assert np.allclose(ts.data["c_5"], np.array([1.0], dtype=float) * 4.184)
144+
assert np.allclose(ts.data["c_7"], np.array([2.0], dtype=float) * 4.184)
145+
146+
147+
def test_convert_lammps_fallback_on_keyerror(monkeypatch):
148+
ops = UniverseOperations()
149+
150+
transformations_captured = []
151+
call_count = [0]
152+
153+
def mock_universe(*args, **kwargs):
154+
call_count[0] += 1
155+
if call_count[0] == 1:
156+
raise KeyError("c_5")
157+
if "transformations" in kwargs:
158+
transformations_captured.extend(kwargs["transformations"])
159+
return MagicMock()
160+
161+
monkeypatch.setattr("CodeEntropy.levels.mda.mda.Universe", mock_universe)
162+
163+
ops.convert_lammps("tpr", "trr", "LAMMPSDUMP")
164+
165+
ts = MagicMock()
166+
ts.forces = np.array([[1.0, 2.0, 3.0]], dtype=float)
167+
168+
transformations_captured[0](ts)
169+
170+
assert np.allclose(ts.forces, np.array([[1.0, 2.0, 3.0]], dtype=float) * 4.184)
171+
assert call_count[0] == 2
172+
173+
121174
def test_select_atoms_builds_merged_universe_and_loads_timeseries(monkeypatch):
122175
ops = UniverseOperations()
123176

0 commit comments

Comments
 (0)