@@ -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
0 commit comments