From 1666b872af3b7e74b60712dfd32ed168adf36de2 Mon Sep 17 00:00:00 2001 From: Louis Smith <> Date: Wed, 13 May 2026 18:58:57 -0400 Subject: [PATCH 1/5] Optionally, trajectories update only a 'target' AG; default behavior stands --- loos/src/loos/pyloos/trajectories.py | 36 ++++++++++++++++++---------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/loos/src/loos/pyloos/trajectories.py b/loos/src/loos/pyloos/trajectories.py index 5820e7696..ba069d1a5 100644 --- a/loos/src/loos/pyloos/trajectories.py +++ b/loos/src/loos/pyloos/trajectories.py @@ -11,12 +11,13 @@ # a loos::Trajectory. The behavior of the trajectory can be controlled # through passed keywords, # -# Keyword | Description -# -----------|------------------------------------------------------------------------------ -# skip=n | Skip the first n-frames of the wrapped trajectory -# stride=n | Step through the wrapped trajectory n-frames at a time -# iterator=i | Use the python iterator object i to select frames from the wrapped trajectory -# subset=s | Use 's' to select a subset of the model to use for each frame +# Keyword | Description +# --------------|------------------------------------------------------------------------------ +# skip=n | Skip the first n-frames of the wrapped trajectory +# stride=n | Step through the wrapped trajectory n-frames at a time +# iterator=i | Use the python iterator object i to select frames from the wrapped trajectory +# subset=s | Use 's' to select a subset of the model to use for each frame +# update_full=b | If False, only update coords for the subset each frame (faster, but other selections from the model won't see updates). Default True. # # Remember that all atoms are shared. If you want to decouple the # trajectory from other groups, pass it a copy of the model. @@ -59,10 +60,11 @@ class Trajectory(object): >>> traj = loos.pyloos.Trajectory('foo.dcd', model) keyword args: - skip = # of frames to skip from start - stride = # of frames to step through - iterator = Python iterator used to pick frame (overrides skip and stride) - subset = Selection used to pick subset for each frame + skip = # of frames to skip from start + stride = # of frames to step through + iterator = Python iterator used to pick frame (overrides skip and stride) + subset = Selection used to pick subset for each frame + update_full = If False, only update subset coords each frame (default True) See the Doxygen documentation for more details. """ @@ -73,6 +75,7 @@ def __init__(self, fname, model, **kwargs): self._skip = 0 self._stride = 1 self._iterator = None + self._update_full = kwargs.get('update_full', True) if 'skip' in kwargs: self._skip = kwargs['skip'] @@ -89,6 +92,11 @@ def __init__(self, fname, model, **kwargs): self._fname = fname self._traj = loos.createTrajectory(fname, model) + if self._update_full: + self._target = self._model + else: + self._target = self._subset + self._stale = 1 self._initFrameList() @@ -130,6 +138,8 @@ def setSubset(self, selection): The selection is a LOOS selection string. """ self._subset = loos.selectAtoms(self._model, selection) + if not self._update_full: + self._target = self._subset def __iter__(self): @@ -182,7 +192,7 @@ def readFrame(self, i): if (i < 0 or i >= len(self._framelist)): raise IndexError self._traj.readFrame(self._framelist[i]) - self._traj.updateGroupCoords(self._model) + self._traj.updateGroupCoords(self._target) return(self._subset) def frame(self): @@ -232,7 +242,7 @@ def _getSlice(self, s): ensemble = [] for i in indices: self._traj.readFrame(self._framelist[i]) - self._traj.updateGroupCoords(self._model) + self._traj.updateGroupCoords(self._target) dup = self._subset.copy() ensemble.append(dup) return(ensemble) @@ -251,7 +261,7 @@ def __getitem__(self, i): if (i >= len(self._framelist) or i < 0): raise IndexError self._traj.readFrame(self._framelist[i]) - self._traj.updateGroupCoords(self._model) + self._traj.updateGroupCoords(self._target) return(self._subset) From d801f06eb9bad7c79d2c25829540ae5f2945f1d0 Mon Sep 17 00:00:00 2001 From: Louis Smith <> Date: Thu, 14 May 2026 12:23:53 -0400 Subject: [PATCH 2/5] pyloos.Trajectory: warn on stale model in subset mode; add refreshModel() --- loos/src/loos/pyloos/trajectories.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/loos/src/loos/pyloos/trajectories.py b/loos/src/loos/pyloos/trajectories.py index ba069d1a5..325e34985 100644 --- a/loos/src/loos/pyloos/trajectories.py +++ b/loos/src/loos/pyloos/trajectories.py @@ -2,6 +2,7 @@ Python-based trajectory classes that wrap loos.Trajectory objects """ +import sys import loos import copy @@ -96,7 +97,13 @@ def __init__(self, fname, model, **kwargs): self._target = self._model else: self._target = self._subset + sys.stderr.write( + "Warning- pyloos.Trajectory: update_full=False means the model " + "you instantiated the trajectory object with will go stale; " + "use traj_object.refreshModel() to update it explicitly.\n" + ) + self._model_dirty = False self._stale = 1 self._initFrameList() @@ -193,8 +200,23 @@ def readFrame(self, i): raise IndexError self._traj.readFrame(self._framelist[i]) self._traj.updateGroupCoords(self._target) + if not self._update_full: + self._model_dirty = True return(self._subset) + + def refreshModel(self): + """ + Update the full model's coordinates from the trajectory's current frame. + Only relevant in update_full=False mode, where the per-frame update + touches only the subset. No-op if the model is already current. + Returns the model AtomicGroup. + """ + if self._model_dirty: + self._traj.updateGroupCoords(self._model) + self._model_dirty = False + return(self._model) + def frame(self): """Return the current frame (subset)""" return(self._subset) @@ -245,6 +267,8 @@ def _getSlice(self, s): self._traj.updateGroupCoords(self._target) dup = self._subset.copy() ensemble.append(dup) + if not self._update_full and indices: + self._model_dirty = True return(ensemble) @@ -262,6 +286,8 @@ def __getitem__(self, i): raise IndexError self._traj.readFrame(self._framelist[i]) self._traj.updateGroupCoords(self._target) + if not self._update_full: + self._model_dirty = True return(self._subset) From 4312a0f2f4b3ae93a0d68ba384b39748a273b161 Mon Sep 17 00:00:00 2001 From: Louis Smith <> Date: Fri, 15 May 2026 13:22:05 -0400 Subject: [PATCH 3/5] Added suppression flag, so that someone has to throw two switches not to get warnings about the risk they're taking. --- loos/src/loos/pyloos/trajectories.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/loos/src/loos/pyloos/trajectories.py b/loos/src/loos/pyloos/trajectories.py index 325e34985..47181aa9f 100644 --- a/loos/src/loos/pyloos/trajectories.py +++ b/loos/src/loos/pyloos/trajectories.py @@ -77,6 +77,7 @@ def __init__(self, fname, model, **kwargs): self._stride = 1 self._iterator = None self._update_full = kwargs.get('update_full', True) + self._suppress_update_full_warning = kwargs.get('suppress_update_full_warning', False) if 'skip' in kwargs: self._skip = kwargs['skip'] @@ -97,11 +98,12 @@ def __init__(self, fname, model, **kwargs): self._target = self._model else: self._target = self._subset - sys.stderr.write( - "Warning- pyloos.Trajectory: update_full=False means the model " - "you instantiated the trajectory object with will go stale; " - "use traj_object.refreshModel() to update it explicitly.\n" - ) + if not self._suppress_update_full_warning: + sys.stderr.write( + "Warning- pyloos.Trajectory: update_full=False means the model " + "you instantiated the trajectory object with will go stale; " + "use traj_object.refreshModel() to update it explicitly.\n" + ) self._model_dirty = False self._stale = 1 From cf43a9520301a5e17a95948e84f1f891c6914d69 Mon Sep 17 00:00:00 2001 From: Louis Smith <> Date: Mon, 18 May 2026 17:16:35 -0400 Subject: [PATCH 4/5] Updated just the readme to provide some install specific comments. --- README.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/README.md b/README.md index 8ade87e3a..3169232b1 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,44 @@ and generally speaking the performance hit for python vs. C++ isn't prohibitive. resources for developing with LOOS on the [GitHub wiki](https://github.com/GrossfieldLab/loos/wiki), particularly the [Tutorials for Developers](https://github.com/GrossfieldLab/loos/wiki/Tutorials-for-Developers). +## Build commands for developers + +Assuming you've already set up your environment following the install process, +you can rebuild into the same env by doing the following: + +The build is driven by CMake; commands below assume an out-of-tree `build/` +directory (see [INSTALL.md](INSTALL.md)). Substitute your build path if +different. + +Rebuild a single tool (after edits under `Tools/` or `Packages/`): + +```bash +cmake --build build/ --target +``` + +Only that executable relinks; `libloos` is left alone unless it was already +stale. + +Rebuild the core library and everything that links against it (after edits +under `src/`): + +```bash +cmake --build build/ -j$(nproc) +``` + +Use `--target loos` to relink just `libloos` without touching the tools. + +Rebuild the PyLOOS bindings (after edits to `src/loos.i` or wrapped C++): + +```bash +cmake --build build/ --target pyloos -j$(nproc) +pip install -e loos/ +``` + +The `cmake` step regenerates `_pyloos.so`. The editable install points pip at +the `loos/` subtree's `pyproject.toml` so subsequent edits to the pure-Python +sources under `loos/src/loos/` take effect with no reinstall. + ### Release 4.1.0 This release includes a number of fixes related to issues listed on github. Added From 5325b9138b8b1e7be4e84bb7bf2d06cb8a77c714 Mon Sep 17 00:00:00 2001 From: Louis Smith <> Date: Mon, 18 May 2026 17:30:20 -0400 Subject: [PATCH 5/5] Fixed mistaken python install line. --- README.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3169232b1..b1f18e7b7 100644 --- a/README.md +++ b/README.md @@ -101,16 +101,25 @@ cmake --build build/ -j$(nproc) Use `--target loos` to relink just `libloos` without touching the tools. -Rebuild the PyLOOS bindings (after edits to `src/loos.i` or wrapped C++): +Rebuild the PyLOOS bindings (after edits to `src/loos.i`, wrapped C++, or the +pure-Python sources under `loos/src/loos/`): ```bash -cmake --build build/ --target pyloos -j$(nproc) -pip install -e loos/ +cmake --build build/ --target loos_python -j$(nproc) ``` -The `cmake` step regenerates `_pyloos.so`. The editable install points pip at -the `loos/` subtree's `pyproject.toml` so subsequent edits to the pure-Python -sources under `loos/src/loos/` take effect with no reinstall. +The `loos_python` target depends on `pyloos`, so this regenerates `_pyloos.so` +and `loos.py` via SWIG and then re-stages the Python package into +`build/src/pyloos/`. The source tree's `loos/` directory is *not* directly +importable — it lacks the SWIG outputs — so the editable install must point at +the staged build copy: + +```bash +pip install -e build/src/pyloos/ +``` + +Run this once. After that, the `cmake --build` command above is enough to pick +up subsequent edits; no reinstall needed. ### Release 4.1.0