# PyRETIS documentation, full text Published at https://www.pyretis.org/dev/. The index with one line per page is at https://www.pyretis.org/dev/llms.txt. The API reference is not included here; it is generated from docstrings and is linked from the index. # About PyRETIS PyRETIS is a Python library for rare event molecular simulations with emphasis on methods based on transition interface sampling and replica exchange transition interface sampling. The papers describing the PyRETIS program can be found here: https://doi.org/10.1002/jcc.24900 (PyRETIS 1, 2017), here: https://doi.org/10.1002/jcc.26112 (PyRETIS 2, 2020), and here: https://doi.org/10.1002/jcc.27319 (PyRETIS 3, 2024). The work on PyRETIS was initiated by Titus van Erp and is used in the research and teaching activities in the theoretical chemistry group at the Norwegian University of Science and Technology. PyRETIS is open source and is released under a GNU Lesser General Public license v2.1+. If you are interested in contributing to the PyRETIS project, please have a look at the developer guide and visit our git repository https://gitlab.com/pyretis/pyretis. The current PyRETIS version is 4.0.0.dev0. For an overview of the official PyRETIS releases, please visit our git repository: https://gitlab.com/pyretis/pyretis/-/releases. Since version 2.4, PyRETIS includes PyVisA, a program created to facilitate post-processing and data analysis. ## Contact For questions about PyRETIS, requests, bug reports, or enquiries about collaborating with us, please contact Enrico Riccardi: - e-mail: enrico.riccardi@uis.no - web page: https://www.uis.no/nb/profile/enrico-riccardi ## The PyRETIS team Head authors & project leaders: - Enrico Riccardi - Titus van Erp Developers: - Dinis O. Abranches - Jonas Jaeger - Arthur Benigno Weidmann Former Developers: - Ola Aarøen - An Ghysels - Anders Lervik - Sander Roet - Daniel Tianhou Zhang Acknowledgments: - Raffaela Cabriolu - Oda Dahlen - Christopher Daub - Sudi Jawahery - Henrik Kiær - Anastasia Maslechko - Mahmoud Moqadam - César A. Urbina-Blanco - Wouter Vervust - Jocelyne Vreede - Magnus Heskestad Waage ## To cite us: When using PyRETIS, or one of our libraries, please cite us! Software papers: - A. Lervik, E. Riccardi and T. S. van Erp, PyRETIS: A well-done, medium-sized python library for rare events, J. Comput. Chem. 38, 2439-2451 (2017). - E. Riccardi, A. Lervik, S. Roet, O. Aarøen and T. S. van Erp, PyRETIS 2: An improbability drive for rare events, J. Comput. Chem. 41, 370-377 (2020). - O. Aarøen, H. Kiær and E. Riccardi, PyVisA: Visualization and Analysis of path sampling trajectories, J. Comput. Chem. 42, 435-446 (2021). - W. Vervust, D. T. Zhang, A. Ghysels, S. Roet, T. S. van Erp and E. Riccardi, PyRETIS 3: Conquering rare and slow events without boundaries, J. Comput. Chem. 45, 1224-1234 (2024). Studies based on PyRETIS: In addition to the software papers above, the following published studies use PyRETIS or PyVisA to sample, analyse or visualise rare events. They include work by members of our group and by other authors building on the software. - T. S. van Erp, M. Moqadam, E. Riccardi and A. Lervik, Analyzing complex reaction mechanisms using path sampling, J. Chem. Theory Comput. 12, 5398-5410 (2016). - M. Moqadam, E. Riccardi, T. T. Trinh, A. Lervik and T. S. van Erp, Rare event simulations reveal subtle key steps in aqueous silicate condensation, Phys. Chem. Chem. Phys. 19, 13361-13371 (2017). - E. Riccardi, O. Dahlen and T. S. van Erp, Fast decorrelating Monte Carlo moves for efficient path sampling, J. Phys. Chem. Lett. 8, 4456-4460 (2017). - M. Moqadam, A. Lervik, E. Riccardi, V. Venkatraman, B. K. Alsberg and T. S. van Erp, Local initiation conditions for water autoionization, Proc. Natl. Acad. Sci. U.S.A. 115, E4569-E4576 (2018). - E. Riccardi, E. C. van Mastbergen, W. W. Navarre and J. Vreede, Predicting the mechanism and rate of H-NS binding to AT-rich DNA, PLoS Comput. Biol. 15, e1006845 (2019). - C. D. Daub, E. Riccardi, V. Hänninen and L. Halonen, Path sampling for atmospheric reactions: formic acid catalysed conversion of SO3 + H2O to H2SO4, PeerJ Phys. Chem. 2, e7 (2020). - E. Riccardi, A. Krämer, T. S. van Erp and A. Ghysels, Permeation rates of oxygen through a lipid bilayer using replica exchange transition interface sampling, J. Phys. Chem. B 125, 193-201 (2021). - S. Roet, C. D. Daub and E. Riccardi, Chemistrees: Data-Driven Identification of Reaction Pathways via Machine Learning, J. Chem. Theory Comput. 17, 6193-6202 (2021). - A. Lervik, I.-H. Svenum, Z. Wang, R. Cabriolu, E. Riccardi, S. Andersson and T. S. van Erp, The role of pressure and defects in the wurtzite to rock salt transition in cadmium selenide, Phys. Chem. Chem. Phys. 24, 8378-8386 (2022). - D. T. Zhang, E. Riccardi and T. S. van Erp, Enhanced path sampling using subtrajectory Monte Carlo moves, J. Chem. Phys. 158, 024113 (2023). - V. Munizaga and M. L. Falk, The thermodynamic effects of solute on void nucleation in Mg alloys, J. Chem. Phys. 161, 044509 (2024). - K. Wilke, S. Tao, S. Calero, A. Lervik and T. S. van Erp, NaCl dissociation explored through predictive power path sampling analysis, J. Chem. Theory Comput. 21, 4604-4614 (2025). - W. Vervust, D. T. Zhang, E. Riccardi, T. S. van Erp and A. Ghysels, Path sampling challenges in large biomolecular systems: RETIS and REPPTIS for ABL-imatinib kinetics, Biophys. J. 124, 3932-3947 (2025). Additional theory papers from our group: - T. S. van Erp, D. Moroni and P. G. Bolhuis, A novel path sampling method for the calculation of rate constants, J. Chem. Phys. 118, 7762-7774 (2003). - T. S. van Erp, Reaction rate calculation by parallel path swapping, Phys. Rev. Lett. 98, 268301 (2007). - A. Ghysels, S. Roet, S. Davoudi and T. S. van Erp, Exact non-Markovian permeability from rare event simulations, Phys. Rev. Res. 3, 033068 (2021). - W. Vervust, D. T. Zhang, T. S. van Erp and A. Ghysels, Path sampling with memory reduction and replica exchange to reach long permeation timescales, Biophys. J. 122, 2960-2972 (2023). - D. T. Zhang, L. Baldauf, S. Roet, A. Lervik and T. S. van Erp, Highly parallelizable path sampling with minimal rejections using asynchronous replica exchange and infinite swaps, Proc. Natl. Acad. Sci. U.S.A. 121, e2318731121 (2024). --- # PyRETIS License (GNU Lesser General Public License v2.1+) Copyright (c) 2023 The PyRETIS team This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 2.1 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . --- # Documentation Terminology This page records the naming and wording conventions for user-facing documentation, and the rule for writing the user guide. The goal is to keep the documentation consistent across the user guide, examples, and API landing pages. ## Project Names Use the configured substitutions in prose: - Use |pyretis| for the project name. - Use |pyvisa| for the visualization and analysis tool. - Use pyretis run, pyretis analyse, and pyvisa for command names. Use the literal package or module name when referring to Python imports, for example pyretis.analysis or pyretis.orderparameter. ## Scientific and File Terms Use these forms in prose: - order parameter for the concept. - orderparameter only for the input section, package, or module name. - Monte Carlo for the method. - HDF5 for the format, and .hdf5 or .hdf5.zip for extensions. - reStructuredText for the markup language, and .rst for files. - Fortran for the language, unless quoting source text that uses another spelling. ## Style Notes Prefer concise, direct instructions. For example, write “Run the analysis with” instead of “The analysis can be executed using”. Keep command examples in literal blocks and wrap filenames, input keys, and command-line flags in double backticks. When a page describes a workflow, start with the result the user is trying to get, then give the commands or settings needed to get there. If a step depends on a previous example, link to that example near the start of the page. ## Writing the User Guide Comments and docstrings describe the code; the user guide, the example pages and the migration guide tell a user how to operate PyRETIS. The reader wants to know what to set, what the run does with it, and what to do when they want something else. Which function reads a key, or which internal route a value takes, does not help them, even when it is true. - A key that a run accepts and ignores is a defect, not a fact to document: the guide promises (user-section-without-effect) that every key either takes effect or is refused. Refuse it (pyretis.inout.key_table.NO_EFFECT, naming what to set instead) or report it. Never document it as behaviour to rely on, and never say which code reads a key (“read only by create_simulation”). A passage that warns users of such a key, in user terms (on which runs the key does nothing, what to set instead), stays, or is written when the key is reported, until the key takes effect or is refused. - Write each statement from the user’s side: the input key with its section ([simulation] allow_setting_change), the command, the file, the result, the message they will see. Do not explain behaviour through the code that implements it (“read by create_simulation only”, “declared in key_table.NO_EFFECT”); name a class, module or function only where the user types, imports, calls or subclasses it (an engine class, an order-parameter class, the functions of the library introduction, python -m pyretis.tools.convert_settings). - When a key is refused, or takes effect on some tasks only and is refused on the others, say on which runs, and what to use instead. - A keyword table gives each key a description a user can act on, in a few words; the details go into the keyword’s own section. - Correct a wrong statement by writing the right one for the user, and keep the explanation that helps them choose (when to restart, when to load). Before replacing a passage, read its earlier versions (its git history) and, for a page PyRETIS 3 had, the PyRETIS 3 page (git show master:): where the older wording was clearer, keep it and correct only what is wrong. Clear any passage that breaks these rules while editing nearby. - A test that pins a passage of the guide to the code (a list of keys, a table) keeps the two in step. When the passage is wrong for users, change the passage and have the test re-pinned (tests come from a separate session); never reword the passage around the test. - History stays out of the guide, except what a user who brings an input or a run file of an earlier PyRETIS (PyRETIS 3, a legacy-runner input, an output.toml or restart.toml an earlier version wrote) needs: what it does now and what to set instead, and which results or output files differ. It goes into the entry of the key or the page concerned, and into the migration guide. --- # Cross-engine validation: water-dimer dissociation This page records a reproducibility check that the external molecular-dynamics engines driven by PyRETIS reproduce the same rare-event kinetics on one simple molecular system with a shared force field. The system is the dissociation (“splitting”) of a hydrogen-bonded water dimer, with the O–O distance as the order parameter. The case and the scripts below live in examples/validation/engines/water_dimer/. The point of the test is that GROMACS, LAMMPS and OpenMM integrate the identical potential, so if their rate-constant estimates agree (within statistical error) the PyRETIS machinery that drives them is reproducible across engines. ## The shared system - Force field: flexible SPC/Fw water (Wu, Tepper and Voth, J. Chem. Phys. 124, 024503, 2006) – harmonic O–H bonds and H–O–H angle, Lennard-Jones on oxygen, point charges. The model is flexible (no SETTLE/SHAKE constraints), so the forces are identical across engines with no constraint-solver divergence. - Conditions: gas-phase dimer in a 5 nm box with a bare (unshifted) 2 nm cutoff, so every engine evaluates the same 1/r + LJ for all dimer pairs. Velocity Verlet with the CSVR thermostat (stochastic velocity rescaling; Bussi, Donadio and Parrinello, J. Chem. Phys. 126, 014101, 2007) at T = 250 K, matched in all three engines: a time constant of 1 ps, rescaling at every step and a 0.5 fs time step. A path-sampling run needs the velocities at the time of the positions (velocity Verlet, GROMACS md-vv), and GROMACS has no Langevin thermostat for md-vv. The temperature was chosen from MD scans with these dynamics (scan_md.py, the time to the first O–O > 6 Å, 500 ps at most): at 300 K the dimer splits after 5.5, 27 and 159 ps; at 150 K it stays bound in three runs of three (too rare to converge); at 250 K it splits after 11, 13, 30, 108 and 208 ps and stays bound in one run of six – a genuine but tractable rare event. - Order parameter: O–O distance. State A (bound) is O–O < 3.5 Å, state B (dissociated) is O–O > 6.5 Å, with eight interfaces between. Every engine’s input is generated from a single source, wd_common.py, so each force field is derived from the same numbers. ## The force-field gate Before any rate is compared, check_forcefield.py evaluates identical configurations – including deliberately strained ones that exercise the bond/angle constants and the factor-of-two prefactor convention that differs between LAMMPS and GROMACS/OpenMM – in all three engines and compares the potential energy: ```pyretis conda run -n pyretis python check_forcefield.py ``` The engines agree to \le 3\times10^{-5} relative (the residual is the documented difference between the codes’ electrostatic constants). This gate must pass before a rate comparison means anything: a green RETIS run with a mismatched force field would silently compare different physics. ## Engines and samplers - GROMACS – native PyRETIS RETIS, canonical streaming engine (class = "gromacs" -> GromacsEngine). - OpenMM – native PyRETIS RETIS, in-process engine. - LAMMPS – the streaming engine via infinite swapping (class = "lammps"); the rate is obtained from WHAM. This is a different sampler from the other two, so agreement is additionally a cross-sampler check. ## Results Flux through \lambda_0 and the dissociation rate constant, converted to a common unit (1/ps). RETIS, 3000 cycles per engine. The table holds the rates of a study that ran Langevin dynamics with a friction of 1/ps in every engine, with the force field, temperature, time step and interfaces above; the inputs of examples/validation/engines/water_dimer/ run the CSVR dynamics above, whose rates the table does not hold. ## Water-dimer dissociation: cross-engine rate comparison (250 K). engine sampler cycles flux (1/ps) rate (1/ps) GROMACS canonical RETIS 3000 0.161 (8 %) 0.0127 (22 %) OpenMM canonical RETIS 3000 0.177 (7 %) 0.0076 (18 %) LAMMPS infinite swapping 3000 0.221 0.0185 The bracketed numbers are relative block-error estimates. Findings: - The two native engines agree on the flux through \lambda_0 within statistical error (0.161 vs 0.177 /ps, ~1 \sigma), and on the rate within ~2 \sigma. The corresponding escape time (~80–130 ps) matches the direct-MD dissociation timescale (~100 ps), an independent sanity check. - The streaming LAMMPS engine (a different engine and sampler) reproduces the same flux and rate to within ~1.4x / ~1.5x. The small systematic offset is consistent with the WHAM point-matching estimator and the infinite-swapping sampler; resolving whether it closes fully would need a longer run. - Overall the three engines agree to within a factor of ~1.4 on the flux and ~2.4 on the rate at 3000 cycles – the level expected for RETIS with ~20 % block errors, and well within an order of magnitude. The force-field gate above guarantees they are integrating the same potential, so the residual spread is statistical / estimator-level, not a physics difference. ## Reproducing the result On a node with gmx, lmp and openmm (conda env pyretis): ```pyretis cd examples/validation/engines/water_dimer python check_forcefield.py # the force-field gate (must pass) python build_inputs.py # GROMACS + LAMMPS inputs from one source python compare_engines.py run --steps 3000 python compare_engines.py compare # flux + rate per engine, in 1/ps ``` The streaming-LAMMPS load paths are bootstrapped by lammps_stream/generate_water_loadpaths.py (adapted from generate_H2_loadpaths); compare_engines.py calls it automatically. The LAMMPS case of the comparison, lammps_stream/, runs the streaming engine (class = "lammps" -> LAMMPSEngine) via infinite swapping. The case directory lammps/ runs the same streaming engine under native PyRETIS RETIS (retis.toml) and is kept for reference. The relaunch engine class = "lammps_steps", which launches LAMMPS once per propagation, is not used here: its kick-initiation is slow on this system, since lambda_0 sits at the bound-state 99th percentile, so the velocity-kick walk needed to reach it requires many LAMMPS starts. GROMACS and OpenMM use canonical RETIS. Because the rate is sampler-independent, mixing samplers is sound – and the agreement between them is an extra cross-sampler validation. --- # Extending and validating PyRETIS This page is for developers adding a new Monte Carlo path move or a new engine to PyRETIS, and – just as importantly – checking that the addition reproduces the same physics as the existing code. A new move or a new engine changes how paths are produced, not which path ensemble is sampled, so a correct implementation must give the same crossing probability and rate constant as the established ones, within statistical error. PyRETIS ships a small reproducibility suite for exactly this check, in examples/validation/. The short test-easy / test-heavy runs in CI cannot afford the cycles needed to drive the rare-event statistics down, so this suite lives in examples/ and is launched manually (on a cluster) when you add or change a move or an engine. # Table of contents - Designing a new path move - Writing a shooting-point selector - Implementing a new engine - Validating reproducibility - Convergence speed across moves - Adding your move or engine as a case ## Designing a new path move The unified path moves live in pyretis/core/moves.py. The existing moves are standard shooting (sh) and the sub-trajectory moves Stone Skipping (ss), Web Throwing (wt) and Wire Fencing (wf), each optionally in a high-acceptance variant. Their input keywords (shooting_moves, n_jumps, interface_sour, interface_cap, high_accept, sigma_v) are documented in the TIS input reference, and their output codes in Path types and rejection reasons. When adding a move: - Implement it alongside the existing moves, give it a short two-letter move code, and make it selectable through the shooting_moves keyword. - Preserve (super-)detailed balance: the move must leave the path ensemble’s distribution unchanged. This is the correctness criterion, and it is what the validation suite measures – a move that breaks it will not reproduce the reference rate. - Document the new move code in Path types and rejection reasons. - Add a validation case for it (see below) and confirm it agrees with the other moves before relying on it. ## Writing a shooting-point selector The biased shooting move (shooting_move = "bias", see user-section-tis-biased-shooting) delegates the choice of shooting point to a ShootingPointSelector (pyretis/core/shooting_selection.py). To add your own bias – analytic, density-based, or machine-learned – subclass it and implement a single method: ``` from pyretis.core.shooting_selection import ShootingPointSelector class MySelector(ShootingPointSelector): def weights(self, path): # Return a 1-D numpy array of strictly positive, finite # weights, ONE PER INTERIOR FRAME (path.phasepoints[1:-1]). ... ``` Contract for weights(path): - Return exactly path.length - 2 values (interior frames only; the two end points are never shooting points). - Every weight must be strictly positive and finite. Non-positive or NaN/inf weights raise rather than being silently clamped – a wrong selection weight silently biases the sampled rate, the worst failure mode for this code. (For exp-type weights, guard against underflow: a too-sharp bias that drives a frame to 0 is an error.) The base class provides sum_weight(path) (the partition function W) and select(path, rgen), which draws one interior frame with probability w_i/W using a single uniform from the per-ensemble random generator. You do not re-implement selection or the acceptance correction; the move applies \min(1, W_\mathrm{old}/W_\mathrm{new}) automatically, so any positive weights is detailed-balance correct. Load a user selector from the input like any other pluggable class, via a module/class pair in the [shooting-selector] section: ``` [shooting-selector] module = "my_selectors.py" class = "MySelector" # any further keys are passed to MySelector.__init__ ``` ## Implementing a new engine How to subclass the engine base classes – EngineBase / MDEngine for an internal engine, or ExternalMDEngine to drive an external program such as GROMACS, LAMMPS or OpenMM – is covered, with templates, in Creating custom engines. For correctness, the key point is the same as for moves: an engine only changes how the dynamics are generated. On a shared system (same potential / force field, temperature, timestep and thermostat) a correct engine must reproduce the same rate as the others. Make the dynamics genuinely match before treating a rate disagreement as a bug – the rate depends on the initial flux, so a mismatched thermostat or timestep, not the engine code, is the usual culprit. ## Validating reproducibility The suite in examples/validation/ has two groups, both modelling the same physical system so their rates must agree: - methods/ – move equivalence on the internal engine: the same 1D double-well sampled with sh / ss / wt / wf (and the high-acceptance and infinite-swapping variants). A direct cross-check of the move implementations. - engines/ – engine equivalence with a shared force field: the same dissociation run through GROMACS / LAMMPS / OpenMM. It is driven by a per-machine config, validation.toml – an ordered list of cases with their target cycles, plus a per-machine seed, a reverse_list toggle, and jobs (internal cases run in parallel). The first positional argument selects the action: ``` python run_validation.py # show this usage help python run_validation.py status # show recorded results (read-only) python run_validation.py analyze # (re)analyse the runs on disk python run_validation.py run # run every case, then analyse python run_validation.py run sh_pcg64 # run just one case ``` cycles is a target total: a case that already has output is continued up to it, otherwise started fresh from seed. Use a different seed on each machine so two runs are independent and can be combined for better statistics. Each case is analysed as soon as it finishes, and the suite maintains a per-case results table in previous_results/ (tracked in git – so git diff of the table is the comparison across validation sessions; running one case updates just that case’s row). The analysis prints a rate comparison (ok / CHECK), the verdict of each *_pcg64 case against its recorded *_mt19937 row, and writes the convergence plot rate_vs_cycles.png. See examples/validation/README.rst for the full description. ### Convergence speed across moves All moves converge to the same rate – that agreement is the validation – but they do not converge at the same speed. The rate_vs_cycles.png plot shows each case’s running rate estimate against cycle number: a curve that flattens onto the common value sooner has converged faster. This is where the suite doubles as a showcase. The sub-trajectory moves (Stone Skipping, Web Throwing, Wire Fencing 1 2) and the high-acceptance variants are designed to decorrelate successive paths more effectively than plain one-shot shooting, so on this system they typically reach a stable estimate in noticeably fewer cycles – which you can read straight off the plot. Running the methods/ group and looking at the overlaid curves is therefore both a correctness check and a practical guide to which move to reach for. ### Adding your move or engine as a case - Copy an existing case directory – e.g. methods/wf_pcg64 for a new move, or one of the engines/ cases for a new engine – keeping the same double-well (or shared force field), temperature, timestep and interfaces, so the rate is required to match. - Point the copy at your new thing (set shooting_moves to your move code, or select your engine). - Register the case in the manifest in examples/validation/run_validation.py and list it in validation.toml. - python run_validation.py run and confirm its row in the table agrees within n_sigma once converged. A CHECK usually means not converged yet – raise its cycles and re-analyse before concluding there is a discrepancy. E. Riccardi, O. Dahlen and T. S. van Erp, Fast decorrelating Monte Carlo moves for efficient path sampling, J. Phys. Chem. Lett. 8, 4456-4460 (2017), https://doi.org/10.1021/acs.jpclett.7b01617 D. T. Zhang, E. Riccardi and T. S. van Erp, Enhanced path sampling using subtrajectory Monte Carlo moves, J. Chem. Phys. 158, 024113 (2023), https://doi.org/10.1063/5.0127249 --- # Developer Guide PyRETIS is open source and hosted on gitlab. We are happy to include more developers and we want active users. If you wish to contribute to the PyRETIS project, please read through the code guidelines and the short description on contributing given below. ## Guidelines The guidelines can be summarised as follows: - Check that your code follows pep8. - Document your code using NumPy style docstrings - Use a logger rather than print() in libraries. - Write comments and docstrings for a reader who arrives cold and who will never see your branch or any earlier version of the file. Everything they say must be a true statement about the code that is there. Three things are therefore never committed. - History. What the code used to do, what changed, what was broken, and where the code came from. No “used to”, “previously”, “no longer”, “now”, “still”, “already”, “fixed”, “was wrong”, “the new behaviour”, “formerly”, “the former X”, “the retired X”, “now-deleted”, “as before”. No commit hashes, no plan, review or issue labels (“A3.2c”, “stage C”, “finding R1”, “MR !396”, “#415”), no dates of changes (“2026-05-27”, “Jul 2025”), no references to who asked for, approved or ruled on a change, no agent or session references, and no pointers to working notes, decision records or review documents. No plans (“a future phase will collapse this”). No word on where the code came from: “ported from”, “adapted from” or “copied from” another project or an earlier version of PyRETIS, the code it replaced, or an author credit. Third-party code keeps its attribution: a line that names the code it is based on and that code’s licence, or the notice its licence requires. One use of “now” stays: the state of the code at that point relative to other code – “the path is now held by the ensemble the swap chose”, or a test stepping through “the folder is now empty” right after the call that emptied it. - What the code does not do. A reader needs the behaviour, not a tour of the behaviours that were rejected: no “this is not a paste bug”, no “rather than crashing”, no “instead of the old check”. Naming a boundary is fine where the boundary is the rule – “a leg that fails returns BTL above this point” says when a branch applies – but a sentence whose only content is a contrast with something absent from the code does not belong. One use of “rather than failing” / “instead of raising” stays: explaining error handling – a try/except, a caught exception, a fallback taken on error (“a missing file reads as empty rather than raising”). - Justification by contrast. No “measured rather than assumed”, no “decided rather than guessed”. If a test measures something, it measures it. - Keep the reasoning and drop the rest. “A two-frame path has no interior frame, so no shooting point can be drawn from it” is useful forever. Where a decision has a scientific source, cite the paper, not the discussion that led to it. The history belongs in the commit message and the issue tracker, which is where someone looking for it will go. - Such text is meaningless to a user reading the source or the API documentation, it dates immediately, and it makes a scientific codebase read like a changelog of internal disagreements. - devtools/prose_audit.py finds the phrases of the three kinds; it reports the labels, dates, rulings, agent references and pointers of the first as a kind of their own, label. test/docs/test_comment_rule.py holds the tree to it. A site that keeps a phrase under one of the two exceptions is listed, with the exception that applies, in test/docs/prose_audit_allowed.json; so is a site whose phrase only looks like a violation, with the reason. - Write the user guide for the person who runs PyRETIS: see developer-guide-docs-style-user-guide. PyRETIS follows the Python pep8 style guide (see also pep8.org) and new code should be checked with the pep8 style guide checker and pylint: ```pyretis pycodestyle source_file.py pylint source_file.py ``` or other tools like PyChecker or pyflakes. NumPy’s imports can be a bit tricky understand so you can help pylint by doing ```pyretis pylint --extension-pkg-whitelist=numpy source_file.py ``` The PyRETIS project is documented using the NumPy/SciPy documentation standard and contributors are requested to familiarise themselves with this style and use it. For user-facing documentation, also see the documentation terminology guide. Documentation style can be checked with pydocstyle ```pyretis pydocstyle source_file.py ``` We also try to avoid print() statements in the libraries and reserve such statements for console output from command line scripts/programs. Output, like debug information, warnings etc. can be handled by using a logger. In a library, we typically just import and set up the logger by doing: ```python import logging logger = logging.getLogger(__name__) # pylint: disable=C0103 logger.addHandler(logging.NullHandler()) ``` We can then use it as follows: ```python # Report some information logger.info('Up and running') logger.debug('Debug info') logger.warning('This is a warning') logger.critical('Something critical happened!') logger.error('An error occurred!') ``` Please note that we do not add any particular handlers here - it is up to the user to define what should happen when a logging event occurs. ### Testing your changes PyRETIS ships two helper scripts in the repository root that bundle the checks a contribution is expected to pass. Run them from the main directory before you commit and before you open a merge request. The fast check you run on a normal machine before every commit. It runs the tests of test/ (test/infswap/ and test/integration/ included) under pytest with four workers (PYTEST_WORKERS sets the count), except the long end-to-end tests marked heavy, which test-heavy.sh runs. The style and lint checks run beside them: pycodestyle, pydocstyle and the project pylint wrapper (devtools/run_linting.py). It is deterministic and finishes in a few minutes; PYRETIS_COVERAGE=1 adds a coverage run of every test. It builds no documentation; python devtools/docs/run_checks.py runs the documentation checks of test-heavy.sh on their own. The heavy tests run before every push: test-heavy.sh runs them, or run them on their own with OMP_NUM_THREADS=1 python -m pytest -n 4 --dist worksteal -m heavy test/ test/infswap/ test/integration/ (where lmp needs the lib/ directory of its environment on LD_LIBRARY_PATH, set it first, as test-heavy.sh does). The slower, end-to-end check you run before pushing changes that can affect numerical results – in particular anything touching an engine, an order parameter or reference data. It runs four stages: the engine reference-data consistency suites (examples/run-tests.sh – internal, GROMACS, PyVisA, CP2K, OpenMM and LAMMPS, each comparing freshly generated output against stored reference data; the GROMACS suite runs beside the others), a tutorial smoke run (examples/run-tutorials.sh: the path-sampling tutorials it shortens run 20 cycles and are analysed, and every other tutorial is stopped after two seconds, which confirms it still launches), the tests of test/ marked heavy, and the documentation checks of the CI job build_docs (devtools/docs/run_checks.py: the literalinclude directives, the HTML build with sphinx-build -W, which fails on any warning, one from a docstring that autodoc renders too, and the checks of the built pages and of the API pages). The documentation stage runs with the Python that PYRETIS_DOCS_PYTHON names, or else the python of the other stages; where that Python cannot import Sphinx, sphinx_bootstrap_theme, myst_parser, the PyVisA stack (PyQt5, scikit-learn, graphviz) or the pyretis of the tree, or is not a Python 3.11 or later, the stage fails and names what is missing. test-heavy.sh is deliberately resilient – every stage always runs, the full output is tee’d to test-heavy.log, and a PASSED/FAILED recap is printed at the end – so it is best run on a cluster. See testing your installation for the detailed breakdown of what each stage covers and the environment variables that tune the engine scope and tutorial length. ## Contributing to PyRETIS There is a nice guide on github about contributing to open source projects. In PyRETIS we largely follow this approach and the issue tracker is used for reporting bugs and issues and requesting new features. This section on contributing is based on the description for gitlab. Before contributing, please read the short guidelines given below for reporting bugs and issues and for requesting new features. ### Bugs and issues If you find a bug in PyRETIS, please create an issue using the following template: ```restructuredtext Summary ------- One sentence summary of the issue: What goes wrong, what did you expect to happen Steps to reproduce ------------------ Describe how the issue can be reproduced. Expected behavior ----------------- Describe what behavior you expected instead of what actually happens. Relevant logs ------------- Add logs from the output. Output of checks ---------------- Be sure that all the tests pass before filing the issue. Possible fixes -------------- If you can, link to the line of code that might be responsible for the problem. ``` If you wish to fix the bug yourself, please follow the approach described for merge requests below. ### New features If you are missing some functionality in PyRETIS you can create a new issue in the issue tracker and label it as a feature proposal. If you do not have rights to add the feature proposal label, you can ask one of the core members of the PyRETIS project to add it for you. You can also implement the changes you want yourself as described below. We cannot promise that we will automatically include your work in PyRETIS but we are happy to have active users and we will consider your contribution. So, when you are finished with your work please make a merge request. ### Merge requests The general approach for making your bug-fix or new feature available in the PyRETIS project is as follows: - Fork 1 PyRETIS into your own personal space. - Create a new branch and work on your feature. - Create tests for your feature. Especially, if you are fixing a bug, create a test that shows where the bug is causing PyRETIS to fail and how your code is fixing this. - Squash all your commits into one and push to your fork. - Submit a merge request to the master branch using the template given below. - Wait for review and the following discussion :-). ```restructuredtext Summary ------- What does this merge request do? Justification ------------- Why is this merge request needed? Description of the new code --------------------------- A short description of the new code. Are there points in the code the reviewer needs to double check? References ---------- Add references to relevant issue numbers. ``` After submitting the merge request the code will be reviewed 2 by (another) member of the PyRETIS team. Gitlab documentation on forking: http://doc.gitlab.com/ce/workflow/forking_workflow.html https://github.com/thoughtbot/guides/tree/master/code-review --- # Infinite-swapping integration status The infinite-swapping source-tree merge was completed on 2026-07-06. This page summarises the resulting architecture, the conventions still in force, and where the historical validation records live. ## Goal PyRETIS has one path-sampling execution path: the scheduler in pyretis/simulation/ drives the unified moves, path/system types, and engine classes. Canonical and runner-style TOML remain separate input schemas, but both feed this scheduler and emit the same per-ensemble output. Two rules continue to govern development: - One terminology. PyRETIS uses method-based names throughout: the analysis is WHAM and the sampler is infinite swapping (stem infswap). User-facing documentation and commands use these names consistently. - Reproducibility first. Results must be deterministic and correct. A reference comparison is only useful if it fails when the physics is wrong (see developer-merge-status-tests). ## What works today - pyretis run is the single entry point. It runs all path-sampling tasks through the scheduler and enables parallel infinite swapping via [simulation] task = "infinite_swapping" or the [runner] section. - The WHAM crossing-probability / rate analysis lives in pyretis/analysis/wham_analysis.py (a faithful implementation of the standard weighted-histogram procedure for TIS; an earlier analysis stub was scientifically wrong). - All tasks share pyretis.core.system_core.System and pyretis.core.path.Path; disk loaders live in pyretis.core.path_load. The random-number generator is PCG64. ## How to configure the scheduler ```bash # Classical RETIS / TIS, adapted to the scheduler with one worker: pyretis run -i retis.toml -p # infinite swapping (multi-worker, infinite-swap + wire fencing): # the TOML selects it via task = "infinite_swapping" or [runner] pyretis run -i infswap.toml ``` See examples/validation/ for runnable method and engine comparisons. ## Test policy (green == correct) - Run ./test-easy.sh (unit + integration + style, without the long end-to-end tests marked heavy) before every commit. - Run ./test-heavy.sh (engine reference suites + tutorials + the heavy tests + the documentation checks) before a push – test-easy does not run the example compare.py harnesses, the heavy tests or the documentation build, so a reference/default change, or a docstring that breaks sphinx-build -W, can pass test-easy and still break test-heavy. - A comparison must not pass on degenerate input. The shared primitives in pyretis/testing/simulation_comparison.py reject empty data, shape mismatches, NaNs at mismatched positions, and entirely-NaN columns; a quantity an engine does not report must be declared explicitly by name, never accepted merely because both sides contain NaN. ## Resolved issues - test-gromacs/test-load/test-initialise asserted that a loaded run bit-for-bit reproduces a fresh run – that is restart semantics, not load semantics. It was reframed to load semantics (commit b619a767), and its check is made by comparison 4 (load) of test-gromacs/test-restart/test-resume (commit 131fcc006, which removed test-load/test-initialise): its run.sh loads the last accepted paths of run-initialise twice, into run-load and run-load2, runs each for 5 cycles from the same GROMACS velocity-seed state, and compares the two runs with python -m pyretis.testing.example_compare compare (the golden and archive checks), which is what a load must guarantee (it works and reproduces previously loaded results). Rigorous load-determinism is covered by the internal engine (test-internal/retis-load-sparse), and exact continuation by the restart tests (test-internal/{retis,tis,md,mdflux}-restart, test-gromacs/test-restart). - The merge-readiness review’s findings (H1–H4, M) are fixed: the load-traj energy comparison is data-driven per ensemble (not a blanket skip); pyretis run restores the completed-restart guard and the descriptive missing-input message; the WHAM lamres default uses the smallest interface gap with a commensurability check; and _wham_pq always returns one crossing probability per interface. ## Known issues - CP2K loaded frames have no computed energy and declare those unavailable terms per ensemble. The reference is the deterministic conda-forge 2026.1 build (single rank, re-blessed 2026-07-19); other builds run execution-smoke only. - The former GROMACS 5.1.4 tarballs were re-baselined onto the single current reference build (double-precision 2025.4, re-blessed 2026-07-19), so one executable validates the complete GROMACS reference set. - External numeric tolerances remain provisional until repeated pinned-engine runs provide measured error envelopes. OpenMM and LAMMPS use additional byte-exact gates on their recorded reference builds. --- # Method-equivalence validation: the double-well on one engine This page records a reproducibility check that every sampling strategy and every code path in PyRETIS reproduces the same crossing probability and rate constant on one simple system, and that the result matches an absolute analytical truth rather than only agreeing with itself. The system is a 1D double well sampled on the internal engine; the cases and the driver script live in examples/validation/methods/ and are launched by examples/validation/run_validation.py. The point of the test is that every sampling strategy, the PCG64 generator of the runs (against the recorded MT19937 rows, below), the multi-worker pool, and both rate estimators (the matched crossing-probability report and WHAM) all work over the identical potential, so if their rate estimates agree – and land on the analytical Kramers rate – the sampling machinery is reproducible across strategies, generators and estimators. This complements the cross-engine validation, which checks engine equivalence on a shared force field. ## The shared system Every method case models the same 1D double well, so they must give the same rate within statistical error: - Potential: the quartic double well V = a x^4 - b x^2 + c with a = 1 and b = 2 (so the barrier is \Delta V = b^2 / (4 a) = 1), c = 0. - Dynamics: inertial Langevin at temperature T = 0.07 with a time step dt = 0.025 and friction gamma = 0.3 (reduced units), unit mass. With these numbers \beta\,\Delta V = 14.3, so escape over the barrier is a genuine rare event. - Order parameter: the particle position, with eight interfaces spanning [-0.99 \ldots 1.0]. The system parameters are not hard-coded into the analysis – they are read back out of each case’s input TOML (read_system_params), so the analytical reference below tracks the actual runs, and every method case is cross-checked to confirm it really models the same well. ## The analytical reference – a known truth The double well is simple enough to have a closed-form escape rate from Kramers’ theory, so the suite checks the simulations against an absolute truth, not only against each other. analytical_double_well_rate() evaluates it in three increasingly complete forms: ## Analytical double-well escape rate (reduced units). Estimator Rate Meaning k_TST 2.81e-07 transition-state theory, no recrossing (upper bound) k_Kramers (spatial) 2.61e-07 moderate/high-friction Kramers prefactor k_Kramers-MM (truth) 2.52e-07 - Mel’nikov–Meshkov turnover factor (headline) With \beta\,\Delta V = 14.3 and reduced energy loss \delta = 11.4 the turnover factor is \Upsilon = 0.97 – the system sits firmly in the spatial-diffusion regime, so the Kramers result is accurate to a few percent. Converged cases should land on 2.5e-07 within their statistical error. A case can agree with every other case yet sit several sigma from the analytical rate – a shared systematic that the self-consistency check alone cannot see. ## The methods The same double well is sampled with every available strategy and checked through both estimators. Every run goes through the single pyretis run entry point, and draws every random number from PCG64 streams spawned from [simulation] seed; the cases differ in their configuration (*_pcg64 states rgen = "pcg64", *_default leaves the config unpinned – byte-identical to its *_pcg64 sibling) and in their analysis kind: matched (the point-matching product of per-ensemble crossing probabilities, reported by pyretis analyse) or wham (the WHAM stitch of the same per-ensemble output, e.g. infswap_wham). ## Method cases on the shared double well. Case Strategy / what it adds sh_pcg64 standard shooting ss_pcg64 stone skipping wt_pcg64 web throwing wf_pcg64 wire fencing wf_ha_pcg64 wire fencing with high acceptance wf_cap_* wire fencing with a capped window ([tis] interface_cap) wt_sour_* web throwing with a shifted source sub-interface ([tis] interface_sour) relshoot RETIS with relative per-ensemble shoot frequencies ([retis] relative_shoots) infswap_wham infinite swapping analysed with WHAM – the cross-check of the second estimator *_default the same samplers with NO RNG pin (the shipping default): byte-identical twins of their *_pcg64 siblings, a routing/determinism guard sh_w3 the default config with a multi-worker pool of three workers (PYRETIS_WORKERS=3) – the parallel-worker route sh_priority the default config with priority_shooting = true (group priority; sh_priority_w3 runs it on three workers) – only the allocation of moves changes, so it must reproduce the same rate bias_gaussian biased shooting-point selection (Gaussian selector toward the barrier) – the move biases the shooting point but preserves the path ensemble, so it must reproduce the same rate (bias_gaussian_default is its byte-twin, bias_uniform the uniform-selector identity check) The wf_cap_* / wt_sour_* / relshoot cases (group params) vary non-default TIS knobs that change the sampling but not the physics, so each must still reach the same analytical rate. The wf_convention group (wf_pcg64/wf_ha_pcg64, infswap_wham, wf_default) cross-checks wire fencing across the estimators. On the scheduler’s per-ensemble-output route the wf occupancy is HA-weighted (the compute_weight crossing count drives the swap, as in the infinite-swapping sampler), so the output writer applies the WHAM Cxy/HA unweighting (Weight = compute_weight / frac) to recover the per-ensemble crossing probability – without it the rate over-counts by ~two orders of magnitude; ss_default carries the same treatment for stone skipping. The suite config’s select key (group tags such as wf_convention or params) runs a chosen subset without dropping the rest. The matched-kind rows (analysed with the standard PyRETIS crossing-probability report) and the wham-kind rows (analysed with WHAM over the identical per-ensemble output) are a direct cross-check of the two estimators. The *_mt19937 rows of previous_results/ hold the rates of the same samplers on numpy’s MT19937 generator; the analysis compares each with its *_pcg64 case, and the pair must agree within statistical error. The *_default twins must stay byte-identical to their *_pcg64 equivalents – a routing/determinism guard. ss_default (stone skipping under the worker pool) earlier crashed the internal engine’s streaming dump (FileNotFoundError on ss_shoot.xyz) because the move re-pointed a persistent path’s phase point at the transient dump file. That is fixed (stone skipping now dumps a copy), and the case is enabled like the others. ## REPPTIS on two Brownian-dynamics systems REPPTIS rebuilds the kinetics from partial paths, assuming that a path forgets where it came from once it crosses the next interface. Overdamped (Brownian) dynamics, [engine] high_friction = true, is Markovian in the position, so the assumption holds and REPPTIS must give the exact result. With the Langevin dynamics of the double well above (friction 0.3) the velocity is remembered over a distance of about 0.9, nine interface spacings, and REPPTIS is an approximation there. The group repptis_validation therefore samples two systems with Brownian dynamics, each with a REPPTIS case and its RETIS twin (the inputs differ only in [simulation] task), and each case is compared with its own system’s analytical value: Cases System and analytical value bd_dw_retis, bd_dw_repptis The double well at k_B T = 0.2 (\beta \Delta V = 5) with D = k_B T/(m\gamma) = 0.2. In one dimension a period in the overall state A runs from the entry into A at \lambda_A to the entry into B at \lambda_B, so the TIS rate is exactly the inverse mean first passage time between them, computed by quadrature from the case parameters: k = 5.49 \times 10^{-3}. It holds at any temperature; at k_B T = 0.2 the Kramers formula in the Smoluchowski limit is 10 % too high. bd_flat_retis, bd_flat_repptis A flat membrane with D = 0.01 and h = 0.2, the Brownian test system of Ghysels et al. 1, in permeability mode: P = D/h = 0.05. The reference bin of \tau sits in the middle of [\lambda_{-1}, \lambda_0]; on a flat potential its position does not matter, so a [0^-] ensemble that misses part of the region shows as a wrong permeability. The status table’s “vs ref” column compares each case with its own system’s value; the flat cases record the permeability in the rate column. A. Ghysels, S. Roet, S. Davoudi and T. S. van Erp, Exact non-Markovian permeability from rare event simulations, Phys. Rev. Research 3, 033068 (2021), https://doi.org/10.1103/PhysRevResearch.3.033068 ## Running it and reading the output The suite is not a unit test and not a tutorial, and it is not run per change or per release – see when to run it. Short developer runs can be launched manually. The full multi-seed campaign does not run in GitLab CI: it needs on the order of 36 hours, far beyond the one-hour job timeout, so it belongs on a workstation or cluster node you control, typically driven by campaign_run.sh. CI carries only the method_validation_smoke launch check – a single case at ten cycles – which proves the driver still runs end to end but deliberately produces no validation evidence. All modes use a per-run config such as validation.toml – an ordered list of cases with their target cycle counts plus a per-machine seed and a reverse_list toggle. The first positional argument selects the action: ```pyretis cd examples/validation # usage helper (also printed when no action is given) python run_validation.py # show the recorded results -- READ-ONLY: runs nothing, writes nothing python run_validation.py status python run_validation.py status sh_pcg64 # ... for one case # (re)analyse the runs already on disk and update the results table python run_validation.py analyze python run_validation.py analyze sh_pcg64 # ... for one case # run every case listed in ./validation.toml, then analyse python run_validation.py run python run_validation.py run sh_pcg64 # ... for one case ``` One-off overrides, usable with run: ```pyretis python run_validation.py run --config machineB.toml # different per-machine config python run_validation.py run --cycles 20000 # override every case's target python run_validation.py run --seed 2 # this machine's seed python run_validation.py run --jobs 8 # internal cases in parallel ``` --jobs N runs that many internal-engine (methods/) cases at once; they are independent single-process runs, so this only uses more cores and does not change any result (each case has its own directory and seed). --cycles is a target total, not an increment – a case with existing output is continued up to that count, otherwise it starts fresh from seed. python run_validation.py --write-config validation.toml writes a fresh config template with every case enabled. Each case is analysed as soon as it finishes – its rate prints on the go – and a combined inventory, convergence plot and comparison follow at the end. The analysis prints a “Rate vs analytical Kramers reference” table (rate, k/k_ref, |d|/sigma, agree?) next to the table against the mean of the cases of the same system (the water-dimer engine cases are compared with their own mean). The persistent summaries land in validation_results.json (one entry per case: rate, relative error, cycles, number of independent runs, the agreement flags agree_analytical and agree – against the analytical rate and against that mean, shown as “vs ref” and “vs mean” – plus provenance – when, at which git commit, and whether the tree was dirty) and the human-readable validation_results.html (the same table with the convergence rate_vs_cycles.png embedded). A CHECK in the comparison almost always means not converged yet – raise that case’s cycles and re-analyse. See examples/validation/README.rst for the full driver reference, including the two-machine combine-for-statistics workflow. ### When to run it Seldom, and only for substantial changes. This campaign is not the routine correctness gate and must not be treated as one. Day-to-day confidence that a modification still produces the correct behaviour comes from the ordinary suites: - the unit and integration tests (test-easy.sh, and the long ones marked heavy in test-heavy.sh), which pin the algorithms against hand-computed and synthetic oracles; - the example suites (test-heavy.sh), which compare complete runs byte-for-byte against committed reference output on every engine; - the tutorial smoke run, which proves every documented workflow still launches. Those catch a regression far faster, far more cheaply, and far more precisely than a 36-hour statistical campaign can: a golden comparison fails on the first differing digit, whereas a rate estimate has to out-run its own error bars before a change becomes visible at all. A campaign that merely reproduces what the goldens already pin has told you nothing new. Re-run the campaign when a change can move the sampled distribution itself, so that the goldens are expected to shift and can no longer serve as the reference: - a new or modified shooting move, swap move, or acceptance rule; - a change to the random-number generator, or to the order in which random numbers are drawn; - a change to the scheduler or the coordinator’s ensemble bookkeeping; - a new or modified rate / crossing-probability estimator, or a change to the high-acceptance reweighting; - a deliberate re-blessing of the reference output, where a campaign is what distinguishes “the numbers moved for the named reason” from “the numbers moved and we no longer know whether they are right”. Do not re-run it for refactors that keep the goldens byte-identical, nor for I/O, CLI, packaging, documentation, engine plumbing, new examples, or test-infrastructure work. If test-heavy.sh reproduces every committed reference byte-for-byte, the sampled distribution did not move, and the recorded campaign still describes the current code. ### How the evidence is recorded The campaign runs off CI, on whatever machines you have. campaign_run.sh takes the seed, the reverse_list flag and a wall-clock budget as arguments, so two or more machines given different seeds sample independently and combine for statistics, and it continues each case to an ever-increasing cycle target; validation_campaign.toml lists the thirteen cases at 20,000 target cycles. The recorded result is the tracked previous_results/validation_results.json, whose per-case rows carry the rate, relative error, cycle count, agreement flags, and the provenance of the host and commit that produced them. That file is the standing evidence: it stays valid until one of the changes listed above invalidates it, which is why the campaign needs to run rarely rather than per release. What CI contributes is only method_validation_smoke: one case at ten cycles, run on a throwaway copy of examples/validation so it cannot touch the recorded table. It proves the driver still executes end to end after a refactor and nothing more – ten cycles cannot resolve a rate, and the job publishes no artifacts precisely so its numbers can never be mistaken for evidence. A row is evidence only when validation_results.json records the analytical-rate agreement and confidence information at a full cycle count; job completion alone is not a scientific pass, and neither is a green method_validation_smoke. --- # Scientific evidence matrix This matrix records why a numerical result is believed, not merely where its implementation is exercised. DIRECT means a test derives an expected value by hand or from a closed form. INDEPENDENT means a second algorithm or analytical physical model reaches the same result. REGRESSION means a frozen output is useful for detecting change but is not, by itself, evidence that the value is scientifically correct. # Central numerical outputs and their independent evidence Result Implementation Independent evidence Status Common means, block errors, MSD analysis.analysis Hand-computed running means, one- and two-point block standard errors, and the squared-lag MSD in test_analysis.py. DIRECT MD crossing flux flux_analysis Hand-counted crossings per physical time window in test_flux_analysis.py; end-to-end rates are also checked by the double-well campaign. DIRECT + INDEPENDENT TIS/RETIS crossing probability path_analysis.analyse_path_ensemble and match_probabilities Accepted-path integer ratios and their explicit product in test_path_analysis.py. DIRECT RETIS flux and rate path_analysis.retis_flux and retis_rate Direct inverse-path-time and product/error-propagation formula checks; the method campaign compares the final rate with Kramers–MM theory. DIRECT + INDEPENDENT WHAM crossing probability and rate wham_analysis Hand-built unweighted and weighted matrices with exact non-zero crossing probabilities; single-ensemble agreement with point matching; Kramers–MM campaign. DIRECT + INDEPENDENT REPPTIS local/global probability repptis_analysis and _repptis_state Raw-column occupancy counts feed both a closed-form recursion and a separately constructed Markov matrix in test_repptis_reference_oracle.py. INDEPENDENT REPPTIS MSM probability and MFPT repptis_msm Symmetric-random-walk closed form, a hand-computed three-state MFPT, stochastic-matrix invariants, and comparison with the upstream implementation. DIRECT + INDEPENDENT Path weights and transmission coefficient path_weights Equal-weight fixture and explicit 8 reactive / 10 crossings = 0.8 calculation in test_path_weights.py. DIRECT Interface placement interface_placement and interface_estimation Exact geometric placement on p(x)=0.5^x, plus hand-counted binless crossing curves. DIRECT Permeability path_analysis.perm_calculations Hand-counted \xi and \tau, explicit permeability product, and quadrature error propagation in test_path_analysis.py. DIRECT Energy/order descriptive reports energy_analysis and order_analysis Frozen fixture outputs, independently tested common statistics, and the shape-one kinetic gamma law checked against 2\exp(-2E) in test_energy_analysis.py. DIRECT Cross-engine rate equivalence examples/validation/engines/water_dimer Shared-force-field energy gate, direct-MD timescale, and three-engine rate comparison documented in engine-validation. INDEPENDENT CAMPAIGN ## Scope boundary The matrix covers numerical results exposed by the analysis reports and interface tools. Plot layout, file formatting, and training-frame selection have deterministic functional tests, but are not themselves scientific estimators and are outside this oracle inventory. --- # Trustworthiness workplan ## Purpose A green test is evidence only when the test demonstrably inspected the quantity it claims to validate. This workplan turns that rule into executable gates. It complements method-validation, engine-validation, and the adversarial comparison audit in docs/test_integrity_audit.md. ## Status terms Implemented and exercised by an automated test or a reproducible command. Work is being implemented in the current change set. The code-side preparation is complete, but reference-grade software, hardware, or a deliberate reference re-baseline is still required. Required work that has not started. ## Definition of trustworthy A release is trustworthy only when all of the following are true: - required unit, integration, and external-engine jobs pass – these are the routine correctness gate, and they are what establishes that a change still behaves correctly; - the recorded method-validation campaign still applies: it is re-run only when a change can move the sampled distribution (a move, an acceptance rule, the RNG or its draw order, the scheduler, a rate estimator, or a deliberate re-blessing), not per release. Byte-identical engine goldens are the evidence that it has not moved. See method-validation; the CI method_validation_smoke job is a launch check, not evidence; - no comparison passes after inspecting zero rows, zero paths, zero swaps, an empty archive, or an unexpected all-NaN physical quantity; - every numerical reference records its generating engine, version, execution mode, command, tolerance rationale, and content digest; - each central analysis method has an analytical, hand-computed, or independently implemented oracle; - deterministic and stochastic workflows have separate contracts; - restart, load, interruption, and parallel shutdown behavior are tested as distinct semantics; - a file that cannot be read or written stops the run, instead of being absorbed into an empty read or a truncated output that still reports success; - reading a file gives the same result on every machine, independent of the locale; - running the tests does not modify the working tree, so no test can quietly rewrite a committed reference or input; - the release pipeline cannot publish when a required validation was skipped or downgraded to execution-smoke coverage. This covers the per-change gate – the unit, integration and external-engine jobs. The method-validation campaign is deliberately outside it: it needs ~36 h against a 1 h GitLab job timeout and is a rare, change-triggered check rather than a per-release one, so the pipeline neither runs nor verifies it. Judging when it must be re-run is a human call; the criteria are in method-validation. ## Workstreams ### W0. Integrate concurrent review work Review the concurrent agent’s path-validation, shooting-weight, documentation, and changelog changes without reverting or duplicating them. Focused path-analysis and replica-exchange tests pass. ### W1. Honest engine-suite outcomes Engine absence must return the conventional skip status (77), and the orchestrator must report VALIDATED, SMOKE, SKIPPED, and FAILED separately. Add PYRETIS_REQUIRE_VALIDATION=1. In this mode, a skipped or smoke-only required engine is a failing release gate rather than a green result. Exercise both permissive developer mode and strict release mode with automated driver-contract tests. ### W2. Reference integrity and physical coverage Add a machine-readable provenance manifest for external-engine references, including deterministic tree digests and explicit tolerance rationale. Require suites to declare energy terms that are intentionally unavailable. Unexpected all-NaN columns must fail, while declared unavailable columns must be checked to remain all-NaN on both sides. Report finite OpenMM potential energy from the OpenMM context and regenerate its golden data twice with the pinned OpenMM Reference platform. The two runs were byte-identical, and the release-grade suite now requires the generated files to remain byte-identical. Establish a run-to-run deterministic CP2K reference build: the conda-forge cp2k 2026.1 package (single rank, OMP_NUM_THREADS=1) reproduces the test-retis-load run byte-identically across clean runs (verified 2026-07-19); the golden was re-blessed on it and the suite validates only on that exact version/revision. Re-baseline the legacy GROMACS 5.1.4 tarballs onto the single current reference build. The whole GROMACS reference suite now targets one era (double-precision 2025.4 on the development reference box, re-blessed 2026-07-19 from a determinism-checked double run), so strict full-scope validation is satisfiable by one executable. Keep the test run out of the committed fixtures. Unit tests built engines against tracked input directories, so a run wrote pyretis.mdp and its logs into directories holding committed references, and the example suites left engine scratch in the shared input directory. Tests now work on copies under tmp_path (the example suites keep running in place and sweep with pyretis tools clean, since their goldens sit alongside). A full ./test-easy.sh now leaves the working tree byte-for-byte unchanged, verified by snapshotting every file before and after, so no test can quietly rewrite a reference or an input. Calibrate each external-engine tolerance from repeated runs on its pinned reference environment. Two strict release-gate runs on 2026-07-27 (PYRETIS_REQUIRE_VALIDATION=1 PYRETIS_GROMACS_SCOPE=full, single rank, OMP_NUM_THREADS=1) both reported six-of-six VALIDATED, and every external-engine golden was reproduced with zero observed deviation across those runs and the 2026-07-19 blessing: the byte-exact GROMACS, LAMMPS, and OpenMM gates and the PyVisA content hash pass only on byte or hash identity, and the pinned CP2K build reproduces its golden byte-identically. The operative tolerance is therefore byte identity (measured envelope 0.0); the retained numeric diagnostics (CP2K 1e-4, OpenMM 1e-5) sit orders of magnitude above the measured envelope, and the provenance manifest records the measurement per set. ### W3. Independent scientific oracles REPPTIS cutoff occupancy is checked from raw columns with hand-audited counts and two independent probability constructions. WHAM has hand-built synthetic exact-value tests, including weighted cases. The method-validation campaign compares rate estimates with the analytical Kramers/Mel’nikov-Meshkov result. Keep an evidence matrix that maps each public analysis result to at least one independent oracle and names any remaining unsupported result. ### W4. Stochastic and parallel contracts Single-worker seeded workflows retain byte-reproducibility checks; multi-worker workflows are explicitly completion-ordered and are not falsely required to be byte-identical. A swap-consistency check that examines no swaps must report zero coverage and fail when the caller explicitly requested swap validation. Promote the multi-seed method-validation campaign to a scheduled CI job with confidence intervals and stored provenance. The campaign needs ~36 h against a 1 h GitLab job timeout, so it runs off CI via examples/validation/campaign_run.sh and the pipeline carries only a ten-cycle launch check. Stored provenance (host, commit, dirty flag) is in place; the scheduled job and the cross-seed confidence interval are not. ### W5. Failure and recovery behavior Async worker exception recovery, burst submission, forced cancellation, repeated lifecycle cleanup, SIGTERM unwind, and atomic continuation writes have automated coverage. Add corruption tests for the reference manifest and explicit failures for missing or changed reference artifacts. Add controlled disk-write failure injection around run-state persistence and verify that the last durable state remains restartable. Surface file-I/O failures instead of absorbing them. FileIO used to log a failed open and keep a None handle, which iterates as end-of-file, so an unreadable or missing file was indistinguishable from an empty one; a failed write was logged and reported through a status value no caller reads, leaving a truncated energy.txt or order.txt behind a run that reported success. Both now raise. The path-ensemble reader used by pyretis analyse opened the file itself and swallowed a read error into an empty path set, so an unreadable or mid-read-truncated pathensemble.txt silently produced a wrong crossing probability; it now propagates the error too. Callers for which a missing file is a legitimate state check for it first and say so: a path loaded from sparse or external files carries no energies, and that is reported per path while the simulation continues. Pin text decoding to UTF-8. The encoding was selected by an inverted test, so every text file was decoded with the platform locale, making a read depend on the machine it ran on. The on-the-fly trajectory reader shared by the streaming LAMMPS and CP2K engines and the LAMMPS dump writer also opened text without an explicit encoding; both now pin UTF-8, so no text open decodes or encodes through the locale. The remaining swallowed I/O error is the directory fsync in inout/common.py, which weakens durability without corrupting data and is documented as such. ### W6. CI and release policy Run reference-manifest validation and strict Sphinx warnings-as-errors in normal CI. Make tagged releases depend on the strict validation jobs. Manual developer smoke jobs remain useful, but cannot satisfy a release dependency. The external-engine jobs honour this: on a release tag they switch to allow_failure: false with PYRETIS_REQUIRE_VALIDATION=1. method_validation_smoke is the one job in do_release’s needs that is a launch check rather than a validation job; it is listed to catch a broken driver at release time and is explicitly not counted as method-validation evidence (see the known exception above). Coverage is collected on CI Python 3.12, separately from the Python 3.14/NumPy combination whose tracer is currently incompatible. ## Execution gates The intended commands, from least to most expensive, are: ```bash python -m pyretis.testing.reference_manifest \ examples/tests/reference_provenance.json python devtools/docs/run_checks.py ./test-easy.sh ./test-heavy.sh PYRETIS_REQUIRE_VALIDATION=1 ./test-heavy.sh ``` In permissive mode, missing or non-reference engines are reported as SKIPPED or SMOKE and do not by themselves fail the driver. The final command is the release gate and may not pass until every required engine is installed and reference-grade, and no test marked heavy skips (for example for an lmp that is not on PATH, or the AMS case of test/infswap/engines/test_velocity_functions.py, which skips on every machine without a licensed AMS). ## Current blockers and decision points - The CP2K determinism blocker is resolved (conda-forge 2026.1, single rank, deterministic; golden re-blessed 2026-07-19). Non-reference CP2K builds still run execution-smoke only. - GROMACS references are unified on a single era (double-precision 2025.4, the development reference box, re-blessed 2026-07-19): the former 5.1.4 archive suite was regenerated on the chosen reference version. - External-engine tolerances are now measured, not provisional: repeated strict release-gate runs on the pinned reference environment (the 2026-07-19 blessing and two runs on 2026-07-27) reproduced every golden with zero observed deviation, so byte or hash identity is the operative tolerance and the retained numeric diagnostics sit far above the measured 0.0 envelope. - Reference updates are review events. A digest update must include the engine provenance, generation command, observed error envelope, and reviewer signoff in the merge request. --- # Example organization The examples tree has three top-level subfolders: examples/tutorials, examples/tests and examples/validation. Tutorials are for users who want to learn how to set up simulations. Tests are for developers who want to check that generated output still matches reference data. The validation suite holds long RETIS runs of one system, launched by hand, that must reproduce the same crossing probability and rate constant (see examples/validation/README.rst). Those goals are related, but the folder layouts inside each branch are allowed to differ. Area Purpose Rule of thumb tutorials/path_sampling/ TIS, RETIS, REPPTIS, and engine-coupling tutorials. Each directory should have a short README.rst, one primary input file, and a matching page under docs/examples/. tutorials/molecular_dynamics/ MD and flux tutorials used to prepare path sampling. Keep these small and make clear whether they are library-based scripts or pyretis run input-file examples. tutorials/umbrella_sampling/ Sampling tutorials outside the path-sampling path. Keep the current topic grouping, but add a README when a folder contains more than one runnable script. tutorials/extending/ Advanced tutorials for custom engines, force fields, and order parameters. Treat these as advanced examples and link them from both the examples pages and the developer guide where relevant. tutorials/analysis/ Python API tutorials for the analysis: pooling the data of independent runs, and the transmission coefficient. Each folder runs on its own with python