Skip to content

Commit acc81d1

Browse files
authored
Merge pull request #447 from CCPBioSim/446-general-gas-phase-pbc
Support non-periodic trajectories without box dimensions
2 parents b9f7fad + 139b98e commit acc81d1

2 files changed

Lines changed: 64 additions & 1 deletion

File tree

‎CodeEntropy/trajectory/mda.py‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,10 @@ def merge_forces(
354354
select_atom_force = u_force.select_atoms("all")
355355

356356
coordinates = self._extract_timeseries(select_atom, kind="positions")
357-
dimensions = self._extract_timeseries(select_atom, kind="dimensions")
357+
if u.dimensions is None:
358+
dimensions = self._build_non_periodic_dimensions(coordinates)
359+
else:
360+
dimensions = self._extract_timeseries(select_atom, kind="dimensions")
358361

359362
forces = self._extract_force_timeseries_with_fallback(
360363
select_atom_force,
@@ -374,6 +377,30 @@ def merge_forces(
374377

375378
return new_universe
376379

380+
@staticmethod
381+
def _build_non_periodic_dimensions(coordinates: np.ndarray) -> np.ndarray:
382+
"""Build a box for a trajectory without box dimensions.
383+
384+
The axes and covariance code needs a box for the minimum image
385+
convention and unwrapping. The box is cubic and more than twice the
386+
largest interatomic distance, so neither step changes the coordinates.
387+
388+
Args:
389+
coordinates: Positions with shape ``(n_frames, n_atoms, 3)``.
390+
391+
Returns:
392+
Array of shape ``(n_frames, 6)`` holding the box lengths and 90
393+
degree angles for every frame.
394+
"""
395+
span = float(np.max(np.ptp(coordinates, axis=1)))
396+
length = max(10.0 * span, 100.0)
397+
box = np.array([length, length, length, 90.0, 90.0, 90.0], dtype=np.float32)
398+
logger.warning(
399+
"Trajectory has no box dimensions; treating it as non-periodic "
400+
f"using a {length:.1f} A cubic box."
401+
)
402+
return np.tile(box, (coordinates.shape[0], 1))
403+
377404
def _extract_timeseries(self, atomgroup, *, kind: str) -> np.ndarray:
378405
"""Extract a time series array using explicit frame indexing.
379406

‎tests/unit/CodeEntropy/levels/test_mda_universe_operations.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,3 +424,39 @@ def test_extract_fragment_atomgroup_returns_lightweight_range_selection() -> Non
424424
"index 10:13",
425425
updating=False,
426426
)
427+
428+
429+
def test_build_non_periodic_dimensions_box_exceeds_twice_largest_extent():
430+
coordinates = np.zeros((3, 2, 3))
431+
coordinates[:, 1] = [50.0, 0.0, 0.0]
432+
433+
out = UniverseOperations._build_non_periodic_dimensions(coordinates)
434+
435+
assert out.shape == (3, 6)
436+
assert np.all(out[:, :3] > 2 * 50.0)
437+
assert np.allclose(out[:, 3:], 90.0)
438+
439+
440+
def test_merge_forces_builds_box_for_non_periodic_trajectory(monkeypatch):
441+
ops = UniverseOperations()
442+
443+
u = MagicMock()
444+
u.dimensions = None
445+
u_force = MagicMock()
446+
monkeypatch.setattr(
447+
"CodeEntropy.trajectory.mda.mda.Universe", MagicMock(side_effect=[u, u_force])
448+
)
449+
ops._extract_timeseries = MagicMock(return_value=np.zeros((2, 2, 3)))
450+
ops._extract_force_timeseries_with_fallback = MagicMock(
451+
return_value=np.ones((2, 2, 3))
452+
)
453+
454+
merged = MagicMock()
455+
monkeypatch.setattr("CodeEntropy.trajectory.mda.mda.Merge", lambda ag: merged)
456+
457+
ops.merge_forces(
458+
tprfile="tpr", trrfile="trr", forcefile="f.trr", fileformat=None, kcal=False
459+
)
460+
461+
dimensions = merged.load_new.call_args.kwargs["dimensions"]
462+
assert dimensions.shape == (2, 6)

0 commit comments

Comments
 (0)