diff --git a/CodeEntropy/config/runtime.py b/CodeEntropy/config/runtime.py index 6ef626b7..6a00d04a 100644 --- a/CodeEntropy/config/runtime.py +++ b/CodeEntropy/config/runtime.py @@ -36,9 +36,9 @@ from CodeEntropy.core.logging import LoggingConfig from CodeEntropy.entropy.workflow import EntropyWorkflow from CodeEntropy.levels.dihedrals import ConformationStateBuilder -from CodeEntropy.levels.mda import UniverseOperations from CodeEntropy.molecules.grouping import MoleculeGrouper from CodeEntropy.results.reporter import ResultsReporter +from CodeEntropy.trajectory.mda import UniverseOperations logger = logging.getLogger(__name__) console = LoggingConfig.get_console() diff --git a/CodeEntropy/entropy/workflow.py b/CodeEntropy/entropy/workflow.py index 75115cd7..2adf3346 100644 --- a/CodeEntropy/entropy/workflow.py +++ b/CodeEntropy/entropy/workflow.py @@ -16,10 +16,8 @@ from __future__ import annotations import logging -import math from collections import defaultdict from collections.abc import Mapping -from dataclasses import dataclass from typing import Any import pandas as pd @@ -29,6 +27,8 @@ from CodeEntropy.entropy.water import WaterEntropy from CodeEntropy.levels.hierarchy import HierarchyBuilder from CodeEntropy.levels.level_dag import LevelDAG +from CodeEntropy.trajectory.frames import FrameSelection +from CodeEntropy.trajectory.source import FrameSource logger = logging.getLogger(__name__) console = LoggingConfig.get_console() @@ -36,23 +36,6 @@ SharedData = dict[str, Any] -@dataclass(frozen=True) -class TrajectorySlice: - """Trajectory slicing parameters. - - Attributes: - start: Inclusive start frame index. - end: Exclusive end frame index (or a concrete index derived from args). - step: Step size between frames. - n_frames: Number of frames in the slice. - """ - - start: int - end: int - step: int - n_frames: int - - class EntropyWorkflow: """Coordinate entropy calculations across structural levels. @@ -95,21 +78,22 @@ def execute(self) -> None: """Run the full entropy workflow and emit results. This orchestrates the complete entropy pipeline: - 1. Build trajectory slice. - 2. Apply atom and frame selection to create a reduced universe. + 1. Build trajectory frame selection. + 2. Apply atom/frame selection to create the current analysis universe. 3. Detect hierarchy levels. 4. Group molecules. 5. Split groups into water and non-water. - 6. Optionally compute water entropy (only if solute exists). + 6. Optionally compute water entropy. 7. Run level DAG and entropy graph. 8. Finalize and persist results. """ - traj = self._build_trajectory_slice() + frame_selection = self._build_frame_selection() console.print( - f"Analyzing a total of {traj.n_frames} frames in this calculation." + f"Analyzing a total of {frame_selection.n_frames} " + f"frames in this calculation." ) - reduced_universe = self._build_reduced_universe() + reduced_universe = self._build_reduced_universe(frame_selection) levels = self._detect_levels(reduced_universe) groups = self._group_molecules.grouping_molecules( @@ -121,7 +105,7 @@ def execute(self) -> None: ) if self._args.water_entropy and water_groups and nonwater_groups: - self._compute_water_entropy(traj, water_groups) + self._compute_water_entropy(frame_selection, water_groups) else: nonwater_groups.update(water_groups) @@ -129,7 +113,7 @@ def execute(self) -> None: reduced_universe=reduced_universe, levels=levels, groups=nonwater_groups, - traj=traj, + frame_selection=frame_selection, ) with self._reporter.progress(transient=False) as p: @@ -139,24 +123,47 @@ def execute(self) -> None: self._finalize_molecule_results() self._reporter.log_tables() + def _build_frame_selection(self) -> FrameSelection: + """Build the workflow frame selection. + + Returns: + FrameSelection containing absolute source-trajectory frame indices. + + Notes: + Physical frame slicing is not used. The selected frame indices are the + global workflow frame contract and are consumed by FrameSource. + """ + start, end, step = self._get_trajectory_bounds() + return FrameSelection.from_bounds( + start=start, + stop=end, + step=step, + ) + def _build_shared_data( self, reduced_universe: Any, levels: Any, groups: Mapping[int, Any], - traj: TrajectorySlice, + frame_selection: FrameSelection, ) -> SharedData: """Build the shared_data dict used by nodes and graphs. Args: - reduced_universe: Universe after applying selection. + reduced_universe: Active analysis universe after atom selection. + The trajectory is not physically frame-sliced. levels: Level definition per molecule id. - groups: Mapping of group id -> list of molecule ids. - traj: Trajectory slice parameters. + groups: Mapping of group id to molecule ids. + frame_selection: Explicit absolute workflow frame selection. Returns: Shared data dictionary for DAG/graph execution. """ + frame_source = FrameSource( + universe=reduced_universe, + selection=frame_selection, + ) + shared_data: SharedData = { "entropy_manager": self, "run_manager": self._run_manager, @@ -166,10 +173,14 @@ def _build_shared_data( "reduced_universe": reduced_universe, "levels": levels, "groups": dict(groups), - "start": traj.start, - "end": traj.end, - "step": traj.step, - "n_frames": traj.n_frames, + "start": frame_selection.source_start, + "end": frame_selection.source_stop_exclusive, + "step": frame_selection.infer_source_step(), + "n_frames": frame_selection.n_frames, + "frame_selection": frame_selection, + "frame_source": frame_source, + "frame_indices": list(frame_selection.indices), + "source_frame_indices": list(frame_selection.indices), } return shared_data @@ -198,66 +209,56 @@ def _run_entropy_graph( entropy_results = EntropyGraph().build().execute(shared_data, progress=progress) shared_data.update(entropy_results) - def _build_trajectory_slice(self) -> TrajectorySlice: - """Compute trajectory slicing parameters from args. + def _get_trajectory_bounds(self) -> tuple[int, int, int]: + """Return validated start, end, and step frame indices from args. Returns: - A TrajectorySlice describing the frames to analyze. + Tuple of ``(start, end, step)``. + + Raises: + ValueError: If the frame window is invalid. """ - start, end, step = self._get_trajectory_bounds() - n_frames = self._get_number_frames(start, end, step) - return TrajectorySlice(start=start, end=end, step=step, n_frames=n_frames) + n_total = len(self._universe.trajectory) - def _get_trajectory_bounds(self) -> tuple[int, int, int]: - """Return start, end, and step frame indices from args. + start = 0 if self._args.start is None else int(self._args.start) + end = ( + n_total + if self._args.end is None or int(self._args.end) == -1 + else int(self._args.end) + ) + step = 1 if self._args.step is None else int(self._args.step) - Returns: - Tuple of (start, end, step). - """ - start = self._args.start or 0 - end = len(self._universe.trajectory) if self._args.end == -1 else self._args.end - step = self._args.step or 1 return start, end, step - def _get_number_frames(self, start: int, end: int, step: int) -> int: - """Compute the number of frames in a trajectory slice. + def _build_reduced_universe(self, frame_selection: FrameSelection) -> Any: + """Apply atom selection and return the active analysis universe. Args: - start: Inclusive start frame index. - end: Exclusive end frame index. - step: Step between frames. + frame_selection: Workflow frame selection. Used for validation. Returns: - Number of frames processed. - """ - return math.floor((end - start) / step) + MDAnalysis Universe after atom selection. Frames are not physically + sliced; selected-frame access is handled by FrameSource. - def _build_reduced_universe(self) -> Any: - """Apply atom and frame selection and return the reduced universe. - - Returns: - MDAnalysis Universe (reduced according to user selections). + Raises: + ValueError: If no frames are selected. """ + if frame_selection.n_frames == 0: + raise ValueError("Frame selection is empty.") + selection = self._args.selection_string - start = self._args.start - end = len(self._universe.trajectory) if self._args.end == -1 else self._args.end - step = self._args.step + if selection == "all": - reduced_atoms = self._universe - else: - reduced_atoms = self._universe_operations.select_atoms( - self._universe, selection - ) - name = f"{len(reduced_atoms.trajectory)}_frame_dump_atom_selection" - self._run_manager.write_universe(reduced_atoms, name) + return self._universe - reduced_frames = self._universe_operations.select_frames( - reduced_atoms, start, end, step + reduced_atoms = self._universe_operations.select_atoms( + self._universe, + selection, ) - name = f"{len(reduced_frames.trajectory)}_frame_dump_frame_selection" - self._run_manager.write_universe(reduced_frames, name) + name = f"{len(reduced_atoms.trajectory)}_frame_dump_atom_selection" + self._run_manager.write_universe(reduced_atoms, name) - return reduced_frames + return reduced_atoms def _detect_levels(self, reduced_universe: Any) -> Any: """Detect hierarchy levels for each molecule in the reduced universe. @@ -316,25 +317,34 @@ def _split_water_groups( return nonwater_groups, water_groups def _compute_water_entropy( - self, traj: TrajectorySlice, water_groups: Mapping[int, Any] + self, + frame_selection: FrameSelection, + water_groups: Mapping[int, Any], ) -> None: """Compute water entropy for each water group and adjust selection string. Args: - traj: Trajectory slice parameters. - water_groups: Mapping of group id -> molecule ids for waters. + frame_selection: Workflow frame selection. + water_groups: Mapping of group id to molecule ids for waters. """ if not water_groups or not self._args.water_entropy: return + start = frame_selection.source_start + end = frame_selection.source_stop_exclusive + step = frame_selection.infer_source_step() + + if start is None or end is None: + return + water_entropy = WaterEntropy(self._args, self._reporter) for group_id in water_groups.keys(): water_entropy.calculate_and_log( universe=self._universe, - start=traj.start, - end=traj.end, - step=traj.step, + start=start, + end=end, + step=step, group_id=group_id, ) @@ -344,8 +354,8 @@ def _compute_water_entropy( else "not water" ) - logger.debug(f"WaterEntropy: molecule_data= {self._reporter.molecule_data}") - logger.debug(f"WaterEntropy: residue_data= {self._reporter.residue_data}") + logger.debug("WaterEntropy: molecule_data= %s", self._reporter.molecule_data) + logger.debug("WaterEntropy: residue_data= %s", self._reporter.residue_data) def _finalize_molecule_results(self) -> None: """Aggregate group totals and persist results to JSON. diff --git a/CodeEntropy/levels/dihedrals.py b/CodeEntropy/levels/dihedrals.py index b5cd2bb7..ee5f9c44 100644 --- a/CodeEntropy/levels/dihedrals.py +++ b/CodeEntropy/levels/dihedrals.py @@ -1,8 +1,16 @@ """Dihedral state assignment for conformational entropy. -This module converts dihedral angle time series into discrete conformational -state labels. The resulting state labels are used downstream to compute -conformational entropy. +This module converts selected-frame dihedral angle time series into discrete +conformational state labels. The resulting state labels are used downstream to +compute configurational entropy. + +Frame-index contract: + - ``FrameSelection.analysis_indices`` are used for MDAnalysis trajectory access + in the active analysis universe. + - ``Dihedral(...).run(start, stop, step)`` uses frame bounds in the active + analysis-universe index space. + - ``dihedral_results.results.angles`` is always indexed locally from zero. + Never use an absolute/source frame index directly into that result array. """ from __future__ import annotations @@ -15,6 +23,7 @@ from rich.progress import TaskID from CodeEntropy.results.reporter import _RichProgressSink +from CodeEntropy.trajectory.frames import FrameSelection logger = logging.getLogger(__name__) @@ -22,10 +31,10 @@ class ConformationStateBuilder: - """Build conformational state labels from dihedral angles.""" + """Build conformational state labels from selected-frame dihedral angles.""" def __init__(self, universe_operations: Any) -> None: - """Initializes the analysis helper. + """Initialize the analysis helper. Args: universe_operations: Object providing helper methods: @@ -40,46 +49,28 @@ def build_conformational_states( levels: dict[Any, list[str]], groups: dict[int, list[Any]], bin_width: float, + frame_selection: FrameSelection, progress: _RichProgressSink | None = None, ) -> tuple[dict[UAKey, list[str]], list[list[str]], dict[UAKey, int], list[int]]: - """Build conformational state labels from trajectory dihedrals. - - This method constructs discrete conformational state descriptors used in - configurational entropy calculations. It supports united-atom (UA) and - residue-level state generation depending on which hierarchy levels are - enabled per molecule. - - Progress reporting is optional and UI-agnostic. If a progress sink is - provided, the method will create a single task and advance it once per - molecule group. + """Build conformational state labels from selected trajectory frames. Args: - data_container: MDAnalysis Universe (or compatible container) used to + data_container: MDAnalysis Universe or compatible container used to extract fragments and compute dihedral time series. - levels: Mapping of molecule_id -> iterable of enabled level names - (e.g., ["united_atom", "residue"]). - groups: Mapping of group_id -> list of molecule_ids. + levels: Mapping of molecule id to enabled level names. + groups: Mapping of group id to molecule ids. bin_width: Histogram bin width in degrees used when identifying peak dihedral populations. - progress: Optional progress sink (e.g., from - ResultsReporter.progress()). Must expose add_task(), update(), - and advance(). + frame_selection: FrameSelection controlling which frames are analysed. + During the current migration stage, ``analysis_indices`` are local + indices into the physically frame-sliced analysis universe. + progress: Optional progress sink. Returns: - tuple: (states_ua, states_res, flexible_ua, flexible_res) - - - states_ua: Dict mapping (group_id, local_residue_id) -> list of state - labels (strings) across the analyzed trajectory. - - states_res: Structure indexed by group_id (or equivalent) containing - residue-level state labels (strings) across the analyzed trajectory. - - Notes: - - This function advances progress once per group_id. - helpers as implemented in this module. + Tuple ``(states_ua, states_res, flexible_ua, flexible_res)``. """ number_groups = len(groups) states_ua: dict[UAKey, list[str]] = {} - # states_res: list[list[str]] = [[]] states_res: list[list[str]] = [[] for _ in range(number_groups)] flexible_ua: dict[UAKey, int] = {} flexible_res: list[int] = [] @@ -97,7 +88,7 @@ def build_conformational_states( if progress is not None and task is not None: progress.update(task, title="No groups") progress.advance(task) - return states_ua, states_res + return states_ua, states_res, flexible_ua, flexible_res for group_id in groups.keys(): molecules = groups[group_id] @@ -110,33 +101,37 @@ def build_conformational_states( if progress is not None and task is not None: progress.update(task, title=f"Group {group_id}") + level_list = levels[molecules[0]] + peaks_ua, peaks_res = self._identify_peaks( data_container=data_container, molecules=molecules, bin_width=bin_width, - level_list=levels[molecules[0]], + level_list=level_list, + frame_selection=frame_selection, ) self._assign_states( data_container=data_container, group_id=group_id, molecules=molecules, - level_list=levels[molecules[0]], + level_list=level_list, peaks_ua=peaks_ua, peaks_res=peaks_res, states_ua=states_ua, states_res=states_res, flexible_ua=flexible_ua, flexible_res=flexible_res, + frame_selection=frame_selection, ) if progress is not None and task is not None: progress.advance(task) - logger.debug(f"States UA: {states_ua}") - logger.debug(f"Number of flexible dihedrals UA: {flexible_ua}") - logger.debug(f"States Res: {states_res}") - logger.debug(f"Number of flexible dihedrals Res: {flexible_res}") + logger.debug("States UA: %s", states_ua) + logger.debug("Number of flexible dihedrals UA: %s", flexible_ua) + logger.debug("States Res: %s", states_res) + logger.debug("Number of flexible dihedrals Res: %s", flexible_res) return states_ua, states_res, flexible_ua, flexible_res @@ -145,7 +140,7 @@ def _select_heavy_residue(self, mol: Any, res_id: int) -> Any: Args: mol: Representative molecule AtomGroup. - res_id: Residue index. + res_id: Local residue index. Returns: AtomGroup containing heavy atoms in the residue selection. @@ -162,18 +157,17 @@ def _get_dihedrals(self, data_container: Any, level: str) -> list[Any]: """Return dihedral AtomGroups for a container at a given level. Args: - data_container: MDAnalysis container (AtomGroup/Universe). - level: Either "united_atom" or "residue". + data_container: MDAnalysis container. + level: Either ``"united_atom"`` or ``"residue"``. Returns: - List of AtomGroups (each representing a dihedral definition). + List of AtomGroups, each representing a dihedral definition. """ atom_groups: list[Any] = [] if level == "united_atom": - dihedrals = data_container.dihedrals - for d in dihedrals: - atom_groups.append(d.atoms) + for dihedral in data_container.dihedrals: + atom_groups.append(dihedral.atoms) if level == "residue": num_residues = len(data_container.residues) @@ -193,7 +187,7 @@ def _get_dihedrals(self, data_container: Any, level: str) -> list[Any]: ) atom_groups.append(atom1 + atom2 + atom3 + atom4) - logger.debug(f"Level: {level}, Dihedrals: {atom_groups}") + logger.debug("Level: %s, Dihedrals: %s", level, atom_groups) return atom_groups def _identify_peaks( @@ -202,34 +196,33 @@ def _identify_peaks( molecules: list[Any], bin_width: float, level_list: list[Any], - ) -> list[list[float]]: - """Identify histogram peaks ("convex turning points") for each dihedral. - - Important: - This function intentionally preserves the legacy behavior: - it samples over the full trajectory length for each molecule - and does not apply start/end/step to the Dihedral run. + frame_selection: FrameSelection, + ) -> tuple[list[list[Any]], list[Any]]: + """Identify histogram peaks for each selected-frame dihedral series. Args: data_container: MDAnalysis universe. molecules: Molecule ids in the group. - levels: Molecule levels. - bin_width: Histogram bin width (degrees). + bin_width: Histogram bin width in degrees. + level_list: Enabled hierarchy levels for the representative molecule. + frame_selection: Selected frames in the active analysis-universe index + space. Returns: - List of peaks per dihedral (peak_values[dihedral_index] -> list of peaks). + Tuple of ``(peaks_ua, peaks_res)``. """ rep_mol = self._universe_operations.extract_fragment( data_container, molecules[0] ) - number_frames = len(rep_mol.trajectory) + number_frames = self._analysis_frame_count(frame_selection) num_residues = len(rep_mol.residues) - num_dihedrals_ua: list[Any] = [0 for _ in range(num_residues)] - phi_ua = {} - phi_res: dict[list, list[float]] = {} + num_dihedrals_ua: list[int] = [0 for _ in range(num_residues)] + phi_ua: dict[int, Any] = {} + phi_res: dict[int, list[float]] | list[Any] = {} peaks_ua: list[list[Any]] = [[] for _ in range(num_residues)] peaks_res: list[Any] = [] + num_dihedrals_res = 0 for molecule in molecules: mol = self._universe_operations.extract_fragment(data_container, molecule) @@ -240,82 +233,101 @@ def _identify_peaks( heavy_res = self._select_heavy_residue(mol, res_id) dihedrals = self._get_dihedrals(heavy_res, level) num_dihedrals_ua[res_id] = len(dihedrals) + if num_dihedrals_ua[res_id] == 0: - # No dihedrals, no peaks phi_ua[res_id] = [] + continue - else: - if res_id not in phi_ua: - phi_ua[res_id] = {} - dihedral_results = Dihedral(dihedrals).run() - phi_ua[res_id] = self._process_dihedral_phi( - dihedral_results, - num_dihedrals_ua[res_id], - number_frames, - phi_ua[res_id], - ) + if res_id not in phi_ua or isinstance(phi_ua[res_id], list): + phi_ua[res_id] = {} + + dihedral_results = self._run_dihedrals( + dihedrals=dihedrals, + frame_selection=frame_selection, + ) + phi_ua[res_id] = self._process_dihedral_phi( + dihedral_results=dihedral_results, + num_dihedrals=num_dihedrals_ua[res_id], + number_frames=number_frames, + phi_values=phi_ua[res_id], + ) elif level == "residue": dihedrals = self._get_dihedrals(mol, level) num_dihedrals_res = len(dihedrals) + if num_dihedrals_res == 0: - # No dihedrals, no peaks phi_res = [] + continue - else: - dihedral_results = Dihedral(dihedrals).run() - phi_res = self._process_dihedral_phi( - dihedral_results, - num_dihedrals_res, - number_frames, - phi_res, - ) + if isinstance(phi_res, list): + phi_res = {} + + dihedral_results = self._run_dihedrals( + dihedrals=dihedrals, + frame_selection=frame_selection, + ) + phi_res = self._process_dihedral_phi( + dihedral_results=dihedral_results, + num_dihedrals=num_dihedrals_res, + number_frames=number_frames, + phi_values=phi_res, + ) - logger.debug(f"phi_ua {phi_ua}") - logger.debug(f"phi_res {phi_res}") + logger.debug("phi_ua %s", phi_ua) + logger.debug("phi_res %s", phi_res) for level in level_list: if level == "united_atom": for res_id in range(num_residues): - if phi_ua[res_id] is None: + phi_values = phi_ua.get(res_id) + if not phi_values: peaks_ua[res_id] = [] else: peaks_ua[res_id] = self._process_histogram( - num_dihedrals_ua[res_id], phi_ua[res_id], bin_width + num_dihedrals=num_dihedrals_ua[res_id], + phi_values=phi_values, + bin_width=bin_width, ) elif level == "residue": - if phi_res is None: + if not phi_res: peaks_res = [] else: peaks_res = self._process_histogram( - num_dihedrals_res, phi_res, bin_width + num_dihedrals=num_dihedrals_res, + phi_values=phi_res, + bin_width=bin_width, ) return peaks_ua, peaks_res def _process_dihedral_phi( self, - dihedral_results, - num_dihedrals, - number_frames, - phi_values, - ): - """ - Find array of dihedral angle values. + dihedral_results: Any, + num_dihedrals: int, + number_frames: int, + phi_values: dict[int, list[float]], + ) -> dict[int, list[float]]: + """Collect positive-angle dihedral values from a local result array. Args: - dihedral_results: the result of MDAnalysis Dihedrals.run. - num_dihedrals: the number of dihedrals in the molecule or residue. + dihedral_results: Result of ``MDAnalysis.analysis.dihedrals.Dihedral``. + num_dihedrals: Number of dihedrals in the result. + number_frames: Number of local frames in ``dihedral_results``. + phi_values: Existing accumulator mapping dihedral index to values. Returns: - peaks + Updated ``phi_values`` accumulator. + + Notes: + ``dihedral_results.results.angles`` is indexed locally from zero. """ for dihedral_index in range(num_dihedrals): phi: list[float] = [] - for timestep in range(number_frames): - value = dihedral_results.results.angles[timestep][dihedral_index] + for local_i in range(number_frames): + value = dihedral_results.results.angles[local_i][dihedral_index] if value < 0: value += 360 phi.append(float(value)) @@ -329,19 +341,19 @@ def _process_dihedral_phi( def _process_histogram( self, - num_dihedrals, - phi_values, - bin_width, - ): - """ - Find peaks from array of dihedral angle values. + num_dihedrals: int, + phi_values: dict[int, list[float]], + bin_width: float, + ) -> list[Any]: + """Find histogram peaks from dihedral angle values. Args: - dihedral_results: the result of MDAnalysis Dihedrals.run. - num_dihedrals: the number of dihedrals in the molecule or residue. + num_dihedrals: Number of dihedrals. + phi_values: Mapping from dihedral index to angle values. + bin_width: Histogram bin width in degrees. Returns: - peaks + List of peak lists, one per dihedral. """ peak_values = [] for dihedral_index in range(num_dihedrals): @@ -349,7 +361,7 @@ def _process_histogram( number_bins = int(360 / bin_width) popul, bin_edges = np.histogram(a=phi, bins=number_bins, range=(0, 360)) - logger.debug(f"Histogram: {popul}") + logger.debug("Histogram: %s", popul) bin_value = [ 0.5 * (bin_edges[i] + bin_edges[i + 1]) for i in range(0, len(popul)) @@ -358,7 +370,7 @@ def _process_histogram( peaks = self._find_histogram_peaks(popul=popul, bin_value=bin_value) peak_values.append(peaks) - logger.debug(f"Dihedral: {dihedral_index} Peaks: {peaks}") + logger.debug("Dihedral: %s Peaks: %s", dihedral_index, peaks) return peak_values @@ -368,16 +380,12 @@ def _find_histogram_peaks( ) -> list[float]: """Return convex turning-point peaks from a histogram. - The selection of the population of the right adjacent bin takes into - account that the dihedral angles are circular. - Args: - popul: the array of counts for each bin - bin_value: the array of dihedral angle value at the center of each - bin. + popul: Histogram bin populations. + bin_value: Histogram bin centre values. Returns: - peaks: list of values associated with peaks. + List of peak positions. """ number_bins = len(popul) peaks: list[float] = [] @@ -406,31 +414,36 @@ def _assign_states( states_res: Any, flexible_ua: Any, flexible_res: Any, - ) -> list[str]: - """Assign discrete state labels for the provided dihedrals. - - Important: - This function intentionally preserves the legacy behavior: - it samples over the full trajectory length for each molecule - and does not apply start/end/step to the Dihedral run. + frame_selection: FrameSelection, + ) -> None: + """Assign discrete state labels for selected-frame dihedrals. Args: data_container: MDAnalysis universe. + group_id: Molecule group id. molecules: Molecule ids in the group. - dihedrals: Dihedral AtomGroups. - peaks: Peaks per dihedral. + level_list: Enabled hierarchy levels. + peaks_ua: UA-level peaks by residue. + peaks_res: Residue-level peaks. + states_ua: UA state accumulator. + states_res: Residue state accumulator. + flexible_ua: UA flexible-dihedral accumulator. + flexible_res: Residue flexible-dihedral accumulator. + frame_selection: Selected frames in the active analysis-universe index + space. Returns: - List of state labels (strings). + None. Mutates the provided state/flexible accumulators. """ rep_mol = self._universe_operations.extract_fragment( data_container, molecules[0] ) - number_frames = len(rep_mol.trajectory) + number_frames = self._analysis_frame_count(frame_selection) num_residues = len(rep_mol.residues) state_res = [] flex_res = 0 + for molecule in molecules: mol = self._universe_operations.extract_fragment(data_container, molecule) @@ -441,78 +454,87 @@ def _assign_states( heavy_res = self._select_heavy_residue(mol, res_id) dihedrals = self._get_dihedrals(heavy_res, level) num_dihedrals = len(dihedrals) + if num_dihedrals == 0: - # No dihedrals, no conformations states_ua[key] = [] flexible_ua[key] = 0 + continue + + dihedral_results = self._run_dihedrals( + dihedrals=dihedrals, + frame_selection=frame_selection, + ) + states, flexible = self._process_conformations( + peaks=peaks_ua[res_id], + dihedral_results=dihedral_results, + num_dihedrals=num_dihedrals, + number_frames=number_frames, + ) + + if key not in states_ua: + states_ua[key] = states + flexible_ua[key] = flexible else: - dihedral_results = Dihedral(dihedrals).run() - states, flexible = self._process_conformations( - peaks_ua[res_id], - dihedral_results, - num_dihedrals, - number_frames, - ) - if key not in states_ua: - states_ua[key] = states - flexible_ua[key] = flexible - else: - states_ua[key].extend(states) - flexible_ua[key] = max(flexible_ua[key], flexible) + states_ua[key].extend(states) + flexible_ua[key] = max(flexible_ua[key], flexible) if level == "residue": dihedrals = self._get_dihedrals(mol, level) num_dihedrals = len(dihedrals) + if num_dihedrals == 0: - # No dihedrals, no conformations state_res = [] - else: - dihedral_results = Dihedral(dihedrals).run() - states, flexible = self._process_conformations( - peaks_res, - dihedral_results, - num_dihedrals, - number_frames, - ) - state_res.extend(states) - flex_res = max(flex_res, flexible) + continue + + dihedral_results = self._run_dihedrals( + dihedrals=dihedrals, + frame_selection=frame_selection, + ) + states, flexible = self._process_conformations( + peaks=peaks_res, + dihedral_results=dihedral_results, + num_dihedrals=num_dihedrals, + number_frames=number_frames, + ) + state_res.extend(states) + flex_res = max(flex_res, flexible) states_res.append(state_res) flexible_res.append(flex_res) def _process_conformations( - self, peaks, dihedral_results, num_dihedrals, number_frames - ): - """ - Find conformations + self, + peaks: list[Any], + dihedral_results: Any, + num_dihedrals: int, + number_frames: int, + ) -> tuple[list[str], int]: + """Assign conformational state labels from local dihedral results. Args: peaks: Histogram peaks. - num_dihedrals: Number of dihedral angles in the molecule or residue. + dihedral_results: Result of ``Dihedral(...).run(...)``. + num_dihedrals: Number of dihedrals. + number_frames: Number of local result frames. + Returns: - conformations + Tuple of ``(states, num_flexible)``. + + Notes: + ``dihedral_results.results.angles`` is indexed locally from zero. """ - states: list[list[Any]] = [] + states: list[str] = [] conformations: list[list[Any]] = [] num_flexible = 0 + for dihedral_index in range(num_dihedrals): conformation: list[Any] = [] - # Check for flexible dihedrals - # if len(peaks[dihedral_index]) > 1: - # num_flexible += 1 - - # Get conformations - for timestep in range(number_frames): - value = dihedral_results.results.angles[timestep][dihedral_index] - # We want postive values in range 0 to 360 to make - # the peak assignment. - # works using the fact that dihedrals have circular symmetry - # (i.e. -15 degrees = +345 degrees) + for local_i in range(number_frames): + value = dihedral_results.results.angles[local_i][dihedral_index] if value < 0: value += 360 - # Find the peak closest to the dihedral value distances = [abs(value - peak) for peak in peaks[dihedral_index]] conformation.append(np.argmin(distances)) @@ -522,8 +544,6 @@ def _process_conformations( conformations.append(conformation) - # Concatenate all the dihedrals in the molecule into the state - # for the frame. mol_states = [ state for state in ( @@ -536,3 +556,49 @@ def _process_conformations( states.extend(mol_states) return states, num_flexible + + def _run_dihedrals(self, dihedrals: list[Any], frame_selection: FrameSelection): + """Run MDAnalysis dihedral analysis over selected absolute frames. + + Args: + dihedrals: Dihedral AtomGroups. + frame_selection: Absolute trajectory frame selection. + + Returns: + MDAnalysis Dihedral analysis result. + + Notes: + ``Dihedral.run(start, stop, step)`` uses absolute trajectory bounds. + The returned ``results.angles`` array is indexed locally from zero. + """ + if not dihedrals: + raise ValueError("Cannot run Dihedral analysis with no dihedrals.") + + start, stop, step = self._analysis_run_bounds(frame_selection) + return Dihedral(dihedrals).run(start=start, stop=stop, step=step) + + @staticmethod + def _analysis_frame_count(frame_selection: FrameSelection) -> int: + """Return the number of selected frames.""" + return frame_selection.n_frames + + @staticmethod + def _analysis_run_bounds(frame_selection: FrameSelection) -> tuple[int, int, int]: + """Return MDAnalysis run bounds for selected absolute frames. + + Args: + frame_selection: Absolute trajectory frame selection. + + Returns: + Tuple of ``(start, stop, step)`` in source-trajectory index space. + + Raises: + ValueError: If the selection is empty. + """ + start = frame_selection.source_start + stop = frame_selection.source_stop_exclusive + + if start is None or stop is None: + raise ValueError("Frame selection is empty.") + + return start, stop, frame_selection.infer_source_step() diff --git a/CodeEntropy/levels/frame_dag.py b/CodeEntropy/levels/frame_dag.py index 4dcc5a1e..05f6f522 100644 --- a/CodeEntropy/levels/frame_dag.py +++ b/CodeEntropy/levels/frame_dag.py @@ -67,19 +67,44 @@ def build(self) -> FrameGraph: return self def execute_frame(self, shared_data: dict[str, Any], frame_index: int) -> Any: - """Execute the frame DAG for a single trajectory frame. + """Execute the frame DAG for one selected analysis frame. + + FrameGraph owns trajectory positioning for frame-local execution. Higher-level + orchestration passes explicit frame indices but must not rely on hidden + MDAnalysis cursor state. Args: - shared_data: Shared workflow data dict. - frame_index: Absolute trajectory frame index. + shared_data: Shared workflow data dictionary. Must contain + ``"frame_source"``. + frame_index: Frame index valid for the active analysis universe. During + this migration stage this is local to the frame-reduced universe. Returns: - Frame-local covariance payload produced by FrameCovarianceNode. + Frame-local covariance payload produced by ``FrameCovarianceNode``. + + Raises: + KeyError: If ``"frame_source"`` is missing from ``shared_data``. + IndexError: If ``frame_index`` is outside trajectory bounds. """ - ctx = self._make_frame_ctx(shared_data=shared_data, frame_index=frame_index) + frame_source = shared_data["frame_source"] + frame_index = int(frame_index) + + try: + frame_source.seek(frame_index) + except IndexError as exc: + n_frames = len(frame_source.universe.trajectory) + raise IndexError( + f"Frame index {frame_index} is outside analysis trajectory bounds " + f"for trajectory with {n_frames} frames." + ) from exc + + ctx = self._make_frame_ctx( + shared_data=shared_data, + frame_index=frame_index, + ) for node_name in nx.topological_sort(self._graph): - logger.debug(f"[FrameGraph] running {node_name} @ frame={frame_index}") + logger.debug("[FrameGraph] running %s @ frame=%s", node_name, frame_index) self._nodes[node_name].run(ctx) return ctx["frame_covariance"] diff --git a/CodeEntropy/levels/level_dag.py b/CodeEntropy/levels/level_dag.py index cc5786a5..7409bcf2 100644 --- a/CodeEntropy/levels/level_dag.py +++ b/CodeEntropy/levels/level_dag.py @@ -153,55 +153,54 @@ def _add_static(self, name: str, node: Any, deps: list[str] | None = None) -> No self._static_graph.add_edge(dep, name) def _run_frame_stage( - self, shared_data: dict[str, Any], *, progress: _RichProgressSink | None = None + self, + shared_data: dict[str, Any], + *, + progress: _RichProgressSink | None = None, ) -> None: """Execute the per-frame DAG stage and reduce frame outputs. - This method iterates over the selected trajectory frames, executes the - frame-local DAG for each frame, and reduces the resulting outputs into the - shared accumulators stored in `shared_data`. + This method iterates over explicit frame indices provided by + ``shared_data["frame_source"]``. During this migration stage, those indices + are local indices into the physically frame-reduced analysis universe. After + physical frame slicing is removed, they will be absolute source-trajectory + indices. - Progress reporting is optional. If a progress sink is provided, a task is - always created. When the total number of frames cannot be determined, the - task is created with total=None (indeterminate). + FrameGraph owns trajectory positioning. LevelDAG only chooses which frame + indices to process and reduces each frame-local output into shared + accumulators. Args: - shared_data: Shared data dictionary. Must contain: - - "reduced_universe": MDAnalysis Universe providing the trajectory. - - "start", "end", "step": frame slicing parameters. - - any additional keys required by the frame DAG and reducer. - progress: Optional progress sink (e.g., from ResultsReporter.progress()). - Must expose add_task(), update(), and advance(). + shared_data: Shared data dictionary. Must contain ``frame_source``. + progress: Optional progress sink. Returns: - None. Mutates `shared_data` in-place via reduction. - - Notes: - The task title shows the current frame index being processed. + None. Mutates ``shared_data`` in-place via reduction. """ - u = shared_data["reduced_universe"] - n_frames = shared_data["n_frames"] + frame_source = shared_data["frame_source"] + frame_indices = [ + int(frame_index) for frame_index in frame_source.iter_indices() + ] + shared_data["n_frames"] = len(frame_indices) task: TaskID | None = None - total_frames: int | None = None if progress is not None: - try: - total_frames = n_frames - except Exception: - total_frames = None - task = progress.add_task( "[green]Frame processing", - total=total_frames, + total=len(frame_indices), title="Initializing", ) - for ts in u.trajectory: + for frame_index in frame_indices: if progress is not None and task is not None: - progress.update(task, title=f"Frame {ts.frame}") + progress.update(task, title=f"Frame {frame_index}") + + frame_out = self._frame_dag.execute_frame( + shared_data, + frame_index, + ) - frame_out = self._frame_dag.execute_frame(shared_data, ts.frame) self._reduce_one_frame(shared_data, frame_out) if progress is not None and task is not None: diff --git a/CodeEntropy/levels/neighbors.py b/CodeEntropy/levels/neighbors.py index a385d219..e40b06aa 100644 --- a/CodeEntropy/levels/neighbors.py +++ b/CodeEntropy/levels/neighbors.py @@ -32,24 +32,29 @@ def __init__(self): self._levels = None self._search = Search() - def get_neighbors(self, universe, levels, groups, n_frames, search_type): - """ - Find the neighbors relative to the central molecule. - - The search defaults to using RAD, but an MDAnalysis method based - on grid searches is also available. - The average number of neighbors is calculated. + def get_neighbors(self, universe, levels, groups, frame_source, search_type): + """Find average neighbour counts for each molecule group. Args: - universe: MDAnalysis universe object for the system - groups: list of groups for averaging - levels: list of levels for each molecule - search_type: str, how to find neigbours + universe: MDAnalysis universe object for the active analysis system. + levels: Level list for each molecule. + groups: Mapping of group id to molecule ids. + frame_source: FrameSource controlling selected trajectory access. + search_type: Neighbour search method, either ``"RAD"`` or ``"grid"``. Returns: - average_number_neighbors (dict): average number of neighbors - at each frame for each group + Dict mapping group id to average number of neighbours. + + Raises: + ValueError: If ``search_type`` is unknown. """ + frame_indices = [ + int(frame_index) for frame_index in frame_source.iter_indices() + ] + n_frames = len(frame_indices) + + if n_frames <= 0: + return {group_id: 0.0 for group_id in groups.keys()} number_neighbors = {} average_number_neighbors = {} @@ -59,37 +64,34 @@ def get_neighbors(self, universe, levels, groups, n_frames, search_type): highest_level = levels[molecules[0]][-1] for mol_id in molecules: - for timestep in range(n_frames): + for frame_index in frame_indices: if search_type == "RAD": - # Use the relative angular distance method to find neighbors neighbors = self._search.get_RAD_neighbors( - universe=universe, mol_id=mol_id, timestep=timestep + universe=universe, + mol_id=mol_id, + frame_source=frame_source, + frame_index=frame_index, ) elif search_type == "grid": - # Use MDAnalysis neighbor search. neighbors = self._search.get_grid_neighbors( universe=universe, mol_id=mol_id, highest_level=highest_level, - timestep=timestep, + frame_source=frame_source, + frame_index=frame_index, ) else: - # Raise error for unavailale search_type raise ValueError(f"unknown search_type {search_type}") - if group_id in number_neighbors: - number_neighbors[group_id].append(len(neighbors)) - else: - number_neighbors[group_id] = [len(neighbors)] + number_neighbors.setdefault(group_id, []).append(len(neighbors)) - # Get the average number of neighbors: - # dividing the sum by the number of molecules and number of frames number = np.sum(number_neighbors[group_id]) average_number_neighbors[group_id] = number / (len(molecules) * n_frames) logger.debug( - f"group: {group_id}" - f"number neighbors {average_number_neighbors[group_id]}" + "group: %s number neighbors %s", + group_id, + average_number_neighbors[group_id], ) return average_number_neighbors diff --git a/CodeEntropy/levels/nodes/conformations.py b/CodeEntropy/levels/nodes/conformations.py index 5e84be21..c73c153d 100644 --- a/CodeEntropy/levels/nodes/conformations.py +++ b/CodeEntropy/levels/nodes/conformations.py @@ -1,9 +1,9 @@ """Compute conformational states for configurational entropy calculations. -This module defines a static DAG node that scans the trajectory and builds -conformational state descriptors (united-atom and residue level). The resulting -states are stored in `shared_data` for later use by configurational entropy -calculations. +This module defines a static DAG node that scans the selected trajectory frames +and builds conformational state descriptors (united-atom and residue level). +The resulting states are stored in ``shared_data`` for later use by +configurational entropy calculations. """ from __future__ import annotations @@ -12,6 +12,7 @@ from typing import Any from CodeEntropy.levels.dihedrals import ConformationStateBuilder +from CodeEntropy.trajectory.frames import FrameSelection SharedData = dict[str, Any] ConformationalStates = dict[str, Any] @@ -23,7 +24,7 @@ class ConformationalStateConfig: """Configuration for conformational state construction. Attributes: - n_frames: Number of frames to be analyised. + n_frames: Number of frames to be analysed. bin_width: Histogram bin width in degrees. """ @@ -36,13 +37,19 @@ class ComputeConformationalStatesNode: Produces: shared_data["conformational_states"] = {"ua": states_ua, "res": states_res} - shared_data["flexible_dihedrals"] = {"ua: flexible_ua, "res": flexible_res} + shared_data["flexible_dihedrals"] = {"ua": flexible_ua, "res": flexible_res} Where: - - states_ua is a dict keyed by (group_id, local_residue_id) - - states_res is a list-like structure indexed by group_id (or equivalent) - - flexible_ua is a dict keyed by (group_id, local_residue_id) - - flexible_res is a list-like structure indexed by group_id (or equivalent) + - states_ua is a dict keyed by ``(group_id, local_residue_id)``. + - states_res is a list-like structure indexed by group id. + - flexible_ua is a dict keyed by ``(group_id, local_residue_id)``. + - flexible_res is a list-like structure indexed by group id. + + Notes: + Frame selection is provided through ``shared_data["frame_selection"]``. + During the current migration stage, that selection uses local + analysis-universe frame indices because the workflow still physically + frame-slices the universe. """ def __init__(self, universe_operations: Any) -> None: @@ -50,7 +57,7 @@ def __init__(self, universe_operations: Any) -> None: Args: universe_operations: Object providing universe selection utilities used - by `ConformationStateBuilder`. + by ``ConformationStateBuilder``. """ self._dihedral_analysis = ConformationStateBuilder( universe_operations=universe_operations @@ -63,19 +70,20 @@ def run( Args: shared_data: Shared data dictionary. Requires: - - "reduced_universe" - - "levels" - - "groups" - - "n_frames" - - "args" with attribute "bin_width" + - ``"reduced_universe"`` + - ``"levels"`` + - ``"groups"`` + - ``"frame_selection"`` + - ``"args"`` with attribute ``bin_width`` progress: Optional progress sink provided by ResultsReporter.progress(). Returns: - Dict containing "conformational_states" (also written into shared_data). + Dict containing ``"conformational_states"``. """ u = shared_data["reduced_universe"] levels = shared_data["levels"] groups = shared_data["groups"] + frame_selection: FrameSelection = shared_data["frame_selection"] bin_width = int(shared_data["args"].bin_width) states_ua, states_res, flexible_ua, flexible_res = ( @@ -84,18 +92,17 @@ def run( levels=levels, groups=groups, bin_width=bin_width, + frame_selection=frame_selection, progress=progress, ) ) - # Get state information into shared_data conformational_states: ConformationalStates = { "ua": states_ua, "res": states_res, } shared_data["conformational_states"] = conformational_states - # Get flexible_dihedral data into shared_data flexible_states: FlexibleStates = { "ua": flexible_ua, "res": flexible_res, diff --git a/CodeEntropy/levels/nodes/find_neighbors.py b/CodeEntropy/levels/nodes/find_neighbors.py index 36cb0b2c..9b905b2a 100644 --- a/CodeEntropy/levels/nodes/find_neighbors.py +++ b/CodeEntropy/levels/nodes/find_neighbors.py @@ -52,42 +52,39 @@ def __init__(self) -> None: def run( self, shared_data: SharedData, *, progress: object | None = None ) -> SharedData: - """Compute conformational states and store them in shared_data. + """Compute neighbour and symmetry information and store it in shared_data. Args: shared_data: Shared data dictionary. Requires: - - "reduced_universe" - - "levels" - - "groups" - - "n_frames" - - "args" with attribute "bin_width" - progress: Optional progress sink provided by ResultsReporter.progress(). + - ``reduced_universe`` + - ``levels`` + - ``groups`` + - ``frame_source`` + - ``args.search_type`` + progress: Optional progress sink. Currently unused. Returns: - shared_data: SharedData + The mutated shared data dictionary. """ u = shared_data["reduced_universe"] levels = shared_data["levels"] groups = shared_data["groups"] - n_frames = int(shared_data["n_frames"]) + frame_source = shared_data["frame_source"] search_type = shared_data["args"].search_type - # Get average number of neighbors number_neighbors = self._neighbor_analysis.get_neighbors( universe=u, levels=levels, groups=groups, - n_frames=n_frames, + frame_source=frame_source, search_type=search_type, ) - # Get symmetry numbers and linearity symmetry_number, linear = self._neighbor_analysis.get_symmetry( universe=u, groups=groups, ) - # Add information to shared_data shared_data["neighbors"] = number_neighbors shared_data["symmetry_number"] = symmetry_number shared_data["linear"] = linear diff --git a/CodeEntropy/levels/search.py b/CodeEntropy/levels/search.py index f0862135..c15cd598 100644 --- a/CodeEntropy/levels/search.py +++ b/CodeEntropy/levels/search.py @@ -135,27 +135,28 @@ def __init__(self): self._cached_coms = None self._cached_dimensions = None - def _update_cache(self, universe): - """ - Update cached MDAnalysis data if the simulation frame has changed. + def _update_cache(self, universe, frame_index: int) -> None: + """Update cached MDAnalysis data for a specific selected frame. Args: - universe (MDAnalysis.Universe): - MDAnalysis universe object containing the system. + universe: MDAnalysis universe object containing the active analysis system. + frame_index: Frame index in the active analysis-universe index space. + + Returns: + None. """ - current_frame = universe.trajectory.ts.frame + frame_index = int(frame_index) - if self._cached_frame == current_frame: + if self._cached_frame == frame_index: return fragments = universe.atoms.fragments - coms = np.array([frag.center_of_mass() for frag in fragments]) self._cached_fragments = fragments self._cached_coms = coms self._cached_dimensions = universe.dimensions[:3] - self._cached_frame = current_frame + self._cached_frame = frame_index def _get_distances(self, coms, i_coords, dimensions): """ @@ -192,35 +193,32 @@ def _get_distances(self, coms, i_coords, dimensions): return np.sqrt((delta * delta).sum(axis=1)) - def get_RAD_neighbors(self, universe, mol_id, timestep): - """ - Find RAD neighbors of a given molecule. + def get_RAD_neighbors(self, universe, mol_id, frame_source, frame_index): + """Find RAD neighbours of a molecule at one selected frame. Args: - universe (MDAnalysis.Universe): - The MDAnalysis universe of the system. - mol_id (int): - Index of the central molecule. + universe: MDAnalysis universe object for the active analysis system. + mol_id: Index of the central molecule. + frame_source: FrameSource controlling selected trajectory access. + frame_index: Frame index in the active analysis-universe index space. Returns: - np.ndarray: - Indices of neighboring molecules identified via the RAD method. + Indices of neighbouring molecules identified by RAD. """ - universe.trajectory[timestep] - self._update_cache(universe) + frame_index = int(frame_index) + frame_source.seek(frame_index) + self._update_cache(universe, frame_index) fragments = self._cached_fragments coms = self._cached_coms dimensions = self._cached_dimensions number_molecules = len(fragments) - central_position = coms[mol_id] distances_array = self._get_distances(coms, central_position, dimensions) indices = np.arange(number_molecules) - mask = indices != mol_id filtered_indices = indices[mask] filtered_distances = distances_array[mask] @@ -240,26 +238,24 @@ def get_RAD_neighbors(self, universe, mol_id, timestep): return neighbor_indices - def get_grid_neighbors(self, universe, mol_id, highest_level, timestep): - """ - Find neighbors using MDAnalysis grid-based neighbor search. - - For small molecules (united_atom), atom-level search is used. - For larger molecules, residue-level search is used. + def get_grid_neighbors( + self, universe, mol_id, highest_level, frame_source, frame_index + ): + """Find neighbours using MDAnalysis grid search at one selected frame. Args: - universe (MDAnalysis.Universe): - MDAnalysis universe object for the system. - mol_id (int): - Index of the molecule of interest. - highest_level (str): - Molecule level ("united_atom" or other). + universe: MDAnalysis universe object for the active analysis system. + mol_id: Index of the molecule of interest. + highest_level: Molecule level, e.g. ``"united_atom"`` or ``"residue"``. + frame_source: FrameSource controlling selected trajectory access. + frame_index: Frame index in the active analysis-universe index space. Returns: - np.ndarray: - Fragment indices of neighboring molecules. + Fragment indices of neighbouring molecules. """ - universe.trajectory[timestep] + frame_index = int(frame_index) + frame_source.seek(frame_index) + fragments = universe.atoms.fragments fragment = fragments[mol_id] diff --git a/CodeEntropy/trajectory/__init__.py b/CodeEntropy/trajectory/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/CodeEntropy/trajectory/frames.py b/CodeEntropy/trajectory/frames.py new file mode 100644 index 00000000..dd247d50 --- /dev/null +++ b/CodeEntropy/trajectory/frames.py @@ -0,0 +1,134 @@ +"""Frame-selection primitives for trajectory-indexed execution. + +Frame-index contract: + - FrameSelection.indices are absolute MDAnalysis trajectory indices. + - MDAnalysis trajectory access must use these absolute frame indices. + - Arrays produced by analyses over FrameSelection are indexed locally with + enumerate(FrameSelection.indices). +""" + +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass + + +@dataclass(frozen=True) +class FrameSelection: + """Absolute trajectory frame selection. + + Attributes: + indices: Absolute source-trajectory frame indices selected for analysis. + """ + + indices: tuple[int, ...] + + @classmethod + def from_bounds(cls, start: int, stop: int, step: int) -> FrameSelection: + """Build a frame selection from Python range semantics. + + Args: + start: Inclusive source-trajectory start frame. + stop: Exclusive source-trajectory stop frame. + step: Frame stride. + + Returns: + FrameSelection containing absolute source-trajectory frame indices. + + Raises: + ValueError: If ``step`` is not positive. + """ + if step <= 0: + raise ValueError(f"Frame step must be positive, got {step}") + + return cls(indices=tuple(range(int(start), int(stop), int(step)))) + + def __len__(self) -> int: + """Return the number of selected frames.""" + return len(self.indices) + + def __iter__(self) -> Iterator[int]: + """Iterate over absolute source-trajectory frame indices.""" + return iter(self.indices) + + @property + def n_frames(self) -> int: + """Return the number of selected frames.""" + return len(self) + + @property + def source_indices(self) -> tuple[int, ...]: + """Return absolute source-trajectory frame indices. + + This compatibility property is intentionally identical to ``indices``. + """ + return self.indices + + @property + def analysis_indices(self) -> tuple[int, ...]: + """Return active analysis frame indices. + + Physical frame slicing has been removed, so analysis indices are absolute + source-trajectory indices. + """ + return self.indices + + @property + def source_start(self) -> int | None: + """Return the first selected source frame, or None if empty.""" + return self.indices[0] if self.indices else None + + @property + def source_stop_exclusive(self) -> int | None: + """Return one past the final selected source frame, or None if empty.""" + return self.indices[-1] + 1 if self.indices else None + + def iter_indices(self) -> Iterator[int]: + """Yield absolute source-trajectory frame indices.""" + yield from self.indices + + def iter_source_indices(self) -> Iterator[int]: + """Yield absolute source-trajectory frame indices.""" + yield from self.indices + + def iter_analysis_indices(self) -> Iterator[int]: + """Yield active analysis frame indices. + + Since physical frame slicing has been removed, these are absolute source + trajectory frame indices. + """ + yield from self.indices + + def iter_pairs(self) -> Iterator[tuple[int, int]]: + """Yield ``(local_i, absolute_frame_index)`` pairs.""" + yield from enumerate(self.indices) + + def infer_step(self) -> int: + """Infer the regular stride in selected frame indices. + + Returns: + Integer step between selected frames. Returns 1 for zero or one frame. + + Raises: + ValueError: If the frame selection is not regularly spaced. + """ + if len(self.indices) <= 1: + return 1 + + step = self.indices[1] - self.indices[0] + if step <= 0: + raise ValueError("Frame indices must be strictly increasing.") + + for left, right in zip(self.indices, self.indices[1:], strict=False): + if right - left != step: + raise ValueError("Frame selection is not regularly spaced.") + + return step + + def infer_source_step(self) -> int: + """Return the regular source-frame stride.""" + return self.infer_step() + + def infer_analysis_step(self) -> int: + """Return the regular analysis-frame stride.""" + return self.infer_step() diff --git a/CodeEntropy/levels/mda.py b/CodeEntropy/trajectory/mda.py similarity index 67% rename from CodeEntropy/levels/mda.py rename to CodeEntropy/trajectory/mda.py index d9cfc37e..04943a4c 100644 --- a/CodeEntropy/levels/mda.py +++ b/CodeEntropy/trajectory/mda.py @@ -9,9 +9,10 @@ from __future__ import annotations import logging +from typing import Any import MDAnalysis as mda -from MDAnalysis.analysis.base import AnalysisFromFunction +import numpy as np from MDAnalysis.coordinates.memory import MemoryReader from MDAnalysis.exceptions import NoDataError @@ -38,72 +39,136 @@ def select_frames( end: int | None = None, step: int = 1, ) -> mda.Universe: - """Create a reduced universe by dropping frames according to user selection. + """Create a reduced universe from explicit frame bounds. Args: - u: A Universe object with topology, coordinates and (optionally) forces. - start: Frame index to start analysis. If None, defaults to 0. - end: Frame index to stop analysis (Python slicing semantics). If None, - defaults to the full trajectory length. - step: Step size between frames. + u: Universe with topology, coordinates and optionally forces. + start: Inclusive start frame. If None, defaults to 0. + end: Exclusive stop frame. If None, defaults to full trajectory length. + step: Frame stride. Returns: - A reduced universe containing the selected frames, with coordinates, - forces (if present) and unit cell dimensions loaded into memory. + A reduced in-memory universe containing the selected frames. + + Raises: + ValueError: If ``step`` is not positive or no frames are selected. """ if start is None: start = 0 if end is None: end = len(u.trajectory) - select_atom = u.select_atoms("all", updating=True) + if step <= 0: + raise ValueError(f"Frame step must be positive, got {step}") - coordinates = self._extract_timeseries(select_atom, kind="positions")[ - start:end:step - ] - forces = self._extract_timeseries(select_atom, kind="forces")[start:end:step] - dimensions = self._extract_timeseries(select_atom, kind="dimensions")[ - start:end:step - ] + frame_indices = tuple(range(int(start), int(end), int(step))) + return self.select_frame_indices(u, frame_indices) - u2 = mda.Merge(select_atom) - u2.load_new( - coordinates, - format=MemoryReader, - forces=forces, - dimensions=dimensions, - ) + def select_frame_indices( + self, + u: mda.Universe, + frame_indices: tuple[int, ...] | list[int], + ) -> mda.Universe: + """Create a reduced universe from explicit trajectory frame indices. + + Args: + u: Universe with topology, coordinates and optionally forces. + frame_indices: Explicit trajectory frame indices to extract. + + Returns: + A reduced in-memory universe containing the selected frames. + + Raises: + ValueError: If ``frame_indices`` is empty. + """ + if not frame_indices: + raise ValueError( + "Cannot build a reduced universe from an empty frame list." + ) + + select_atom = u.select_atoms("all", updating=True) + reduced = self._build_memory_universe_from_atomgroup(select_atom, frame_indices) - logger.debug(f"MDAnalysis.Universe - reduced universe (frame-selected): {u2}") - return u2 + logger.debug( + "MDAnalysis.Universe - reduced universe (frame-selected): %s", reduced + ) + return reduced def select_atoms(self, u: mda.Universe, select_string: str = "all") -> mda.Universe: - """Create a reduced universe by dropping atoms according to user selection. + """Create a reduced universe by selecting atoms. Args: - u: A Universe object with topology, coordinates and (optionally) forces. - select_string: MDAnalysis `select_atoms` selection string. + u: Universe with topology, coordinates and optionally forces. + select_string: MDAnalysis selection string. Returns: - A reduced universe containing only the selected atoms. Coordinates, - forces (if present) and dimensions are loaded into memory. + A reduced universe containing only the selected atoms. Coordinates, forces + if present, and dimensions are loaded into memory. """ select_atom = u.select_atoms(select_string, updating=True) + frame_indices = tuple(range(len(u.trajectory))) - coordinates = self._extract_timeseries(select_atom, kind="positions") - forces = self._extract_timeseries(select_atom, kind="forces") - dimensions = self._extract_timeseries(select_atom, kind="dimensions") + reduced = self._build_memory_universe_from_atomgroup(select_atom, frame_indices) - u2 = mda.Merge(select_atom) - u2.load_new( - coordinates, - format=MemoryReader, - forces=forces, - dimensions=dimensions, + logger.debug( + "MDAnalysis.Universe - reduced universe (atom-selected): %s", reduced ) + return reduced + + def _build_memory_universe_from_atomgroup( + self, + atomgroup, + frame_indices: tuple[int, ...] | list[int], + ) -> mda.Universe: + """Build an in-memory Universe for an AtomGroup over explicit frames. - logger.debug(f"MDAnalysis.Universe - reduced universe (atom-selected): {u2}") - return u2 + Args: + atomgroup: MDAnalysis AtomGroup to copy into the new universe. + frame_indices: Explicit trajectory frame indices to extract. + + Returns: + In-memory MDAnalysis Universe. + + Raises: + ValueError: If no frames are provided. + """ + if not frame_indices: + raise ValueError("Cannot build a memory universe from an empty frame list.") + + universe = atomgroup.universe + + coordinates: list[np.ndarray] = [] + forces: list[np.ndarray] | None = [] + dimensions: list[np.ndarray] = [] + + for frame_index in frame_indices: + universe.trajectory[int(frame_index)] + + coordinates.append(atomgroup.positions.copy()) + dimensions.append(universe.dimensions.copy()) + + if forces is not None: + try: + forces.append(atomgroup.forces.copy()) + except NoDataError: + forces = None + + merged = mda.Merge(atomgroup) + + load_kwargs: dict[str, Any] = { + "format": MemoryReader, + "dimensions": np.asarray(dimensions), + } + + if forces is not None: + load_kwargs["forces"] = np.asarray(forces) + + merged.load_new( + np.asarray(coordinates), + **load_kwargs, + ) + + return merged def extract_fragment( self, universe: mda.Universe, molecule_id: int @@ -289,67 +354,38 @@ def merge_forces( return new_universe - def _extract_timeseries(self, atomgroup, *, kind: str): - """Extract a time series array for the requested kind from an AtomGroup. + def _extract_timeseries(self, atomgroup, *, kind: str) -> np.ndarray: + """Extract a time series array using explicit frame indexing. Args: - atomgroup: MDAnalysis AtomGroup (may be updating). - kind: One of {"positions", "forces", "dimensions"}. + atomgroup: MDAnalysis AtomGroup. + kind: One of ``"positions"``, ``"forces"``, or ``"dimensions"``. Returns: - Time series with shape: - - positions: (n_frames, n_atoms, 3) - - forces: (n_frames, n_atoms, 3) if available, else raises NoDataError - - dimensions: (n_frames, 6) or (n_frames, 3) depending on reader + NumPy array containing the requested data for all trajectory frames. Raises: - ValueError: If kind is not one of the supported values. - NoDataError: If kind is "forces" and the trajectory does not provide - forces via the configured reader. + ValueError: If ``kind`` is unknown. + NoDataError: If ``kind`` is ``"forces"`` and forces are unavailable. """ - if kind == "positions": - func = self._positions_copy - elif kind == "forces": - func = self._forces_copy - elif kind == "dimensions": - func = self._dimensions_copy - else: + valid_kinds = {"positions", "forces", "dimensions"} + if kind not in valid_kinds: raise ValueError(f"Unknown timeseries kind: {kind}") - return AnalysisFromFunction(func, atomgroup).run().results["timeseries"] + universe = atomgroup.universe + values: list[np.ndarray] = [] - def _positions_copy(self, ag): - """Return a copy of positions for AnalysisFromFunction. + for frame_index in range(len(universe.trajectory)): + universe.trajectory[int(frame_index)] - Args: - ag: MDAnalysis AtomGroup. - - Returns: - Copy of ag.positions. - """ - return ag.positions.copy() + if kind == "positions": + values.append(atomgroup.positions.copy()) + elif kind == "forces": + values.append(atomgroup.forces.copy()) + else: + values.append(universe.dimensions.copy()) - def _forces_copy(self, ag): - """Return a copy of forces for AnalysisFromFunction. - - Args: - ag: MDAnalysis AtomGroup. - - Returns: - Copy of ag.forces. - """ - return ag.forces.copy() - - def _dimensions_copy(self, ag): - """Return a copy of box dimensions for AnalysisFromFunction. - - Args: - ag: MDAnalysis AtomGroup. - - Returns: - Copy of ag.dimensions. - """ - return ag.dimensions.copy() + return np.asarray(values) def _extract_force_timeseries_with_fallback( self, diff --git a/CodeEntropy/trajectory/source.py b/CodeEntropy/trajectory/source.py new file mode 100644 index 00000000..b0b736a2 --- /dev/null +++ b/CodeEntropy/trajectory/source.py @@ -0,0 +1,49 @@ +"""MDAnalysis frame access boundary.""" + +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass +from typing import Any + +from CodeEntropy.trajectory.frames import FrameSelection + + +@dataclass +class FrameSource: + """Single owner of selected MDAnalysis trajectory frame access. + + Attributes: + universe: Active MDAnalysis Universe used for analysis. + selection: Absolute trajectory frame selection. + """ + + universe: Any + selection: FrameSelection + + def __len__(self) -> int: + """Return the number of selected frames.""" + return len(self.selection) + + def iter_indices(self) -> Iterator[int]: + """Yield absolute selected trajectory frame indices.""" + yield from self.selection.iter_indices() + + def iter_source_indices(self) -> Iterator[int]: + """Yield absolute selected trajectory frame indices.""" + yield from self.selection.iter_source_indices() + + def iter_pairs(self) -> Iterator[tuple[int, int]]: + """Yield ``(local_i, absolute_frame_index)`` pairs.""" + yield from self.selection.iter_pairs() + + def seek(self, frame_index: int) -> Any: + """Move the universe to an absolute trajectory frame. + + Args: + frame_index: Absolute source-trajectory frame index. + + Returns: + The MDAnalysis timestep for the selected frame. + """ + return self.universe.trajectory[int(frame_index)] diff --git a/docs/api/CodeEntropy.entropy.nodes.orientational.rst b/docs/api/CodeEntropy.entropy.nodes.orientational.rst new file mode 100644 index 00000000..21763359 --- /dev/null +++ b/docs/api/CodeEntropy.entropy.nodes.orientational.rst @@ -0,0 +1,7 @@ +CodeEntropy.entropy.nodes.orientational module +============================================== + +.. automodule:: CodeEntropy.entropy.nodes.orientational + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/api/CodeEntropy.entropy.nodes.rst b/docs/api/CodeEntropy.entropy.nodes.rst index 02c19511..2fa9338e 100644 --- a/docs/api/CodeEntropy.entropy.nodes.rst +++ b/docs/api/CodeEntropy.entropy.nodes.rst @@ -14,4 +14,5 @@ Submodules CodeEntropy.entropy.nodes.aggregate CodeEntropy.entropy.nodes.configurational + CodeEntropy.entropy.nodes.orientational CodeEntropy.entropy.nodes.vibrational diff --git a/docs/api/CodeEntropy.levels.mda.rst b/docs/api/CodeEntropy.levels.mda.rst deleted file mode 100644 index cda20a25..00000000 --- a/docs/api/CodeEntropy.levels.mda.rst +++ /dev/null @@ -1,7 +0,0 @@ -CodeEntropy.levels.mda module -============================= - -.. automodule:: CodeEntropy.levels.mda - :members: - :show-inheritance: - :undoc-members: diff --git a/docs/api/CodeEntropy.levels.neighbors.rst b/docs/api/CodeEntropy.levels.neighbors.rst new file mode 100644 index 00000000..ef34b9f9 --- /dev/null +++ b/docs/api/CodeEntropy.levels.neighbors.rst @@ -0,0 +1,7 @@ +CodeEntropy.levels.neighbors module +=================================== + +.. automodule:: CodeEntropy.levels.neighbors + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/api/CodeEntropy.levels.nodes.find_neighbors.rst b/docs/api/CodeEntropy.levels.nodes.find_neighbors.rst new file mode 100644 index 00000000..99bde1f6 --- /dev/null +++ b/docs/api/CodeEntropy.levels.nodes.find_neighbors.rst @@ -0,0 +1,7 @@ +CodeEntropy.levels.nodes.find\_neighbors module +=============================================== + +.. automodule:: CodeEntropy.levels.nodes.find_neighbors + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/api/CodeEntropy.levels.nodes.rst b/docs/api/CodeEntropy.levels.nodes.rst index 7ce5004f..e6112640 100644 --- a/docs/api/CodeEntropy.levels.nodes.rst +++ b/docs/api/CodeEntropy.levels.nodes.rst @@ -18,3 +18,4 @@ Submodules CodeEntropy.levels.nodes.covariance CodeEntropy.levels.nodes.detect_levels CodeEntropy.levels.nodes.detect_molecules + CodeEntropy.levels.nodes.find_neighbors diff --git a/docs/api/CodeEntropy.levels.rst b/docs/api/CodeEntropy.levels.rst index f3e6210c..a521eb6b 100644 --- a/docs/api/CodeEntropy.levels.rst +++ b/docs/api/CodeEntropy.levels.rst @@ -27,4 +27,5 @@ Submodules CodeEntropy.levels.hierarchy CodeEntropy.levels.level_dag CodeEntropy.levels.linalg - CodeEntropy.levels.mda + CodeEntropy.levels.neighbors + CodeEntropy.levels.search diff --git a/docs/api/CodeEntropy.levels.search.rst b/docs/api/CodeEntropy.levels.search.rst new file mode 100644 index 00000000..5084eb75 --- /dev/null +++ b/docs/api/CodeEntropy.levels.search.rst @@ -0,0 +1,7 @@ +CodeEntropy.levels.search module +================================ + +.. automodule:: CodeEntropy.levels.search + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/api/CodeEntropy.rst b/docs/api/CodeEntropy.rst index 1e9b3bdc..ae75c621 100644 --- a/docs/api/CodeEntropy.rst +++ b/docs/api/CodeEntropy.rst @@ -18,6 +18,7 @@ Subpackages CodeEntropy.levels CodeEntropy.molecules CodeEntropy.results + CodeEntropy.trajectory Submodules ---------- diff --git a/docs/api/CodeEntropy.trajectory.frames.rst b/docs/api/CodeEntropy.trajectory.frames.rst new file mode 100644 index 00000000..6cf1a699 --- /dev/null +++ b/docs/api/CodeEntropy.trajectory.frames.rst @@ -0,0 +1,7 @@ +CodeEntropy.trajectory.frames module +==================================== + +.. automodule:: CodeEntropy.trajectory.frames + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/api/CodeEntropy.trajectory.mda.rst b/docs/api/CodeEntropy.trajectory.mda.rst new file mode 100644 index 00000000..b686c2a7 --- /dev/null +++ b/docs/api/CodeEntropy.trajectory.mda.rst @@ -0,0 +1,7 @@ +CodeEntropy.trajectory.mda module +================================= + +.. automodule:: CodeEntropy.trajectory.mda + :members: + :show-inheritance: + :undoc-members: diff --git a/docs/api/CodeEntropy.trajectory.rst b/docs/api/CodeEntropy.trajectory.rst new file mode 100644 index 00000000..14005c6c --- /dev/null +++ b/docs/api/CodeEntropy.trajectory.rst @@ -0,0 +1,17 @@ +CodeEntropy.trajectory package +============================== + +.. automodule:: CodeEntropy.trajectory + :members: + :show-inheritance: + :undoc-members: + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + CodeEntropy.trajectory.frames + CodeEntropy.trajectory.mda + CodeEntropy.trajectory.source diff --git a/docs/api/CodeEntropy.trajectory.source.rst b/docs/api/CodeEntropy.trajectory.source.rst new file mode 100644 index 00000000..059201ff --- /dev/null +++ b/docs/api/CodeEntropy.trajectory.source.rst @@ -0,0 +1,7 @@ +CodeEntropy.trajectory.source module +==================================== + +.. automodule:: CodeEntropy.trajectory.source + :members: + :show-inheritance: + :undoc-members: diff --git a/tests/regression/baselines/benzaldehyde/axes_off.json b/tests/regression/baselines/benzaldehyde/axes_off.json index da07dbbb..5bddd538 100644 --- a/tests/regression/baselines/benzaldehyde/axes_off.json +++ b/tests/regression/baselines/benzaldehyde/axes_off.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/benzaldehyde/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/benzaldehyde/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/benzaldehyde/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw0/test_regression_matches_baseli0/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": false, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/benzaldehyde/combined_forcetorque_false.json b/tests/regression/baselines/benzaldehyde/combined_forcetorque_false.json index 507025ab..a5d1af0e 100644 --- a/tests/regression/baselines/benzaldehyde/combined_forcetorque_false.json +++ b/tests/regression/baselines/benzaldehyde/combined_forcetorque_false.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/benzaldehyde/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/benzaldehyde/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/benzaldehyde/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw0/test_regression_matches_baseli1/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": false, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/benzaldehyde/default.json b/tests/regression/baselines/benzaldehyde/default.json index 806d6975..c99a2d15 100644 --- a/tests/regression/baselines/benzaldehyde/default.json +++ b/tests/regression/baselines/benzaldehyde/default.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/benzaldehyde/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/benzaldehyde/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/benzaldehyde/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "all", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw1/test_regression_matches_baseli0/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/benzaldehyde/frame_window.json b/tests/regression/baselines/benzaldehyde/frame_window.json index d4fa3028..70bb2766 100644 --- a/tests/regression/baselines/benzaldehyde/frame_window.json +++ b/tests/regression/baselines/benzaldehyde/frame_window.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/benzaldehyde/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/benzaldehyde/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/benzaldehyde/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 5, - "step": 2, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw1/test_regression_matches_baseli1/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/benzaldehyde/grouping_each.json b/tests/regression/baselines/benzaldehyde/grouping_each.json index 3c4e80b9..0a218472 100644 --- a/tests/regression/baselines/benzaldehyde/grouping_each.json +++ b/tests/regression/baselines/benzaldehyde/grouping_each.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/benzaldehyde/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/benzaldehyde/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/benzaldehyde/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw2/test_regression_matches_baseli0/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "each", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/benzaldehyde/rad.json b/tests/regression/baselines/benzaldehyde/rad.json index 6e0757ff..8e39e21b 100644 --- a/tests/regression/baselines/benzaldehyde/rad.json +++ b/tests/regression/baselines/benzaldehyde/rad.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/benzaldehyde/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/benzaldehyde/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/benzaldehyde/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "all", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw2/test_regression_matches_baseli1/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "RAD" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/benzaldehyde/selection_subset.json b/tests/regression/baselines/benzaldehyde/selection_subset.json index cc633e79..6d50b1a7 100644 --- a/tests/regression/baselines/benzaldehyde/selection_subset.json +++ b/tests/regression/baselines/benzaldehyde/selection_subset.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/benzaldehyde/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/benzaldehyde/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/benzaldehyde/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw3/test_regression_matches_baseli0/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/benzene/axes_off.json b/tests/regression/baselines/benzene/axes_off.json index f7ba5b75..6177e0d0 100644 --- a/tests/regression/baselines/benzene/axes_off.json +++ b/tests/regression/baselines/benzene/axes_off.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/benzene/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/benzene/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/benzene/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw3/test_regression_matches_baseli1/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": false, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/benzene/combined_forcetorque_off.json b/tests/regression/baselines/benzene/combined_forcetorque_off.json index 1184f872..16b5e375 100644 --- a/tests/regression/baselines/benzene/combined_forcetorque_off.json +++ b/tests/regression/baselines/benzene/combined_forcetorque_off.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/benzene/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/benzene/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/benzene/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw4/test_regression_matches_baseli0/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": false, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/benzene/default.json b/tests/regression/baselines/benzene/default.json index b49f953a..86ae25d0 100644 --- a/tests/regression/baselines/benzene/default.json +++ b/tests/regression/baselines/benzene/default.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/benzene/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/benzene/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/benzene/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "all", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw4/test_regression_matches_baseli1/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/benzene/frame_window.json b/tests/regression/baselines/benzene/frame_window.json index b6409fd5..c1ffd000 100644 --- a/tests/regression/baselines/benzene/frame_window.json +++ b/tests/regression/baselines/benzene/frame_window.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/benzene/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/benzene/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/benzene/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 5, - "step": 2, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw5/test_regression_matches_baseli0/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/benzene/grouping_each.json b/tests/regression/baselines/benzene/grouping_each.json index 65ac98ca..36535d8f 100644 --- a/tests/regression/baselines/benzene/grouping_each.json +++ b/tests/regression/baselines/benzene/grouping_each.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/benzene/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/benzene/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/benzene/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw5/test_regression_matches_baseli1/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "each", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/benzene/rad.json b/tests/regression/baselines/benzene/rad.json index 0c60ea5a..c4aca55f 100644 --- a/tests/regression/baselines/benzene/rad.json +++ b/tests/regression/baselines/benzene/rad.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/benzene/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/benzene/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/benzene/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "all", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw6/test_regression_matches_baseli0/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "RAD" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/benzene/selection_subset.json b/tests/regression/baselines/benzene/selection_subset.json index c2440b14..ff2d09b5 100644 --- a/tests/regression/baselines/benzene/selection_subset.json +++ b/tests/regression/baselines/benzene/selection_subset.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/benzene/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/benzene/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/benzene/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw6/test_regression_matches_baseli1/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/cyclohexane/axes_off.json b/tests/regression/baselines/cyclohexane/axes_off.json index 68573a94..5bf80282 100644 --- a/tests/regression/baselines/cyclohexane/axes_off.json +++ b/tests/regression/baselines/cyclohexane/axes_off.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/cyclohexane/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/cyclohexane/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/cyclohexane/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw7/test_regression_matches_baseli0/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": false, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/cyclohexane/combined_forcetorque_off.json b/tests/regression/baselines/cyclohexane/combined_forcetorque_off.json index cabf08c7..a548349d 100644 --- a/tests/regression/baselines/cyclohexane/combined_forcetorque_off.json +++ b/tests/regression/baselines/cyclohexane/combined_forcetorque_off.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/cyclohexane/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/cyclohexane/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/cyclohexane/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw7/test_regression_matches_baseli1/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": false, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/cyclohexane/default.json b/tests/regression/baselines/cyclohexane/default.json index ab21034c..c0233178 100644 --- a/tests/regression/baselines/cyclohexane/default.json +++ b/tests/regression/baselines/cyclohexane/default.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/cyclohexane/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/cyclohexane/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/cyclohexane/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "all", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw8/test_regression_matches_baseli0/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/cyclohexane/frame_window.json b/tests/regression/baselines/cyclohexane/frame_window.json index 1c9bcc2a..d21d437d 100644 --- a/tests/regression/baselines/cyclohexane/frame_window.json +++ b/tests/regression/baselines/cyclohexane/frame_window.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/cyclohexane/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/cyclohexane/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/cyclohexane/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 5, - "step": 2, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw8/test_regression_matches_baseli1/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/cyclohexane/grouping_each.json b/tests/regression/baselines/cyclohexane/grouping_each.json index d64b5bd9..364af0c5 100644 --- a/tests/regression/baselines/cyclohexane/grouping_each.json +++ b/tests/regression/baselines/cyclohexane/grouping_each.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/cyclohexane/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/cyclohexane/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/cyclohexane/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw9/test_regression_matches_baseli0/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "each", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/cyclohexane/rad.json b/tests/regression/baselines/cyclohexane/rad.json index 725d92cb..f65d9a4c 100644 --- a/tests/regression/baselines/cyclohexane/rad.json +++ b/tests/regression/baselines/cyclohexane/rad.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/cyclohexane/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/cyclohexane/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/cyclohexane/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "all", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw9/test_regression_matches_baseli1/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "RAD" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/cyclohexane/selection_subset.json b/tests/regression/baselines/cyclohexane/selection_subset.json index ef637557..b1a14443 100644 --- a/tests/regression/baselines/cyclohexane/selection_subset.json +++ b/tests/regression/baselines/cyclohexane/selection_subset.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/cyclohexane/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/cyclohexane/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/cyclohexane/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw10/test_regression_matches_baseli0/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/dna/axes_off.json b/tests/regression/baselines/dna/axes_off.json index becd0663..9504b7b5 100644 --- a/tests/regression/baselines/dna/axes_off.json +++ b/tests/regression/baselines/dna/axes_off.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/dna/md_A4_dna.tpr", - "/home/ogo12949/CodeEntropy/.testdata/dna/md_A4_dna_xf.trr" - ], - "force_file": null, - "file_format": null, - "kcal_force_units": false, - "selection_string": "resid 1:10", - "start": 0, - "end": 5, - "step": 2, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw10/test_regression_matches_baseli1/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": false, - "search_type": "RAD" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/dna/combined_forcetorque_off.json b/tests/regression/baselines/dna/combined_forcetorque_off.json index c4efcec9..e867cee1 100644 --- a/tests/regression/baselines/dna/combined_forcetorque_off.json +++ b/tests/regression/baselines/dna/combined_forcetorque_off.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/dna/md_A4_dna.tpr", - "/home/ogo12949/CodeEntropy/.testdata/dna/md_A4_dna_xf.trr" - ], - "force_file": null, - "file_format": null, - "kcal_force_units": false, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw12/test_regression_matches_baseli0/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": false, - "customised_axes": true, - "search_type": "RAD" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/dna/default.json b/tests/regression/baselines/dna/default.json index a7f2a872..2cbef740 100644 --- a/tests/regression/baselines/dna/default.json +++ b/tests/regression/baselines/dna/default.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/dna/md_A4_dna.tpr", - "/home/ogo12949/CodeEntropy/.testdata/dna/md_A4_dna_xf.trr" - ], - "force_file": null, - "file_format": null, - "kcal_force_units": false, - "selection_string": "all", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw12/test_regression_matches_baseli1/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "RAD" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/dna/frame_window.json b/tests/regression/baselines/dna/frame_window.json index dfbcb800..c35d2211 100644 --- a/tests/regression/baselines/dna/frame_window.json +++ b/tests/regression/baselines/dna/frame_window.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/dna/md_A4_dna.tpr", - "/home/ogo12949/CodeEntropy/.testdata/dna/md_A4_dna_xf.trr" - ], - "force_file": null, - "file_format": null, - "kcal_force_units": false, - "selection_string": "resid 1:10", - "start": 0, - "end": 5, - "step": 2, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw11/test_regression_matches_baseli0/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "RAD" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/dna/grouping_each.json b/tests/regression/baselines/dna/grouping_each.json index 6e2d6555..2cbef740 100644 --- a/tests/regression/baselines/dna/grouping_each.json +++ b/tests/regression/baselines/dna/grouping_each.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/dna/md_A4_dna.tpr", - "/home/ogo12949/CodeEntropy/.testdata/dna/md_A4_dna_xf.trr" - ], - "force_file": null, - "file_format": null, - "kcal_force_units": false, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw11/test_regression_matches_baseli1/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "each", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "RAD" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/dna/selection_subset.json b/tests/regression/baselines/dna/selection_subset.json index 86ed6ba3..2cbef740 100644 --- a/tests/regression/baselines/dna/selection_subset.json +++ b/tests/regression/baselines/dna/selection_subset.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/dna/md_A4_dna.tpr", - "/home/ogo12949/CodeEntropy/.testdata/dna/md_A4_dna_xf.trr" - ], - "force_file": null, - "file_format": null, - "kcal_force_units": false, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw13/test_regression_matches_baseli0/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "RAD" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/ethyl-acetate/axes_off.json b/tests/regression/baselines/ethyl-acetate/axes_off.json index 32c9fb89..4c28d3bd 100644 --- a/tests/regression/baselines/ethyl-acetate/axes_off.json +++ b/tests/regression/baselines/ethyl-acetate/axes_off.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/ethyl-acetate/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/ethyl-acetate/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/ethyl-acetate/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw13/test_regression_matches_baseli1/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": false, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/ethyl-acetate/combined_forcetorque_off.json b/tests/regression/baselines/ethyl-acetate/combined_forcetorque_off.json index bb3dd52c..8c68cce7 100644 --- a/tests/regression/baselines/ethyl-acetate/combined_forcetorque_off.json +++ b/tests/regression/baselines/ethyl-acetate/combined_forcetorque_off.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/ethyl-acetate/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/ethyl-acetate/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/ethyl-acetate/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw14/test_regression_matches_baseli0/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": false, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/ethyl-acetate/default.json b/tests/regression/baselines/ethyl-acetate/default.json index c5523de1..077cdbe3 100644 --- a/tests/regression/baselines/ethyl-acetate/default.json +++ b/tests/regression/baselines/ethyl-acetate/default.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/ethyl-acetate/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/ethyl-acetate/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/ethyl-acetate/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "all", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw14/test_regression_matches_baseli1/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/ethyl-acetate/frame_window.json b/tests/regression/baselines/ethyl-acetate/frame_window.json index 6bf54603..be566d07 100644 --- a/tests/regression/baselines/ethyl-acetate/frame_window.json +++ b/tests/regression/baselines/ethyl-acetate/frame_window.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/ethyl-acetate/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/ethyl-acetate/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/ethyl-acetate/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 5, - "step": 2, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw15/test_regression_matches_baseli0/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { @@ -37,9 +8,9 @@ "residue:FTmat-Rovibrational": 55.1631201009248, "united_atom:Conformational": 8.635471458692102, "residue:Conformational": 0.0, - "residue:Orientational": 0.0 + "residue:Orientational": 3.8984476469128193 }, - "total": 211.90727050294976 + "total": 215.80571814986257 } } } diff --git a/tests/regression/baselines/ethyl-acetate/grouping_each.json b/tests/regression/baselines/ethyl-acetate/grouping_each.json index b5cf4dd5..e8330e08 100644 --- a/tests/regression/baselines/ethyl-acetate/grouping_each.json +++ b/tests/regression/baselines/ethyl-acetate/grouping_each.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/ethyl-acetate/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/ethyl-acetate/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/ethyl-acetate/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw15/test_regression_matches_baseli1/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "each", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/ethyl-acetate/rad.json b/tests/regression/baselines/ethyl-acetate/rad.json index 8f60029d..ee7df624 100644 --- a/tests/regression/baselines/ethyl-acetate/rad.json +++ b/tests/regression/baselines/ethyl-acetate/rad.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/ethyl-acetate/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/ethyl-acetate/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/ethyl-acetate/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "all", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw12/test_regression_matches_baseli2/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "RAD" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/ethyl-acetate/selection_subset.json b/tests/regression/baselines/ethyl-acetate/selection_subset.json index c5123a99..270dbbd5 100644 --- a/tests/regression/baselines/ethyl-acetate/selection_subset.json +++ b/tests/regression/baselines/ethyl-acetate/selection_subset.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/ethyl-acetate/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/ethyl-acetate/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/ethyl-acetate/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw13/test_regression_matches_baseli2/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/methane/axes_off.json b/tests/regression/baselines/methane/axes_off.json index 58750f51..dddf93d3 100644 --- a/tests/regression/baselines/methane/axes_off.json +++ b/tests/regression/baselines/methane/axes_off.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/methane/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/methane/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/methane/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 112.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw11/test_regression_matches_baseli2/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": false, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/methane/combined_forcetorque_off.json b/tests/regression/baselines/methane/combined_forcetorque_off.json index 8660c840..318c30c6 100644 --- a/tests/regression/baselines/methane/combined_forcetorque_off.json +++ b/tests/regression/baselines/methane/combined_forcetorque_off.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/methane/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/methane/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/methane/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 112.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw12/test_regression_matches_baseli3/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": false, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/methane/default.json b/tests/regression/baselines/methane/default.json index b427cc21..d08ac60f 100644 --- a/tests/regression/baselines/methane/default.json +++ b/tests/regression/baselines/methane/default.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/methane/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/methane/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/methane/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "all", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 112.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw2/test_regression_matches_baseli2/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/methane/frame_window.json b/tests/regression/baselines/methane/frame_window.json index 763e8c59..9a45dc98 100644 --- a/tests/regression/baselines/methane/frame_window.json +++ b/tests/regression/baselines/methane/frame_window.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/methane/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/methane/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/methane/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 5, - "step": 2, - "bin_width": 30, - "temperature": 112.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw4/test_regression_matches_baseli2/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/methane/grouping_each.json b/tests/regression/baselines/methane/grouping_each.json index d2f55a0d..3b31963b 100644 --- a/tests/regression/baselines/methane/grouping_each.json +++ b/tests/regression/baselines/methane/grouping_each.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/methane/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/methane/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/methane/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 112.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw15/test_regression_matches_baseli2/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "each", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/methane/rad.json b/tests/regression/baselines/methane/rad.json index 17bda111..df8fbbd7 100644 --- a/tests/regression/baselines/methane/rad.json +++ b/tests/regression/baselines/methane/rad.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/methane/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/methane/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/methane/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "all", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 112.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw11/test_regression_matches_baseli3/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "RAD" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/methane/selection_subset.json b/tests/regression/baselines/methane/selection_subset.json index 563a747a..318c30c6 100644 --- a/tests/regression/baselines/methane/selection_subset.json +++ b/tests/regression/baselines/methane/selection_subset.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/methane/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/methane/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/methane/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 112.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw3/test_regression_matches_baseli2/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/methanol/axes_off.json b/tests/regression/baselines/methanol/axes_off.json index 9f5b4443..d16ce042 100644 --- a/tests/regression/baselines/methanol/axes_off.json +++ b/tests/regression/baselines/methanol/axes_off.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/methanol/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/methanol/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/methanol/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw5/test_regression_matches_baseli2/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": false, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/methanol/combined_forcetorque_off.json b/tests/regression/baselines/methanol/combined_forcetorque_off.json index bba69499..634a9deb 100644 --- a/tests/regression/baselines/methanol/combined_forcetorque_off.json +++ b/tests/regression/baselines/methanol/combined_forcetorque_off.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/methanol/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/methanol/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/methanol/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw14/test_regression_matches_baseli2/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": false, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/methanol/default.json b/tests/regression/baselines/methanol/default.json index 94e52c5b..be7f59aa 100644 --- a/tests/regression/baselines/methanol/default.json +++ b/tests/regression/baselines/methanol/default.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/methanol/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/methanol/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/methanol/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "all", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw0/test_regression_matches_baseli2/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/methanol/frame_window.json b/tests/regression/baselines/methanol/frame_window.json index ea727e58..41bd2a34 100644 --- a/tests/regression/baselines/methanol/frame_window.json +++ b/tests/regression/baselines/methanol/frame_window.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/methanol/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/methanol/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/methanol/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 5, - "step": 2, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw7/test_regression_matches_baseli2/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/methanol/grouping_each.json b/tests/regression/baselines/methanol/grouping_each.json index c260bdc2..bba3b3c8 100644 --- a/tests/regression/baselines/methanol/grouping_each.json +++ b/tests/regression/baselines/methanol/grouping_each.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/methanol/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/methanol/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/methanol/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw9/test_regression_matches_baseli2/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "each", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/methanol/rad.json b/tests/regression/baselines/methanol/rad.json index 849b9f41..3def6f3d 100644 --- a/tests/regression/baselines/methanol/rad.json +++ b/tests/regression/baselines/methanol/rad.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/methanol/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/methanol/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/methanol/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "all", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw13/test_regression_matches_baseli3/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "RAD" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/methanol/selection_subset.json b/tests/regression/baselines/methanol/selection_subset.json index f357b86a..293daec6 100644 --- a/tests/regression/baselines/methanol/selection_subset.json +++ b/tests/regression/baselines/methanol/selection_subset.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/methanol/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/methanol/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/methanol/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw10/test_regression_matches_baseli2/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/octonol/axes_off.json b/tests/regression/baselines/octonol/axes_off.json index ab7d8db6..bc391e6f 100644 --- a/tests/regression/baselines/octonol/axes_off.json +++ b/tests/regression/baselines/octonol/axes_off.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/octonol/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/octonol/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/octonol/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw11/test_regression_matches_baseli4/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": false, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/octonol/combined_forcetorque_off.json b/tests/regression/baselines/octonol/combined_forcetorque_off.json index e07b8134..920589a7 100644 --- a/tests/regression/baselines/octonol/combined_forcetorque_off.json +++ b/tests/regression/baselines/octonol/combined_forcetorque_off.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/octonol/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/octonol/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/octonol/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw10/test_regression_matches_baseli3/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": false, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/octonol/default.json b/tests/regression/baselines/octonol/default.json index c1d16e78..fdd22382 100644 --- a/tests/regression/baselines/octonol/default.json +++ b/tests/regression/baselines/octonol/default.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/octonol/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/octonol/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/octonol/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "all", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw3/test_regression_matches_baseli3/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/octonol/frame_window.json b/tests/regression/baselines/octonol/frame_window.json index 1c4e99d3..5c07dcc2 100644 --- a/tests/regression/baselines/octonol/frame_window.json +++ b/tests/regression/baselines/octonol/frame_window.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/octonol/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/octonol/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/octonol/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 5, - "step": 2, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw0/test_regression_matches_baseli3/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/octonol/grouping_each.json b/tests/regression/baselines/octonol/grouping_each.json index d8b159b9..b4801742 100644 --- a/tests/regression/baselines/octonol/grouping_each.json +++ b/tests/regression/baselines/octonol/grouping_each.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/octonol/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/octonol/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/octonol/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw5/test_regression_matches_baseli3/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "each", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/octonol/rad.json b/tests/regression/baselines/octonol/rad.json index 1521b6d2..c2c20839 100644 --- a/tests/regression/baselines/octonol/rad.json +++ b/tests/regression/baselines/octonol/rad.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/octonol/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/octonol/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/octonol/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "all", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw15/test_regression_matches_baseli3/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "RAD" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/octonol/selection_subset.json b/tests/regression/baselines/octonol/selection_subset.json index c4ae6bdd..5f9d18f1 100644 --- a/tests/regression/baselines/octonol/selection_subset.json +++ b/tests/regression/baselines/octonol/selection_subset.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/octonol/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/octonol/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/octonol/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw10/test_regression_matches_baseli4/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/water/axes_off.json b/tests/regression/baselines/water/axes_off.json index f10b21b3..9b798bdc 100644 --- a/tests/regression/baselines/water/axes_off.json +++ b/tests/regression/baselines/water/axes_off.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/water/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/water/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/water/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw3/test_regression_matches_baseli4/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": false, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/water/combined_forcetorque_off.json b/tests/regression/baselines/water/combined_forcetorque_off.json index 15ac1d57..2e632568 100644 --- a/tests/regression/baselines/water/combined_forcetorque_off.json +++ b/tests/regression/baselines/water/combined_forcetorque_off.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/water/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/water/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/water/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw13/test_regression_matches_baseli4/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": false, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/water/default.json b/tests/regression/baselines/water/default.json index 393e0608..c98bc3eb 100644 --- a/tests/regression/baselines/water/default.json +++ b/tests/regression/baselines/water/default.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/water/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/water/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/water/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "all", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw5/test_regression_matches_baseli4/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/water/frame_window.json b/tests/regression/baselines/water/frame_window.json index 322c4f09..c6ae550d 100644 --- a/tests/regression/baselines/water/frame_window.json +++ b/tests/regression/baselines/water/frame_window.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/water/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/water/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/water/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 5, - "step": 2, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw15/test_regression_matches_baseli4/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/water/grouping_each.json b/tests/regression/baselines/water/grouping_each.json index c7e83e57..acccd86b 100644 --- a/tests/regression/baselines/water/grouping_each.json +++ b/tests/regression/baselines/water/grouping_each.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/water/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/water/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/water/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw7/test_regression_matches_baseli3/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "each", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/water/rad.json b/tests/regression/baselines/water/rad.json index 59230c5e..cbda37a9 100644 --- a/tests/regression/baselines/water/rad.json +++ b/tests/regression/baselines/water/rad.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/water/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/water/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/water/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": false, - "selection_string": "all", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw11/test_regression_matches_baseli5/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "RAD" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/water/selection_subset.json b/tests/regression/baselines/water/selection_subset.json index 662ddc66..2e632568 100644 --- a/tests/regression/baselines/water/selection_subset.json +++ b/tests/regression/baselines/water/selection_subset.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/water/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/water/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/water/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw6/test_regression_matches_baseli2/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": true, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/regression/baselines/water/water_off.json b/tests/regression/baselines/water/water_off.json index f9575fdf..2e632568 100644 --- a/tests/regression/baselines/water/water_off.json +++ b/tests/regression/baselines/water/water_off.json @@ -1,33 +1,4 @@ { - "args": { - "top_traj_file": [ - "/home/ogo12949/CodeEntropy/.testdata/water/molecules.top", - "/home/ogo12949/CodeEntropy/.testdata/water/trajectory.crd" - ], - "force_file": "/home/ogo12949/CodeEntropy/.testdata/water/forces.frc", - "file_format": "MDCRD", - "kcal_force_units": true, - "selection_string": "resid 1:10", - "start": 0, - "end": 1, - "step": 1, - "bin_width": 30, - "temperature": 298.0, - "verbose": false, - "output_file": "/tmp/pytest-of-ogo12949/pytest-3/popen-gw7/test_regression_matches_baseli4/job001/output_file.json", - "force_partitioning": 0.5, - "water_entropy": false, - "grouping": "molecules", - "combined_forcetorque": true, - "customised_axes": true, - "search_type": "grid" - }, - "provenance": { - "python": "3.13.5", - "platform": "Linux-6.17.0-1017-oem-x86_64-with-glibc2.39", - "codeentropy_version": "2.1.1", - "git_sha": "f2e0e7c509c6683eea94dd25a25bba2639c0bb96" - }, "groups": { "0": { "components": { diff --git a/tests/unit/CodeEntropy/entropy/test_workflow.py b/tests/unit/CodeEntropy/entropy/test_workflow.py index 9271ac7a..5f6ab900 100644 --- a/tests/unit/CodeEntropy/entropy/test_workflow.py +++ b/tests/unit/CodeEntropy/entropy/test_workflow.py @@ -2,7 +2,10 @@ from types import SimpleNamespace from unittest.mock import MagicMock, patch +import pytest + from CodeEntropy.entropy.workflow import EntropyWorkflow +from CodeEntropy.trajectory.frames import FrameSelection def _make_wf(args): @@ -17,6 +20,15 @@ def _make_wf(args): ) +def _make_frame_selection( + start: int = 0, + end: int = 5, + step: int = 1, +) -> FrameSelection: + """Build a FrameSelection for workflow unit tests.""" + return FrameSelection.from_bounds(start=start, stop=end, step=step) + + def test_execute_calls_level_dag_and_entropy_graph_and_logs_tables(): args = SimpleNamespace( start=0, @@ -143,18 +155,16 @@ def test_get_trajectory_bounds_end_minus_one_uses_trajectory_length(): assert (start, end, step) == (0, 10, 2) -def test_get_number_frames_matches_python_slice_math(): - wf = EntropyWorkflow( - run_manager=MagicMock(), - args=MagicMock(), - universe=MagicMock(), - reporter=MagicMock(), - group_molecules=MagicMock(), - dihedral_analysis=MagicMock(), - universe_operations=MagicMock(), - ) - assert wf._get_number_frames(0, 10, 1) == 10 - assert wf._get_number_frames(0, 10, 2) == 5 +def test_frame_selection_matches_python_slice_math(): + """FrameSelection uses Python range semantics for selected frames.""" + selection_unit_step = _make_frame_selection(start=0, end=10, step=1) + selection_stride_two = _make_frame_selection(start=0, end=10, step=2) + + assert selection_unit_step.n_frames == 10 + assert selection_unit_step.indices == tuple(range(0, 10, 1)) + + assert selection_stride_two.n_frames == 5 + assert selection_stride_two.indices == tuple(range(0, 10, 2)) def test_finalize_results_called_even_if_empty(): @@ -195,7 +205,7 @@ def test_split_water_groups_returns_empty_when_none(): assert water == {} -def test_build_reduced_universe_non_all_selects_and_writes_universe(): +def test_build_reduced_universe_non_all_selects_atoms_and_writes_universe(): args = SimpleNamespace( selection_string="protein", grouping="molecules", @@ -209,14 +219,10 @@ def test_build_reduced_universe_non_all_selects_and_writes_universe(): universe.trajectory = list(range(3)) reduced = MagicMock() - reduced.trajectory = list(range(2)) - - reduced2 = MagicMock() - reduced2.trajectory = list(range(2)) + reduced.trajectory = list(range(3)) uops = MagicMock() uops.select_atoms.return_value = reduced - uops.select_frames.return_value = reduced2 run_manager = MagicMock() reporter = MagicMock() @@ -231,16 +237,59 @@ def test_build_reduced_universe_non_all_selects_and_writes_universe(): universe_operations=uops, ) - out = wf._build_reduced_universe() + frame_selection = _make_frame_selection(start=0, end=3, step=1) - assert out is reduced2 + out = wf._build_reduced_universe(frame_selection) + + assert out is reduced uops.select_atoms.assert_called_once_with(universe, "protein") - uops.select_frames.assert_called_once_with(reduced, 0, 3, 1) + uops.select_frames.assert_not_called() + run_manager.write_universe.assert_called_once_with( + reduced, + f"{len(reduced.trajectory)}_frame_dump_atom_selection", + ) + + +def test_build_reduced_universe_raises_when_frame_selection_empty(): + args = SimpleNamespace( + selection_string="all", + grouping="molecules", + start=0, + end=0, + step=1, + water_entropy=False, + output_file="out.json", + ) + + universe = MagicMock() + uops = MagicMock() + run_manager = MagicMock() + + wf = EntropyWorkflow( + run_manager=run_manager, + args=args, + universe=universe, + reporter=MagicMock(), + group_molecules=MagicMock(), + dihedral_analysis=MagicMock(), + universe_operations=uops, + ) + + frame_selection = FrameSelection(indices=()) + + with pytest.raises(ValueError, match="Frame selection is empty"): + wf._build_reduced_universe(frame_selection) + + uops.select_atoms.assert_not_called() + uops.select_frames.assert_not_called() + run_manager.write_universe.assert_not_called() def test_compute_water_entropy_updates_selection_string_and_calls_internal_method(): args = SimpleNamespace( - selection_string="all", water_entropy=True, temperature=298.0 + selection_string="all", + water_entropy=True, + temperature=298.0, ) wf = EntropyWorkflow( run_manager=MagicMock(), @@ -252,20 +301,20 @@ def test_compute_water_entropy_updates_selection_string_and_calls_internal_metho universe_operations=MagicMock(), ) - traj = SimpleNamespace(start=0, end=5, step=1) + frame_selection = _make_frame_selection(start=0, end=5, step=1) water_groups = {9: [1, 2]} with patch("CodeEntropy.entropy.workflow.WaterEntropy") as WaterCls: inst = WaterCls.return_value inst.calculate_and_log = MagicMock() - wf._compute_water_entropy(traj, water_groups) + wf._compute_water_entropy(frame_selection, water_groups) inst.calculate_and_log.assert_called_once_with( universe=wf._universe, - start=traj.start, - end=traj.end, - step=traj.step, + start=0, + end=5, + step=1, group_id=9, ) assert wf._args.selection_string == "not water" @@ -307,28 +356,29 @@ def test_build_reduced_universe_all_returns_original_universe(): output_file="out.json", ) universe = MagicMock() - - reduced = MagicMock() - reduced.trajectory = list(range(2)) - - reduced2 = MagicMock() - reduced2.trajectory = list(range(2)) + universe.trajectory = list(range(2)) uops = MagicMock() - uops.select_atoms.return_value = reduced - uops.select_frames.return_value = reduced2 - run_manager = MagicMock() + wf = EntropyWorkflow( - run_manager, args, universe, MagicMock(), MagicMock(), MagicMock(), uops + run_manager=run_manager, + args=args, + universe=universe, + reporter=MagicMock(), + group_molecules=MagicMock(), + dihedral_analysis=MagicMock(), + universe_operations=uops, ) - out = wf._build_reduced_universe() + frame_selection = _make_frame_selection(start=0, end=2, step=1) + + out = wf._build_reduced_universe(frame_selection) - assert out is reduced2 + assert out is universe uops.select_atoms.assert_not_called() - uops.select_frames.assert_called_once() - run_manager.write_universe.assert_called_once() + uops.select_frames.assert_not_called() + run_manager.write_universe.assert_not_called() def test_split_water_groups_partitions_correctly(): @@ -374,34 +424,94 @@ def test_split_water_groups_partitions_correctly(): def test_compute_water_entropy_instantiates_waterentropy_and_updates_selection_string(): args = SimpleNamespace( - selection_string="all", water_entropy=True, temperature=298.0 + selection_string="all", + water_entropy=True, + temperature=298.0, ) universe = MagicMock() reporter = MagicMock() + wf = EntropyWorkflow( - MagicMock(), args, universe, reporter, MagicMock(), MagicMock(), MagicMock() + MagicMock(), + args, + universe, + reporter, + MagicMock(), + MagicMock(), + MagicMock(), ) - traj = SimpleNamespace(start=0, end=5, step=1, n_frames=5) + frame_selection = _make_frame_selection(start=0, end=5, step=1) water_groups = {9: [0]} with patch("CodeEntropy.entropy.workflow.WaterEntropy") as WaterCls: inst = WaterCls.return_value inst.calculate_and_log = MagicMock() - wf._compute_water_entropy(traj, water_groups) + wf._compute_water_entropy(frame_selection, water_groups) WaterCls.assert_called_once_with(args, reporter) inst.calculate_and_log.assert_called_once_with( universe=universe, - start=traj.start, - end=traj.end, - step=traj.step, + start=0, + end=5, + step=1, group_id=9, ) assert args.selection_string == "not water" +def test_compute_water_entropy_returns_when_no_water_groups(): + args = SimpleNamespace( + selection_string="all", + water_entropy=True, + temperature=298.0, + output_file="out.json", + ) + wf = _make_wf(args) + frame_selection = FrameSelection.from_bounds(start=0, stop=5, step=1) + + with patch("CodeEntropy.entropy.workflow.WaterEntropy") as WaterCls: + wf._compute_water_entropy(frame_selection, water_groups={}) + + WaterCls.assert_not_called() + assert args.selection_string == "all" + + +def test_compute_water_entropy_returns_when_water_entropy_disabled(): + args = SimpleNamespace( + selection_string="all", + water_entropy=False, + temperature=298.0, + output_file="out.json", + ) + wf = _make_wf(args) + frame_selection = FrameSelection.from_bounds(start=0, stop=5, step=1) + + with patch("CodeEntropy.entropy.workflow.WaterEntropy") as WaterCls: + wf._compute_water_entropy(frame_selection, water_groups={9: [0]}) + + WaterCls.assert_not_called() + assert args.selection_string == "all" + + +def test_compute_water_entropy_returns_when_frame_selection_empty(): + args = SimpleNamespace( + selection_string="all", + water_entropy=True, + temperature=298.0, + output_file="out.json", + ) + wf = _make_wf(args) + frame_selection = FrameSelection(indices=()) + + with patch("CodeEntropy.entropy.workflow.WaterEntropy") as WaterCls: + wf._compute_water_entropy(frame_selection, water_groups={9: [0]}) + + WaterCls.assert_not_called() + assert args.selection_string == "all" + + def test_detect_levels_calls_hierarchy_builder(): args = SimpleNamespace( selection_string="all", water_entropy=False, output_file="out.json" @@ -426,11 +536,13 @@ def test_compute_water_entropy_returns_early_when_disabled_or_empty_groups(): ) wf = _make_wf(args) - traj = SimpleNamespace(start=0, end=5, step=1, n_frames=5) + frame_selection = _make_frame_selection(start=0, end=5, step=1) + + with patch("CodeEntropy.entropy.workflow.WaterEntropy") as WaterCls: + wf._compute_water_entropy(frame_selection, water_groups={}) - # empty water groups OR water_entropy disabled -> early return - wf._compute_water_entropy(traj, water_groups={}) - # no exception and no side effects expected + WaterCls.assert_not_called() + assert args.selection_string == "all" def test_finalize_molecule_results_skips_group_total_rows(): diff --git a/tests/unit/CodeEntropy/levels/nodes/test_conformations_node.py b/tests/unit/CodeEntropy/levels/nodes/test_conformations_node.py index 7e17f075..5f927f11 100644 --- a/tests/unit/CodeEntropy/levels/nodes/test_conformations_node.py +++ b/tests/unit/CodeEntropy/levels/nodes/test_conformations_node.py @@ -2,30 +2,55 @@ from unittest.mock import MagicMock from CodeEntropy.levels.nodes.conformations import ComputeConformationalStatesNode +from CodeEntropy.trajectory.frames import FrameSelection def test_compute_conformational_states_node_runs_and_writes_shared_data(): uops = MagicMock() node = ComputeConformationalStatesNode(universe_operations=uops) + frame_selection = FrameSelection.from_bounds(start=0, stop=10, step=1) + node._dihedral_analysis.build_conformational_states = MagicMock( - return_value=({"ua_key": ["0", "1"]}, [["00", "01"]], {"ua_key": [0]}, [0]) + return_value=( + {"ua_key": ["0", "1"]}, + [["00", "01"]], + {"ua_key": [0]}, + [0], + ) ) shared = { "reduced_universe": MagicMock(), "levels": {0: ["united_atom"]}, "groups": {0: [0]}, - "start": 0, - "end": 10, - "step": 1, - "n_frames": 10, + "frame_selection": frame_selection, "args": SimpleNamespace(bin_width=10), } out = node.run(shared) - assert "conformational_states" in out - assert shared["conformational_states"]["ua"] == {"ua_key": ["0", "1"]} - assert shared["conformational_states"]["res"] == [["00", "01"]] - node._dihedral_analysis.build_conformational_states.assert_called_once() + assert out == { + "conformational_states": { + "ua": {"ua_key": ["0", "1"]}, + "res": [["00", "01"]], + } + } + + assert shared["conformational_states"] == { + "ua": {"ua_key": ["0", "1"]}, + "res": [["00", "01"]], + } + assert shared["flexible_dihedrals"] == { + "ua": {"ua_key": [0]}, + "res": [0], + } + + node._dihedral_analysis.build_conformational_states.assert_called_once_with( + data_container=shared["reduced_universe"], + levels=shared["levels"], + groups=shared["groups"], + bin_width=10, + frame_selection=frame_selection, + progress=None, + ) diff --git a/tests/unit/CodeEntropy/levels/nodes/test_find_neighbors.py b/tests/unit/CodeEntropy/levels/nodes/test_find_neighbors.py index 93185021..56790306 100644 --- a/tests/unit/CodeEntropy/levels/nodes/test_find_neighbors.py +++ b/tests/unit/CodeEntropy/levels/nodes/test_find_neighbors.py @@ -7,28 +7,34 @@ def test_compute_find_neighbors_node_runs_and_writes_shared_data(): node = ComputeNeighborsNode() - node._neighbor_analysis.get_neighbors = MagicMock(return_value=({0: 7.8})) + frame_source = MagicMock() + node._neighbor_analysis.get_neighbors = MagicMock(return_value={0: 7.8}) node._neighbor_analysis.get_symmetry = MagicMock(return_value=({0: 2}, {0: False})) shared = { "reduced_universe": MagicMock(), "levels": {0: ["united_atom"]}, "groups": {0: [0]}, - "start": 0, - "end": 10, - "step": 1, - "n_frames": 10, + "frame_source": frame_source, "args": SimpleNamespace(search_type="RAD"), } out = node.run(shared) - assert "neighbors" in out - assert "symmetry_number" in out - assert "linear" in out + assert out is shared assert shared["neighbors"] == {0: 7.8} assert shared["symmetry_number"] == {0: 2} assert shared["linear"] == {0: False} - node._neighbor_analysis.get_neighbors.assert_called_once() - node._neighbor_analysis.get_symmetry.assert_called_once() + + node._neighbor_analysis.get_neighbors.assert_called_once_with( + universe=shared["reduced_universe"], + levels=shared["levels"], + groups=shared["groups"], + frame_source=frame_source, + search_type="RAD", + ) + node._neighbor_analysis.get_symmetry.assert_called_once_with( + universe=shared["reduced_universe"], + groups=shared["groups"], + ) diff --git a/tests/unit/CodeEntropy/levels/test_dihedrals.py b/tests/unit/CodeEntropy/levels/test_dihedrals.py index 1bbdbe95..f477ab8d 100644 --- a/tests/unit/CodeEntropy/levels/test_dihedrals.py +++ b/tests/unit/CodeEntropy/levels/test_dihedrals.py @@ -3,8 +3,10 @@ from unittest.mock import MagicMock, patch import numpy as np +import pytest from CodeEntropy.levels.dihedrals import ConformationStateBuilder +from CodeEntropy.trajectory.frames import FrameSelection class _AddableAG: @@ -34,6 +36,15 @@ def _fake_progress_bar(*_args, **_kwargs): yield _FakeProgress() +def _make_frame_selection( + start: int = 0, + stop: int = 2, + step: int = 1, +) -> FrameSelection: + """Build a FrameSelection for dihedral unit tests.""" + return FrameSelection.from_bounds(start=start, stop=stop, step=step) + + def test_select_heavy_residue_builds_two_selections(): uops = MagicMock() dt = ConformationStateBuilder(universe_operations=uops) @@ -103,44 +114,51 @@ def test_identify_peaks_sets_empty_outputs_when_no_dihedrals(): dt = ConformationStateBuilder(universe_operations=uops) mol = MagicMock() - mol.trajectory = [0, 1] + mol.residues = [MagicMock()] + mol.residues[0].atoms.indices = np.array([0, 1, 2, 3], dtype=int) uops.extract_fragment.return_value = mol + dt._select_heavy_residue = MagicMock(return_value=mol) + dt._get_dihedrals = MagicMock(return_value=[]) + + frame_selection = _make_frame_selection(start=0, stop=2, step=1) + peaks_ua, peaks_res = dt._identify_peaks( data_container=MagicMock(), molecules=[0], bin_width=30.0, level_list=["united_atom", "residue"], + frame_selection=frame_selection, ) - assert peaks_ua == [] + assert peaks_ua == [[]] assert peaks_res == [] -def test_identify_peaks_wraps_negative_angles_and_calls_find_histogram_peaks(): +def test_identify_peaks_wraps_negative_angles_and_calls_process_histogram(): uops = MagicMock() dt = ConformationStateBuilder(universe_operations=uops) mol = MagicMock() - mol.trajectory = [0, 1] mol.residues = [MagicMock()] mol.residues[0].atoms.indices = np.array([0, 1, 2, 3], dtype=int) uops.extract_fragment.return_value = mol - dihedral = MagicMock() + dihedrals = ["D0"] angles = np.array([[-10.0], [10.0]], dtype=float) dt._select_heavy_residue = MagicMock(return_value=mol) - dt._get_dihedrals = MagicMock(return_value=dihedral) - dt._process_dihedral_phi = MagicMock(return_value=angles) + dt._get_dihedrals = MagicMock(return_value=dihedrals) class _FakeDihedral: def __init__(self, _dihedrals): pass - def run(self): + def run(self, *args, **kwargs): return SimpleNamespace(results=SimpleNamespace(angles=angles)) + frame_selection = _make_frame_selection(start=0, stop=2, step=1) + with ( patch("CodeEntropy.levels.dihedrals.Dihedral", _FakeDihedral), patch.object(dt, "_process_histogram", return_value=[15.0]) as peaks_spy, @@ -150,9 +168,10 @@ def run(self): molecules=[0], bin_width=10.0, level_list=["united_atom", "residue"], + frame_selection=frame_selection, ) - assert out_ua[0] == [15.0] + assert out_ua == [[15.0]] assert out_res == [15.0] assert peaks_spy.call_count == 2 @@ -171,7 +190,6 @@ def test_assign_states_initialises_then_extends_for_multiple_molecules(): dt = ConformationStateBuilder(universe_operations=uops) mol = MagicMock() - mol.trajectory = [0, 1] mol.residues = [MagicMock()] mol.residues[0].atoms.indices = np.array([0, 1, 2, 3], dtype=int) uops.extract_fragment.return_value = mol @@ -179,6 +197,7 @@ def test_assign_states_initialises_then_extends_for_multiple_molecules(): dihedrals = ["D0"] angles = np.array([[5.0], [15.0]], dtype=float) peaks = [[5.0, 15.0]] + states_ua = {} states_res = [] flexible_ua = {} @@ -191,9 +210,11 @@ class _FakeDihedral: def __init__(self, _dihedrals): pass - def run(self): + def run(self, *args, **kwargs): return SimpleNamespace(results=SimpleNamespace(angles=angles)) + frame_selection = _make_frame_selection(start=0, stop=2, step=1) + with patch("CodeEntropy.levels.dihedrals.Dihedral", _FakeDihedral): dt._assign_states( data_container=MagicMock(), @@ -206,6 +227,7 @@ def run(self): states_res=states_res, flexible_ua=flexible_ua, flexible_res=flexible_res, + frame_selection=frame_selection, ) assert states_ua[(0, 0)] == ["0", "1", "0", "1"] @@ -214,26 +236,35 @@ def run(self): assert flexible_res[0] == 1 -def test_build_conformational_states_runs_group_and_skips_empty_group(monkeypatch): +def test_build_conformational_states_runs_group_and_skips_empty_group(): uops = MagicMock() dt = ConformationStateBuilder(universe_operations=uops) groups = {0: [], 1: [7]} levels = {7: ["residue"]} - uops.extract_fragment.return_value = MagicMock(trajectory=[0]) + dt._identify_peaks = MagicMock(return_value=([], [])) + dt._assign_states = MagicMock() + + frame_selection = _make_frame_selection(start=0, stop=1, step=1) states_ua, states_res, flex_ua, flex_res = dt.build_conformational_states( data_container=MagicMock(), levels=levels, groups=groups, bin_width=30.0, + frame_selection=frame_selection, ) assert states_ua == {} - assert len(states_res) == 3 + assert states_res == [[], []] assert flex_ua == {} - assert flex_res[0] == 0 + assert flex_res == [] + + dt._identify_peaks.assert_called_once() + dt._assign_states.assert_called_once() + assert dt._identify_peaks.call_args.kwargs["frame_selection"] is frame_selection + assert dt._assign_states.call_args.kwargs["frame_selection"] is frame_selection def test_identify_peaks_handles_multiple_dihedrals(): @@ -241,12 +272,11 @@ def test_identify_peaks_handles_multiple_dihedrals(): dt = ConformationStateBuilder(universe_operations=uops) mol = MagicMock() - mol.trajectory = [0, 1] mol.residues = [MagicMock()] mol.residues[0].atoms.indices = np.array([0, 1, 2, 3], dtype=int) uops.extract_fragment.return_value = mol - dihedrals = (["D0", "D1"],) + dihedrals = ["D0", "D1"] angles = np.array( [ [-10.0, 10.0], @@ -257,26 +287,67 @@ def test_identify_peaks_handles_multiple_dihedrals(): dt._select_heavy_residue = MagicMock(return_value=mol) dt._get_dihedrals = MagicMock(return_value=dihedrals) - dt._process_dihedral_phi = MagicMock(return_value=angles) dt._process_histogram = MagicMock(return_value=[1, 2]) class _FakeDihedral: def __init__(self, _dihedrals): pass - def run(self): + def run(self, *args, **kwargs): return SimpleNamespace(results=SimpleNamespace(angles=angles)) + frame_selection = _make_frame_selection(start=0, stop=2, step=1) + with patch("CodeEntropy.levels.dihedrals.Dihedral", _FakeDihedral): out_ua, out_res = dt._identify_peaks( data_container=MagicMock(), molecules=[0], bin_width=30.0, level_list=["united_atom", "residue"], + frame_selection=frame_selection, ) - assert len(out_ua[0]) == 2 - assert len(out_res) == 2 + assert out_ua == [[1, 2]] + assert out_res == [1, 2] + assert dt._process_histogram.call_count == 2 + + +def test_identify_peaks_initialises_phi_res_dict_before_processing_residue_dihedrals(): + uops = MagicMock() + dt = ConformationStateBuilder(universe_operations=uops) + + mol = MagicMock() + mol.residues = [MagicMock(), MagicMock(), MagicMock(), MagicMock()] + uops.extract_fragment.return_value = mol + + frame_selection = _make_frame_selection(start=0, stop=2, step=1) + + dihedrals = ["D0"] + dihedral_results = MagicMock() + processed_phi = {0: [10.0, 20.0]} + + dt._get_dihedrals = MagicMock(return_value=dihedrals) + dt._run_dihedrals = MagicMock(return_value=dihedral_results) + dt._process_dihedral_phi = MagicMock(return_value=processed_phi) + dt._process_histogram = MagicMock(return_value=[[15.0]]) + + peaks_ua, peaks_res = dt._identify_peaks( + data_container=MagicMock(), + molecules=[0], + bin_width=30.0, + level_list=["residue"], + frame_selection=frame_selection, + ) + + assert peaks_ua == [[], [], [], []] + assert peaks_res == [[15.0]] + + dt._process_dihedral_phi.assert_called_once_with( + dihedral_results=dihedral_results, + num_dihedrals=1, + number_frames=2, + phi_values={}, + ) def test_assign_states_filters_out_empty_state_strings_when_no_dihedrals(): @@ -284,40 +355,33 @@ def test_assign_states_filters_out_empty_state_strings_when_no_dihedrals(): dt = ConformationStateBuilder(universe_operations=uops) mol = MagicMock() - mol.trajectory = [0, 1, 2] mol.residues = [MagicMock()] mol.residues[0].atoms.indices = np.array([0, 1, 2, 3], dtype=int) uops.extract_fragment.return_value = mol - dihedrals = [] states_ua = {} states_res = [] flexible_ua = {} flexible_res = [] dt._select_heavy_residue = MagicMock(return_value=mol) - dt._get_dihedrals = MagicMock(return_value=dihedrals) - - class _FakeDihedral: - def __init__(self, _dihedrals): - pass + dt._get_dihedrals = MagicMock(return_value=[]) - def run(self): - return SimpleNamespace(results=SimpleNamespace(angles=[])) + frame_selection = _make_frame_selection(start=0, stop=3, step=1) - with patch("CodeEntropy.levels.dihedrals.Dihedral", _FakeDihedral): - dt._assign_states( - data_container=MagicMock(), - group_id=0, - molecules=[0], - level_list=["united_atom", "residue"], - peaks_ua=[], - peaks_res=[], - states_ua=states_ua, - states_res=states_res, - flexible_ua=flexible_ua, - flexible_res=flexible_res, - ) + dt._assign_states( + data_container=MagicMock(), + group_id=0, + molecules=[0], + level_list=["united_atom", "residue"], + peaks_ua=[], + peaks_res=[], + states_ua=states_ua, + states_res=states_res, + flexible_ua=flexible_ua, + flexible_res=flexible_res, + frame_selection=frame_selection, + ) assert states_ua[(0, 0)] == [] assert flexible_ua[(0, 0)] == 0 @@ -330,11 +394,10 @@ def test_identify_peaks_multiple_molecules_real_histogram(): dt = ConformationStateBuilder(universe_operations=uops) mol0 = MagicMock() - mol0.trajectory = [0, 1] mol0.residues = [MagicMock()] mol0.residues[0].atoms.indices = np.array([0, 1, 2, 3], dtype=int) + mol1 = MagicMock() - mol1.trajectory = [0, 1] mol1.residues = [MagicMock()] mol1.residues[0].atoms.indices = np.array([0, 1, 2, 3], dtype=int) @@ -342,32 +405,26 @@ def test_identify_peaks_multiple_molecules_real_histogram(): dihedrals = ["D0"] angles = np.array([[10.0], [20.0]], dtype=float) - phi_values = {} - phi_values[0] = np.array([[10.0], [20.0]], dtype=float) dt._select_heavy_residue = MagicMock(return_value=mol0) dt._get_dihedrals = MagicMock(return_value=dihedrals) - dt._process_dihedral_phi = MagicMock(return_value=phi_values) class _FakeDihedral: def __init__(self, _dihedrals): pass - def run(self): + def run(self, *args, **kwargs): return SimpleNamespace(results=SimpleNamespace(angles=angles)) - with ( - patch("CodeEntropy.levels.dihedrals.Dihedral", _FakeDihedral), - patch( - "CodeEntropy.levels.dihedrals.ConformationStateBuilder._process_dihedral_phi", - dt._process_dihedral_phi, - ), - ): + frame_selection = _make_frame_selection(start=0, stop=2, step=1) + + with patch("CodeEntropy.levels.dihedrals.Dihedral", _FakeDihedral): peaks_ua, peaks_res = dt._identify_peaks( data_container=MagicMock(), molecules=[0, 1], bin_width=90.0, level_list=["united_atom", "residue"], + frame_selection=frame_selection, ) assert len(peaks_ua) == 1 @@ -379,7 +436,6 @@ def test_assign_states_wraps_negative_angles(): dt = ConformationStateBuilder(universe_operations=uops) mol = MagicMock() - mol.trajectory = [0, 1] mol.residues = [MagicMock()] mol.residues[0].atoms.indices = np.array([0, 1, 2, 3], dtype=int) uops.extract_fragment.return_value = mol @@ -387,6 +443,7 @@ def test_assign_states_wraps_negative_angles(): angles = np.array([[-10.0], [10.0]], dtype=float) peaks = [[10.0, 350.0]] dihedrals = ["D0"] + states_ua = {} states_res = [] flexible_ua = {} @@ -399,9 +456,11 @@ class _FakeDihedral: def __init__(self, _dihedrals): pass - def run(self): + def run(self, *args, **kwargs): return SimpleNamespace(results=SimpleNamespace(angles=angles)) + frame_selection = _make_frame_selection(start=0, stop=2, step=1) + with patch("CodeEntropy.levels.dihedrals.Dihedral", _FakeDihedral): dt._assign_states( data_container=MagicMock(), @@ -414,6 +473,7 @@ def run(self): states_res=states_res, flexible_ua=flexible_ua, flexible_res=flexible_res, + frame_selection=frame_selection, ) assert states_ua[(0, 0)] == ["1", "0", "1", "0"] @@ -429,16 +489,22 @@ def test_build_conformational_states_with_progress_handles_no_groups(): progress = MagicMock() progress.add_task.return_value = 123 - states_ua, states_res = dt.build_conformational_states( + frame_selection = _make_frame_selection(start=0, stop=1, step=1) + + states_ua, states_res, flex_ua, flex_res = dt.build_conformational_states( data_container=MagicMock(), levels={}, - groups={}, # empty + groups={}, bin_width=30.0, + frame_selection=frame_selection, progress=progress, ) assert states_ua == {} assert states_res == [] + assert flex_ua == {} + assert flex_res == [] + progress.add_task.assert_called_once() progress.update.assert_called_once_with(123, title="No groups") progress.advance.assert_called_once_with(123) @@ -451,26 +517,27 @@ def test_build_conformational_states_with_progress_skips_empty_molecule_group(): progress = MagicMock() progress.add_task.return_value = 5 - groups = {0: []} - levels = {} + frame_selection = _make_frame_selection(start=0, stop=1, step=1) states_ua, states_res, flex_ua, flex_res = dt.build_conformational_states( data_container=MagicMock(), - levels=levels, - groups=groups, + levels={}, + groups={0: []}, bin_width=30.0, + frame_selection=frame_selection, progress=progress, ) assert states_ua == {} - assert len(states_res) == 1 + assert states_res == [[]] assert flex_ua == {} assert flex_res == [] + progress.update.assert_called_with(5, title="Group 0 (empty)") progress.advance.assert_called_with(5) -def test_build_conformational_states_with_progress_updates_title_per_group(monkeypatch): +def test_build_conformational_states_with_progress_updates_title_per_group(): uops = MagicMock() dt = ConformationStateBuilder(universe_operations=uops) @@ -480,18 +547,24 @@ def test_build_conformational_states_with_progress_updates_title_per_group(monke groups = {1: [7]} levels = {7: ["residue"]} - uops.extract_fragment.return_value = MagicMock(trajectory=[0]) + dt._identify_peaks = MagicMock(return_value=([], [])) + dt._assign_states = MagicMock() + + frame_selection = _make_frame_selection(start=0, stop=1, step=1) dt.build_conformational_states( data_container=MagicMock(), levels=levels, groups=groups, bin_width=30.0, + frame_selection=frame_selection, progress=progress, ) progress.update.assert_any_call(9, title="Group 1") progress.advance.assert_called_with(9) + assert dt._identify_peaks.call_args.kwargs["frame_selection"] is frame_selection + assert dt._assign_states.call_args.kwargs["frame_selection"] is frame_selection def test_process_dihedral_phi(): @@ -528,3 +601,23 @@ def test_process_dihedral_phi_negative(): assert len(phi_values) == 3 assert phi_values[0] == [0, 357] + + +def test_run_dihedrals_raises_when_no_dihedrals(): + dt = ConformationStateBuilder(universe_operations=MagicMock()) + frame_selection = _make_frame_selection(start=0, stop=2, step=1) + + with pytest.raises( + ValueError, match="Cannot run Dihedral analysis with no dihedrals" + ): + dt._run_dihedrals( + dihedrals=[], + frame_selection=frame_selection, + ) + + +def test_analysis_run_bounds_raises_when_frame_selection_empty(): + frame_selection = FrameSelection(indices=()) + + with pytest.raises(ValueError, match="Frame selection is empty"): + ConformationStateBuilder._analysis_run_bounds(frame_selection) diff --git a/tests/unit/CodeEntropy/levels/test_frame_graph.py b/tests/unit/CodeEntropy/levels/test_frame_graph.py index e4490c01..e1134772 100644 --- a/tests/unit/CodeEntropy/levels/test_frame_graph.py +++ b/tests/unit/CodeEntropy/levels/test_frame_graph.py @@ -1,5 +1,7 @@ from unittest.mock import MagicMock +import pytest + from CodeEntropy.levels.frame_dag import FrameGraph @@ -36,15 +38,50 @@ def _b_run(ctx): b.run.side_effect = _b_run - out = fg.execute_frame(shared_data={"S": 1}, frame_index=3) + frame_source = MagicMock() + shared_data = { + "frame_source": frame_source, + } + + out = fg.execute_frame(shared_data=shared_data, frame_index=3) assert out == {"ok": True} + frame_source.seek.assert_called_once_with(3) + assert a.run.call_count == 1 assert b.run.call_count == 1 + a_ctx = a.run.call_args.args[0] + b_ctx = b.run.call_args.args[0] + + assert a_ctx is b_ctx + assert a_ctx["frame_index"] == 3 + def test_build_adds_frame_covariance_node(): fg = FrameGraph() fg.build() assert "frame_covariance" in fg._nodes assert "frame_covariance" in fg._graph.nodes + + +def test_execute_frame_reraises_index_error_with_analysis_bounds_message(): + fg = FrameGraph() + + frame_source = MagicMock() + frame_source.seek.side_effect = IndexError("bad frame") + frame_source.universe.trajectory = [object(), object()] + + shared_data = { + "frame_source": frame_source, + } + + with pytest.raises( + IndexError, + match="Frame index 5 is outside analysis trajectory bounds for trajectory " + "with 2 frames", + ) as exc_info: + fg.execute_frame(shared_data=shared_data, frame_index=5) + + frame_source.seek.assert_called_once_with(5) + assert isinstance(exc_info.value.__cause__, IndexError) diff --git a/tests/unit/CodeEntropy/levels/test_level_dag_orchestration.py b/tests/unit/CodeEntropy/levels/test_level_dag_orchestration.py index 1895cd08..1238a9eb 100644 --- a/tests/unit/CodeEntropy/levels/test_level_dag_orchestration.py +++ b/tests/unit/CodeEntropy/levels/test_level_dag_orchestration.py @@ -60,26 +60,41 @@ def test_run_static_stage_calls_nodes_in_topological_sort_order(): def test_run_frame_stage_iterates_selected_frames_and_reduces_each(): dag = LevelDAG() - ts0 = MagicMock(frame=10) - ts1 = MagicMock(frame=11) - u = MagicMock() - u.trajectory = [ts0, ts1] + frame_source = MagicMock() + frame_source.iter_indices.return_value = [10, 11] - shared = {"reduced_universe": u, "start": 0, "end": 2, "step": 1, "n_frames": 2} + shared = { + "frame_source": frame_source, + "n_frames": 2, + } - dag._frame_dag = MagicMock() - dag._frame_dag.execute_frame.side_effect = [ + frame_outputs = [ + { + "force": {"ua": {}, "res": {}, "poly": {}}, + "torque": {"ua": {}, "res": {}, "poly": {}}, + }, { "force": {"ua": {}, "res": {}, "poly": {}}, "torque": {"ua": {}, "res": {}, "poly": {}}, - } - ] * 2 + }, + ] + + dag._frame_dag = MagicMock() + dag._frame_dag.execute_frame.side_effect = frame_outputs dag._reduce_one_frame = MagicMock() dag._run_frame_stage(shared) + assert shared["n_frames"] == 2 + frame_source.iter_indices.assert_called_once() + assert dag._frame_dag.execute_frame.call_count == 2 + dag._frame_dag.execute_frame.assert_any_call(shared, 10) + dag._frame_dag.execute_frame.assert_any_call(shared, 11) + assert dag._reduce_one_frame.call_count == 2 + dag._reduce_one_frame.assert_any_call(shared, frame_outputs[0]) + dag._reduce_one_frame.assert_any_call(shared, frame_outputs[1]) def test_incremental_mean_handles_non_copyable_values(): @@ -289,18 +304,21 @@ def run(self, shared_data): def test_run_frame_stage_with_progress_creates_task_and_updates_titles(): dag = LevelDAG() - ts0 = MagicMock(frame=10) - ts1 = MagicMock(frame=11) - u = MagicMock() - u.trajectory = [ts0, ts1] + frame_source = MagicMock() + frame_source.iter_indices.return_value = [10, 11] - shared = {"reduced_universe": u, "start": 0, "end": 2, "step": 1, "n_frames": 2} + shared = { + "frame_source": frame_source, + "n_frames": 2, + } - dag._frame_dag = MagicMock() - dag._frame_dag.execute_frame.return_value = { + frame_out = { "force": {"ua": {}, "res": {}, "poly": {}}, "torque": {"ua": {}, "res": {}, "poly": {}}, } + + dag._frame_dag = MagicMock() + dag._frame_dag.execute_frame.return_value = frame_out dag._reduce_one_frame = MagicMock() progress = MagicMock() @@ -308,27 +326,45 @@ def test_run_frame_stage_with_progress_creates_task_and_updates_titles(): dag._run_frame_stage(shared, progress=progress) - progress.add_task.assert_called_once() + progress.add_task.assert_called_once_with( + "[green]Frame processing", + total=2, + title="Initializing", + ) + + assert progress.update.call_count == 2 + progress.update.assert_any_call(77, title="Frame 10") + progress.update.assert_any_call(77, title="Frame 11") + assert progress.advance.call_count == 2 + progress.advance.assert_any_call(77) + + assert dag._frame_dag.execute_frame.call_count == 2 + dag._frame_dag.execute_frame.assert_any_call(shared, 10) + dag._frame_dag.execute_frame.assert_any_call(shared, 11) + + assert dag._reduce_one_frame.call_count == 2 + dag._reduce_one_frame.assert_any_call(shared, frame_out) -def test_run_frame_stage_with_negative_end_computes_total_frames(): +def test_run_frame_stage_progress_total_comes_from_frame_source_indices(): dag = LevelDAG() - ts_list = [MagicMock(frame=i) for i in range(10)] - u = MagicMock() - u.trajectory = ts_list + frame_source = MagicMock() + frame_source.iter_indices.return_value = list(range(10)) shared = { - "reduced_universe": u, - "n_frames": 10, + "frame_source": frame_source, + "n_frames": 0, } - dag._frame_dag = MagicMock() - dag._frame_dag.execute_frame.return_value = { + frame_out = { "force": {"ua": {}, "res": {}, "poly": {}}, "torque": {"ua": {}, "res": {}, "poly": {}}, } + + dag._frame_dag = MagicMock() + dag._frame_dag.execute_frame.return_value = frame_out dag._reduce_one_frame = MagicMock() progress = MagicMock() @@ -336,8 +372,15 @@ def test_run_frame_stage_with_negative_end_computes_total_frames(): dag._run_frame_stage(shared, progress=progress) - progress.add_task.assert_called_once() - _, kwargs = progress.add_task.call_args - assert kwargs["total"] == 10 + progress.add_task.assert_called_once_with( + "[green]Frame processing", + total=10, + title="Initializing", + ) + + assert shared["n_frames"] == 10 + frame_source.iter_indices.assert_called_once() + assert dag._frame_dag.execute_frame.call_count == 10 + assert dag._reduce_one_frame.call_count == 10 assert progress.advance.call_count == 10 diff --git a/tests/unit/CodeEntropy/levels/test_level_dag_reduction.py b/tests/unit/CodeEntropy/levels/test_level_dag_reduction.py index 9ad31523..68e981e9 100644 --- a/tests/unit/CodeEntropy/levels/test_level_dag_reduction.py +++ b/tests/unit/CodeEntropy/levels/test_level_dag_reduction.py @@ -69,23 +69,38 @@ def test_reduce_forcetorque_no_key_is_noop(): assert shared["forcetorque_covariances"]["res"][0] is None -def test_run_frame_stage_calls_execute_frame_for_each_ts(simple_ts_list): +def test_run_frame_stage_calls_execute_frame_for_each_frame_index(): dag = LevelDAG() - u = MagicMock() - u.trajectory = simple_ts_list + frame_source = MagicMock() + frame_source.iter_indices.return_value = list(range(10)) - shared = {"reduced_universe": u, "start": 0, "end": 10, "step": 1, "n_frames": 10} + shared = { + "frame_source": frame_source, + "n_frames": 10, + } - dag._frame_dag = MagicMock() - dag._frame_dag.execute_frame.side_effect = lambda shared_data, frame_index: { + frame_out = { "force": {"ua": {}, "res": {}, "poly": {}}, "torque": {"ua": {}, "res": {}, "poly": {}}, } + dag._frame_dag = MagicMock() + dag._frame_dag.execute_frame.side_effect = lambda shared_data, frame_index: ( + frame_out + ) + dag._reduce_one_frame = MagicMock() dag._run_frame_stage(shared) + frame_source.iter_indices.assert_called_once() + assert dag._frame_dag.execute_frame.call_count == 10 assert dag._reduce_one_frame.call_count == 10 + + for frame_index in range(10): + dag._frame_dag.execute_frame.assert_any_call(shared, frame_index) + + for call in dag._reduce_one_frame.call_args_list: + assert call.args == (shared, frame_out) diff --git a/tests/unit/CodeEntropy/levels/test_mda_universe_operations.py b/tests/unit/CodeEntropy/levels/test_mda_universe_operations.py index 5fb16094..e8d92329 100644 --- a/tests/unit/CodeEntropy/levels/test_mda_universe_operations.py +++ b/tests/unit/CodeEntropy/levels/test_mda_universe_operations.py @@ -1,11 +1,10 @@ -from types import SimpleNamespace -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock import numpy as np import pytest from MDAnalysis.exceptions import NoDataError -from CodeEntropy.levels.mda import UniverseOperations +from CodeEntropy.trajectory.mda import UniverseOperations class _FakeAF: @@ -21,10 +20,30 @@ def run(self): return self +class _FakeTrajectory: + def __init__(self, n_frames: int): + self.n_frames = n_frames + self.seen = [] + + def __len__(self): + return self.n_frames + + def __getitem__(self, frame_index): + self.seen.append(frame_index) + return frame_index + + def test_extract_timeseries_unknown_kind_raises(): ops = UniverseOperations() - with pytest.raises(ValueError): - ops._extract_timeseries(MagicMock(), kind="nope") + + universe = MagicMock() + universe.trajectory = _FakeTrajectory(0) + + ag = MagicMock() + ag.universe = universe + + with pytest.raises(ValueError, match="Unknown timeseries kind"): + ops._extract_timeseries(ag, kind="nope") def test_extract_force_timeseries_fallback_to_positions_when_no_forces(): @@ -58,25 +77,24 @@ def test_select_frames_defaults_start_end_and_slices(monkeypatch): ops = UniverseOperations() u = MagicMock() - u.trajectory = list(range(10)) - u.select_atoms.return_value = MagicMock() + u.trajectory = _FakeTrajectory(10) + u.dimensions = np.array([10.0, 10.0, 10.0, 90.0, 90.0, 90.0]) - # timeseries arrays - ops._extract_timeseries = MagicMock( - side_effect=[ - np.zeros((10, 2, 3)), # positions - np.ones((10, 2, 3)), # forces - np.zeros((10, 6)), # dimensions - ] - ) + select_atom = MagicMock() + select_atom.universe = u + select_atom.positions = np.zeros((2, 3)) + select_atom.forces = np.ones((2, 3)) + u.select_atoms.return_value = select_atom merged = MagicMock() merged.load_new = MagicMock() - monkeypatch.setattr("CodeEntropy.levels.mda.mda.Merge", lambda ag: merged) + monkeypatch.setattr("CodeEntropy.trajectory.mda.mda.Merge", lambda ag: merged) out = ops.select_frames(u, start=None, end=None, step=2) assert out is merged + u.select_atoms.assert_called_once_with("all", updating=True) + assert u.trajectory.seen == [0, 2, 4, 6, 8] merged.load_new.assert_called_once() @@ -89,7 +107,7 @@ def test_merge_forces_scales_kcal(monkeypatch): u_force.select_atoms.return_value = MagicMock() monkeypatch.setattr( - "CodeEntropy.levels.mda.mda.Universe", MagicMock(side_effect=[u, u_force]) + "CodeEntropy.trajectory.mda.mda.Universe", MagicMock(side_effect=[u, u_force]) ) ops._extract_timeseries = MagicMock( @@ -104,7 +122,7 @@ def test_merge_forces_scales_kcal(monkeypatch): merged = MagicMock() merged.load_new = MagicMock() - monkeypatch.setattr("CodeEntropy.levels.mda.mda.Merge", lambda ag: merged) + monkeypatch.setattr("CodeEntropy.trajectory.mda.mda.Merge", lambda ag: merged) out = ops.merge_forces( tprfile="tpr", @@ -129,7 +147,7 @@ def capture_universe(*args, **kwargs): transformations_captured.extend(kwargs["transformations"]) return mock_universe - monkeypatch.setattr("CodeEntropy.levels.mda.mda.Universe", capture_universe) + monkeypatch.setattr("CodeEntropy.trajectory.mda.mda.Universe", capture_universe) ops.convert_lammps("tpr", "trr", "LAMMPSDUMP") @@ -158,7 +176,7 @@ def mock_universe(*args, **kwargs): transformations_captured.extend(kwargs["transformations"]) return MagicMock() - monkeypatch.setattr("CodeEntropy.levels.mda.mda.Universe", mock_universe) + monkeypatch.setattr("CodeEntropy.trajectory.mda.mda.Universe", mock_universe) ops.convert_lammps("tpr", "trr", "LAMMPSDUMP") @@ -175,26 +193,29 @@ def test_select_atoms_builds_merged_universe_and_loads_timeseries(monkeypatch): ops = UniverseOperations() u = MagicMock() + u.trajectory = _FakeTrajectory(2) + sel = MagicMock() - u.select_atoms.return_value = sel + sel.universe = u + sel.positions = np.zeros((3, 3)) + sel.forces = np.ones((3, 3)) - monkeypatch.setattr( - ops, - "_extract_timeseries", - lambda _sel, kind: np.zeros((2, 3)) if kind == "positions" else np.zeros((2,)), - ) + u.dimensions = np.array([10.0, 10.0, 10.0, 90.0, 90.0, 90.0]) + u.select_atoms.return_value = sel merged = MagicMock() - with ( - patch("CodeEntropy.levels.mda.mda.Merge", return_value=merged) as MergeCls, - patch("CodeEntropy.levels.mda.MemoryReader"), - ): - out = ops.select_atoms(u, "name CA") + merged.load_new = MagicMock() + + monkeypatch.setattr("CodeEntropy.trajectory.mda.mda.Merge", lambda ag: merged) + + out = ops.select_atoms(u, "name CA") + assert out is merged u.select_atoms.assert_called_once_with("name CA", updating=True) - MergeCls.assert_called_once_with(sel) merged.load_new.assert_called_once() - assert out is merged + + coordinates = merged.load_new.call_args.args[0] + assert coordinates.shape == (2, 3, 3) def test_extract_fragment_selects_by_resindices(monkeypatch): @@ -218,53 +239,171 @@ def test_extract_fragment_selects_by_resindices(monkeypatch): def test_extract_timeseries_kind_positions_returns_xyz_array(): uops = UniverseOperations() + universe = MagicMock() + universe.trajectory = _FakeTrajectory(2) + ag = MagicMock() + ag.universe = universe ag.positions = np.array([[1.0, 2.0, 3.0]], dtype=float) - class _FakeAnalysisFromFunction: - def __init__(self, func, atomgroup): - self.func = func - self.atomgroup = atomgroup - - def run(self): - return SimpleNamespace(results={"timeseries": self.func(self.atomgroup)}) + out = uops._extract_timeseries(atomgroup=ag, kind="positions") - with patch( - "CodeEntropy.levels.mda.AnalysisFromFunction", _FakeAnalysisFromFunction - ): - out = uops._extract_timeseries(atomgroup=ag, kind="positions") - - assert out.shape == (1, 3) - assert np.allclose(out, np.array([[1.0, 2.0, 3.0]])) + assert out.shape == (2, 1, 3) + assert np.allclose(out[0], np.array([[1.0, 2.0, 3.0]])) + assert universe.trajectory.seen == [0, 1] def test_extract_timeseries_invalid_kind_raises_value_error(): uops = UniverseOperations() + + universe = MagicMock() + universe.trajectory = _FakeTrajectory(0) + ag = MagicMock() + ag.universe = universe - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Unknown timeseries kind"): uops._extract_timeseries(atomgroup=ag, kind="not-a-kind") def test_extract_timeseries_forces_branch_uses_forces_copy(): uops = UniverseOperations() + universe = MagicMock() + universe.trajectory = _FakeTrajectory(2) + ag = MagicMock() + ag.universe = universe ag.forces = np.array([[1.0, 2.0, 3.0]], dtype=float) - with patch("CodeEntropy.levels.mda.AnalysisFromFunction", _FakeAF): - out = uops._extract_timeseries(ag, kind="forces") + out = uops._extract_timeseries(ag, kind="forces") - assert np.allclose(out, ag.forces) + assert out.shape == (2, 1, 3) + assert np.allclose(out[0], ag.forces) + assert universe.trajectory.seen == [0, 1] def test_extract_timeseries_dimensions_branch_uses_dimensions_copy(): uops = UniverseOperations() + universe = MagicMock() + universe.trajectory = _FakeTrajectory(2) + universe.dimensions = np.array([10.0, 10.0, 10.0, 90, 90, 90], dtype=float) + ag = MagicMock() - ag.dimensions = np.array([10.0, 10.0, 10.0, 90, 90, 90], dtype=float) + ag.universe = universe - with patch("CodeEntropy.levels.mda.AnalysisFromFunction", _FakeAF): - out = uops._extract_timeseries(ag, kind="dimensions") + out = uops._extract_timeseries(ag, kind="dimensions") + + assert out.shape == (2, 6) + assert np.allclose(out[0], universe.dimensions) + assert universe.trajectory.seen == [0, 1] + + +def test_select_frames_raises_when_step_is_zero(): + ops = UniverseOperations() + + u = MagicMock() + u.trajectory = list(range(5)) + + with pytest.raises(ValueError, match="Frame step must be positive, got 0"): + ops.select_frames(u, start=0, end=5, step=0) + + u.select_atoms.assert_not_called() + + +def test_select_frames_raises_when_step_is_negative(): + ops = UniverseOperations() + + u = MagicMock() + u.trajectory = list(range(5)) + + with pytest.raises(ValueError, match="Frame step must be positive, got -1"): + ops.select_frames(u, start=0, end=5, step=-1) + + u.select_atoms.assert_not_called() + + +def test_build_memory_universe_from_atomgroup_raises_when_frame_indices_empty(): + ops = UniverseOperations() + + atomgroup = MagicMock() + + with pytest.raises( + ValueError, + match="Cannot build a memory universe from an empty frame list", + ): + ops._build_memory_universe_from_atomgroup(atomgroup, frame_indices=[]) + + +def test_build_memory_universe_from_atomgroup_omits_forces_when_force_data_missing( + monkeypatch, +): + ops = UniverseOperations() + + class _FakeTrajectory: + def __init__(self): + self.seen = [] + + def __getitem__(self, frame_index): + self.seen.append(frame_index) + return frame_index + + class _FakeAtomGroup: + def __init__(self, universe): + self.universe = universe + self.positions = np.array([[1.0, 2.0, 3.0]], dtype=float) + + @property + def forces(self): + raise NoDataError("no forces") + + universe = MagicMock() + universe.trajectory = _FakeTrajectory() + universe.dimensions = np.array([10.0, 10.0, 10.0, 90.0, 90.0, 90.0]) + + atomgroup = _FakeAtomGroup(universe) + + merged = MagicMock() + merged.load_new = MagicMock() + + monkeypatch.setattr("CodeEntropy.trajectory.mda.mda.Merge", lambda ag: merged) + + out = ops._build_memory_universe_from_atomgroup(atomgroup, frame_indices=[0, 2]) + + assert out is merged + assert universe.trajectory.seen == [0, 2] + + merged.load_new.assert_called_once() + _, kwargs = merged.load_new.call_args + + assert "forces" not in kwargs + assert "dimensions" in kwargs + + +def test_convert_lammps_raises_for_non_lammpsdump_format(): + ops = UniverseOperations() + + with pytest.raises( + ValueError, + match="Incorrect file format: TRR, LAMMPSDUMP expected", + ): + ops.convert_lammps( + tprfile="topology.tpr", + trrfile="trajectory.trr", + fileformat="TRR", + ) + + +def test_select_frame_indices_raises_when_frame_indices_empty(): + ops = UniverseOperations() + + u = MagicMock() + + with pytest.raises( + ValueError, + match="Cannot build a reduced universe from an empty frame list", + ): + ops.select_frame_indices(u, frame_indices=[]) - assert np.allclose(out, ag.dimensions) + u.select_atoms.assert_not_called() diff --git a/tests/unit/CodeEntropy/levels/test_neighbors.py b/tests/unit/CodeEntropy/levels/test_neighbors.py index c7211d5e..f8814267 100644 --- a/tests/unit/CodeEntropy/levels/test_neighbors.py +++ b/tests/unit/CodeEntropy/levels/test_neighbors.py @@ -26,71 +26,124 @@ def _fake_progress_bar(*_args, **_kwargs): yield _FakeProgress() +def _make_frame_source(indices): + frame_source = MagicMock() + frame_source.iter_indices.return_value = list(indices) + return frame_source + + def test_raises_error_unknown_search_type(): neighbors = Neighbors() universe = MagicMock() - universe.trajectory.__len__.return_value = 2 levels = {0: ["united_atom"]} groups = {0: [0]} - n_frames = 2 - search_type = "weird" - - with pytest.raises(ValueError): - neighbors.get_neighbors(universe, levels, groups, n_frames, search_type) + frame_source = _make_frame_source([0, 1]) + + with pytest.raises(ValueError, match="unknown search_type"): + neighbors.get_neighbors( + universe=universe, + levels=levels, + groups=groups, + frame_source=frame_source, + search_type="weird", + ) def test_average_number_neighbors_RAD(): neighbors = Neighbors() universe = MagicMock() - universe.trajectory.__len__.return_value = 2 levels = {0: ["united_atom"]} groups = {0: [0]} - n_frames = 2 - search_type = "RAD" + frame_source = _make_frame_source([0, 1]) neighbors._search.get_RAD_neighbors = MagicMock(side_effect=[[1, 2, 3], [1, 3]]) - result = neighbors.get_neighbors(universe, levels, groups, n_frames, search_type) + result = neighbors.get_neighbors( + universe=universe, + levels=levels, + groups=groups, + frame_source=frame_source, + search_type="RAD", + ) assert result == {0: np.float64(2.5)} + assert neighbors._search.get_RAD_neighbors.call_args_list == [ + call( + universe=universe, + mol_id=0, + frame_source=frame_source, + frame_index=0, + ), + call( + universe=universe, + mol_id=0, + frame_source=frame_source, + frame_index=1, + ), + ] def test_average_number_neighbors_grid(): neighbors = Neighbors() universe = MagicMock() - universe.trajectory.__len__.return_value = 2 levels = {0: ["united_atom"]} groups = {0: [0]} - n_frames = 2 - search_type = "grid" + frame_source = _make_frame_source([0, 1]) neighbors._search.get_grid_neighbors = MagicMock(side_effect=[[1, 2, 3], [1, 3]]) - result = neighbors.get_neighbors(universe, levels, groups, n_frames, search_type) + result = neighbors.get_neighbors( + universe=universe, + levels=levels, + groups=groups, + frame_source=frame_source, + search_type="grid", + ) assert result == {0: np.float64(2.5)} + assert neighbors._search.get_grid_neighbors.call_args_list == [ + call( + universe=universe, + mol_id=0, + highest_level="united_atom", + frame_source=frame_source, + frame_index=0, + ), + call( + universe=universe, + mol_id=0, + highest_level="united_atom", + frame_source=frame_source, + frame_index=1, + ), + ] def test_average_number_neighbors_RAD_multiple(): neighbors = Neighbors() universe = MagicMock() - universe.trajectory.__len__.return_value = 2 levels = {0: ["united_atom"]} groups = {0: [0, 1]} - n_frames = 2 - search_type = "RAD" + frame_source = _make_frame_source([0, 1]) neighbors._search.get_RAD_neighbors = MagicMock( side_effect=[[1, 2, 3, 5], [1, 3], [2, 3, 4, 5], [3, 5]] ) - result = neighbors.get_neighbors(universe, levels, groups, n_frames, search_type) + result = neighbors.get_neighbors( + universe=universe, + levels=levels, + groups=groups, + frame_source=frame_source, + search_type="RAD", + ) assert result == {0: np.float64(3.0)} + assert neighbors._search.get_RAD_neighbors.call_count == 4 def test_get_symmetry_number_res(): @@ -491,3 +544,76 @@ def test_get_rdkit_mol_returns_correct_heavy_and_hydrogen_counts(): assert rdkit_out is rdkit_mol assert number_heavy == 1 assert number_hydrogen == 2 + + +def test_get_neighbors_returns_zero_for_each_group_when_no_frames_selected(): + neighbors = Neighbors() + + universe = MagicMock() + levels = { + 0: ["united_atom"], + 1: ["residue"], + } + groups = { + 0: [0], + 1: [1], + } + + frame_source = MagicMock() + frame_source.iter_indices.return_value = [] + + neighbors._search.get_RAD_neighbors = MagicMock() + neighbors._search.get_grid_neighbors = MagicMock() + + result = neighbors.get_neighbors( + universe=universe, + levels=levels, + groups=groups, + frame_source=frame_source, + search_type="RAD", + ) + + assert result == { + 0: 0.0, + 1: 0.0, + } + + frame_source.iter_indices.assert_called_once() + neighbors._search.get_RAD_neighbors.assert_not_called() + neighbors._search.get_grid_neighbors.assert_not_called() + + +def test_get_rdkit_mol_falls_back_to_inferrer_none_when_convert_to_raises(): + neighbors = Neighbors() + + universe = MagicMock() + molecule = MagicMock() + dummy = MagicMock() + + universe.atoms.elements = ["C", "H"] + universe.atoms.fragments = [molecule] + + molecule.select_atoms.return_value = dummy + dummy.__len__.return_value = 0 + + rdkit_mol = MagicMock() + rdkit_mol.GetNumHeavyAtoms.return_value = 1 + rdkit_mol.GetNumAtoms.return_value = 4 + + molecule.convert_to.side_effect = [ + RuntimeError("constraint bond issue"), + rdkit_mol, + ] + + result = neighbors._get_rdkit_mol(universe, mol_id=0) + + assert result == (rdkit_mol, 1, 3) + + molecule.select_atoms.assert_called_once_with("prop mass < 0.1") + molecule.convert_to.assert_has_calls( + [ + call("RDKIT", force=True), + call("RDKIT", force=True, inferrer=None), + ] + ) + universe.guess_TopologyAttrs.assert_not_called() diff --git a/tests/unit/CodeEntropy/levels/test_search.py b/tests/unit/CodeEntropy/levels/test_search.py index 65a9ec00..820a6b3f 100644 --- a/tests/unit/CodeEntropy/levels/test_search.py +++ b/tests/unit/CodeEntropy/levels/test_search.py @@ -11,6 +11,12 @@ def search(): return Search() +def _make_frame_source(): + frame_source = MagicMock() + frame_source.seek = MagicMock() + return frame_source + + def test_apply_pbc_wraps_positive(): vec = np.array([11.0, 0.0, 0.0]) dimensions = np.array([10.0, 10.0, 10.0]) @@ -50,7 +56,6 @@ def test_get_distances_applies_pbc(search): def test_update_cache_initializes_and_skips_on_same_frame(search): universe = MagicMock() - universe.trajectory.ts.frame = 0 universe.dimensions = np.array([10.0, 10.0, 10.0]) frag1 = MagicMock() @@ -61,15 +66,17 @@ def test_update_cache_initializes_and_skips_on_same_frame(search): universe.atoms.fragments = [frag1, frag2] - search._update_cache(universe) + search._update_cache(universe, frame_index=0) assert search._cached_frame == 0 assert search._cached_coms.shape == (2, 3) old = search._cached_coms.copy() - search._update_cache(universe) + search._update_cache(universe, frame_index=0) assert np.array_equal(old, search._cached_coms) + assert frag1.center_of_mass.call_count == 1 + assert frag2.center_of_mass.call_count == 1 def test_update_cache_updates_on_new_frame(search): @@ -78,21 +85,17 @@ def test_update_cache_updates_on_new_frame(search): frag = MagicMock() frag.center_of_mass.return_value = np.array([0.0, 0.0, 0.0]) universe.atoms.fragments = [frag] - universe.dimensions = np.array([10.0, 10.0, 10.0]) - universe.trajectory.ts.frame = 0 - search._update_cache(universe) - - universe.trajectory.ts.frame = 1 - search._update_cache(universe) + search._update_cache(universe, frame_index=0) + search._update_cache(universe, frame_index=1) assert search._cached_frame == 1 + assert frag.center_of_mass.call_count == 2 def test_get_RAD_neighbors_returns_array(search): universe = MagicMock() - universe.trajectory.ts.frame = 0 universe.dimensions = np.array([10.0, 10.0, 10.0]) frag1 = MagicMock() @@ -104,15 +107,21 @@ def test_get_RAD_neighbors_returns_array(search): frag3.center_of_mass.return_value = np.array([2.0, 0.0, 0.0]) universe.atoms.fragments = [frag1, frag2, frag3] + frame_source = _make_frame_source() - result = search.get_RAD_neighbors(universe, mol_id=0, timestep=0) + result = search.get_RAD_neighbors( + universe=universe, + mol_id=0, + frame_source=frame_source, + frame_index=0, + ) + frame_source.seek.assert_called_once_with(0) assert isinstance(result, np.ndarray) def test_rad_pbc_path_triggers_wrapping(search): universe = MagicMock() - universe.trajectory.ts.frame = 0 universe.dimensions = np.array([10.0, 10.0, 10.0]) frag1 = MagicMock() @@ -122,9 +131,16 @@ def test_rad_pbc_path_triggers_wrapping(search): frag2.center_of_mass.return_value = np.array([9.5, 0.0, 0.0]) universe.atoms.fragments = [frag1, frag2] + frame_source = _make_frame_source() - result = search.get_RAD_neighbors(universe, mol_id=0, timestep=0) + result = search.get_RAD_neighbors( + universe=universe, + mol_id=0, + frame_source=frame_source, + frame_index=0, + ) + frame_source.seek.assert_called_once_with(0) assert isinstance(result, np.ndarray) @@ -133,7 +149,6 @@ def test_get_grid_neighbors_united_atom(search): fragment = MagicMock() fragment.indices = [10, 11] - universe.atoms.fragments = [fragment] molecule_atom_group = MagicMock() @@ -142,24 +157,27 @@ def test_get_grid_neighbors_united_atom(search): search_result = MagicMock() diff_result = MagicMock() diff_result.fragindices = np.array([1, 2]) - search_result.__sub__.return_value = diff_result + frame_source = _make_frame_source() + with patch( "CodeEntropy.levels.search.mda.lib.NeighborSearch.AtomNeighborSearch.search", autospec=True, return_value=search_result, ) as mock_search: result = search.get_grid_neighbors( - universe, + universe=universe, mol_id=0, highest_level="united_atom", - timestep=0, + frame_source=frame_source, + frame_index=0, ) - mock_search.assert_called_once() - universe.select_atoms.assert_called_once_with("index 10:11") - assert np.array_equal(result, np.array([1, 2])) + frame_source.seek.assert_called_once_with(0) + mock_search.assert_called_once() + universe.select_atoms.assert_called_once_with("index 10:11") + assert np.array_equal(result, np.array([1, 2])) def test_get_grid_neighbors_residue(search): @@ -168,7 +186,6 @@ def test_get_grid_neighbors_residue(search): fragment = MagicMock() fragment.indices = [4, 5, 6] fragment.residues = MagicMock() - universe.atoms.fragments = [fragment] molecule_atom_group = MagicMock() @@ -178,24 +195,27 @@ def test_get_grid_neighbors_residue(search): diff_result = MagicMock() diff_result.atoms = MagicMock() diff_result.atoms.fragindices = np.array([7, 8, 9]) - search_result.__sub__.return_value = diff_result + frame_source = _make_frame_source() + with patch( "CodeEntropy.levels.search.mda.lib.NeighborSearch.AtomNeighborSearch.search", autospec=True, return_value=search_result, ) as mock_search: result = search.get_grid_neighbors( - universe, + universe=universe, mol_id=0, highest_level="other", - timestep=0, + frame_source=frame_source, + frame_index=0, ) - mock_search.assert_called_once() - universe.select_atoms.assert_called_once_with("index 4:6") - assert np.array_equal(result, np.array([7, 8, 9])) + frame_source.seek.assert_called_once_with(0) + mock_search.assert_called_once() + universe.select_atoms.assert_called_once_with("index 4:6") + assert np.array_equal(result, np.array([7, 8, 9])) def test_get_grid_neighbors_selection_string(search): @@ -203,22 +223,25 @@ def test_get_grid_neighbors_selection_string(search): fragment = MagicMock() fragment.indices = [3, 7] - universe.atoms.fragments = [fragment] universe.select_atoms.return_value = MagicMock() + frame_source = _make_frame_source() + with patch( "CodeEntropy.levels.search.mda.lib.NeighborSearch.AtomNeighborSearch.search", autospec=True, return_value=MagicMock(), ): search.get_grid_neighbors( - universe, + universe=universe, mol_id=0, highest_level="united_atom", - timestep=0, + frame_source=frame_source, + frame_index=0, ) + frame_source.seek.assert_called_once_with(0) universe.select_atoms.assert_called_once_with("index 3:7") diff --git a/tests/unit/CodeEntropy/trajectory/test_frames.py b/tests/unit/CodeEntropy/trajectory/test_frames.py new file mode 100644 index 00000000..625898c1 --- /dev/null +++ b/tests/unit/CodeEntropy/trajectory/test_frames.py @@ -0,0 +1,147 @@ +import pytest + +from CodeEntropy.trajectory.frames import FrameSelection + + +def test_from_bounds_uses_python_range_semantics(): + selection = FrameSelection.from_bounds(start=2, stop=10, step=3) + + assert selection.indices == (2, 5, 8) + + +def test_from_bounds_casts_bounds_to_ints(): + selection = FrameSelection.from_bounds(start=0.0, stop=5.0, step=2.0) + + assert selection.indices == (0, 2, 4) + + +def test_from_bounds_rejects_zero_step(): + with pytest.raises(ValueError, match="Frame step must be positive"): + FrameSelection.from_bounds(start=0, stop=10, step=0) + + +def test_from_bounds_rejects_negative_step(): + with pytest.raises(ValueError, match="Frame step must be positive"): + FrameSelection.from_bounds(start=10, stop=0, step=-1) + + +def test_len_returns_number_of_selected_frames(): + selection = FrameSelection(indices=(0, 2, 4)) + + assert len(selection) == 3 + + +def test_iter_yields_absolute_frame_indices(): + selection = FrameSelection(indices=(3, 6, 9)) + + assert list(selection) == [3, 6, 9] + + +def test_n_frames_matches_len(): + selection = FrameSelection(indices=(1, 4, 7)) + + assert selection.n_frames == 3 + + +def test_source_indices_aliases_indices(): + selection = FrameSelection(indices=(1, 2, 3)) + + assert selection.source_indices is selection.indices + + +def test_analysis_indices_aliases_indices(): + selection = FrameSelection(indices=(1, 2, 3)) + + assert selection.analysis_indices is selection.indices + + +def test_source_start_returns_first_frame(): + selection = FrameSelection(indices=(5, 10, 15)) + + assert selection.source_start == 5 + + +def test_source_start_returns_none_for_empty_selection(): + selection = FrameSelection(indices=()) + + assert selection.source_start is None + + +def test_source_stop_exclusive_returns_one_past_last_frame(): + selection = FrameSelection(indices=(5, 10, 15)) + + assert selection.source_stop_exclusive == 16 + + +def test_source_stop_exclusive_returns_none_for_empty_selection(): + selection = FrameSelection(indices=()) + + assert selection.source_stop_exclusive is None + + +def test_iter_indices_yields_indices(): + selection = FrameSelection(indices=(2, 4, 6)) + + assert list(selection.iter_indices()) == [2, 4, 6] + + +def test_iter_source_indices_yields_indices(): + selection = FrameSelection(indices=(2, 4, 6)) + + assert list(selection.iter_source_indices()) == [2, 4, 6] + + +def test_iter_analysis_indices_yields_indices(): + selection = FrameSelection(indices=(2, 4, 6)) + + assert list(selection.iter_analysis_indices()) == [2, 4, 6] + + +def test_iter_pairs_yields_local_and_absolute_indices(): + selection = FrameSelection(indices=(10, 20, 30)) + + assert list(selection.iter_pairs()) == [(0, 10), (1, 20), (2, 30)] + + +def test_infer_step_returns_one_for_empty_selection(): + selection = FrameSelection(indices=()) + + assert selection.infer_step() == 1 + + +def test_infer_step_returns_one_for_single_frame_selection(): + selection = FrameSelection(indices=(7,)) + + assert selection.infer_step() == 1 + + +def test_infer_step_returns_regular_stride(): + selection = FrameSelection(indices=(2, 5, 8, 11)) + + assert selection.infer_step() == 3 + + +def test_infer_step_rejects_non_increasing_indices(): + selection = FrameSelection(indices=(4, 4, 5)) + + with pytest.raises(ValueError, match="strictly increasing"): + selection.infer_step() + + +def test_infer_step_rejects_irregular_indices(): + selection = FrameSelection(indices=(0, 2, 5)) + + with pytest.raises(ValueError, match="not regularly spaced"): + selection.infer_step() + + +def test_infer_source_step_delegates_to_infer_step(): + selection = FrameSelection(indices=(1, 4, 7)) + + assert selection.infer_source_step() == 3 + + +def test_infer_analysis_step_delegates_to_infer_step(): + selection = FrameSelection(indices=(1, 4, 7)) + + assert selection.infer_analysis_step() == 3 diff --git a/tests/unit/CodeEntropy/trajectory/test_source.py b/tests/unit/CodeEntropy/trajectory/test_source.py new file mode 100644 index 00000000..e94a8685 --- /dev/null +++ b/tests/unit/CodeEntropy/trajectory/test_source.py @@ -0,0 +1,80 @@ +from unittest.mock import MagicMock + +from CodeEntropy.trajectory.frames import FrameSelection +from CodeEntropy.trajectory.source import FrameSource + + +def test_len_returns_selection_length(): + selection = FrameSelection(indices=(0, 2, 4)) + source = FrameSource(universe=MagicMock(), selection=selection) + + assert len(source) == 3 + + +def test_iter_indices_delegates_to_selection_indices(): + selection = FrameSelection(indices=(1, 3, 5)) + source = FrameSource(universe=MagicMock(), selection=selection) + + assert list(source.iter_indices()) == [1, 3, 5] + + +def test_iter_source_indices_delegates_to_selection_source_indices(): + selection = FrameSelection(indices=(1, 3, 5)) + source = FrameSource(universe=MagicMock(), selection=selection) + + assert list(source.iter_source_indices()) == [1, 3, 5] + + +def test_iter_pairs_delegates_to_selection_pairs(): + selection = FrameSelection(indices=(10, 20)) + source = FrameSource(universe=MagicMock(), selection=selection) + + assert list(source.iter_pairs()) == [(0, 10), (1, 20)] + + +def test_seek_uses_absolute_trajectory_index(): + timestep = object() + + universe = MagicMock() + universe.trajectory = MagicMock() + universe.trajectory.__getitem__.return_value = timestep + + selection = FrameSelection(indices=(2, 4, 6)) + source = FrameSource(universe=universe, selection=selection) + + out = source.seek(4) + + assert out is timestep + universe.trajectory.__getitem__.assert_called_once_with(4) + + +def test_seek_casts_frame_index_to_int(): + timestep = object() + + universe = MagicMock() + universe.trajectory = MagicMock() + universe.trajectory.__getitem__.return_value = timestep + + selection = FrameSelection(indices=(0, 1, 2)) + source = FrameSource(universe=universe, selection=selection) + + out = source.seek("2") + + assert out is timestep + universe.trajectory.__getitem__.assert_called_once_with(2) + + +def test_seek_allows_underlying_trajectory_to_raise_index_error(): + universe = MagicMock() + universe.trajectory = MagicMock() + universe.trajectory.__getitem__.side_effect = IndexError("bad frame") + + selection = FrameSelection(indices=(0, 1)) + source = FrameSource(universe=universe, selection=selection) + + try: + source.seek(99) + except IndexError as exc: + assert str(exc) == "bad frame" + else: + raise AssertionError("Expected IndexError")