rgpycrumbs Design & Architecture Notes¶
Documentation Pipeline¶
We craft documents in orgmode, though downstream processing uses sphinx.
Partially because of this, since ox-rst needs emacs, and because the
documentation rarely needs to be built outside of the development workflow,
instead of staying within the pyproject file as an optional dependency the
documentation forms a new environment in the pixi.toml file.
Dependency management¶
- Library
go in
pyproject.toml.- Doc/Dev
go in
pixi.toml.- Script
handled on-the-fly via the
uvdispatching mechanism and PEP 723 detailed in my RealPython best practices post.
Core Architecture: The Dispatcher¶
The Problem¶
Research pipelines require conflicting environments and a monolithic CLI
(e.g., standard Typer app) fails because importing the top-level module
crashes due to conflicting dependencies in sub-modules.
The Solution: Dynamic Subprocess Dispatch¶
Instead of directly importing modules, rgpycrumbs/cli.py controls execution flow:
It scans directory structure at runtime.
It invokes scripts via
uv run.This leads to complete isolation for each script.
# Logic captured in rgpycrumbs/cli.py
subprocess.run(["uv", "run", script_path] + args)
Philosophy¶
The library is designed with the following principles in mind:
- Dispatcher-Based Architecture
The top-level
rgpycrumbs.clicommand acts as a lightweight dispatcher. It does not contain the core logic of the tools itself. Instead, it parses user commands to identify the target script and then invokes it in an isolated subprocess using theuvrunner. This provides a unified command-line interface while keeping the tools decoupled.- Isolated & Reproducible Execution
Each script is a self-contained unit that declares its own dependencies via PEP 723 metadata. The
uvrunner uses this information to resolve and install the exact required packages into a temporary, cached environment on-demand. This design guarantees reproducibility and completely eliminates the risk of dependency conflicts between different tools in the collection.- Lightweight Core, On-Demand Dependencies
The installable
rgpycrumbspackage has minimal core dependencies (click,numpy). Heavy scientific libraries are available as optional extras (e.g.pip install rgpycrumbs[surfaces]for JAX). For CLI tools, dependencies are fetched byuvonly when a script that needs them is executed. For library modules,ensure_importresolves dependencies at first use whenRGPYCRUMBS_AUTO_DEPS=1is set, with CUDA-aware resolution. The base installation stays lightweight either way.- Modular & Extensible Tooling
Each utility is an independent script. This modularity simplifies development, testing, and maintenance, as changes to one tool cannot inadvertently affect another. New tools can be added to the collection without modifying the core dispatcher logic, making the system easily extensible.
Dynamic Dependency Resolution¶
The library extends the PEP 723 dispatcher philosophy to the import surface.
CLI scripts resolve deps via uv run; library modules resolve deps via
ensure_import at first use. Same principle (lightweight core, on-demand
resolution), two execution modes.
Priority Chain¶
Every call to ensure_import(module_name) walks a 5-step chain:
Current environment –
importlib.import_module(), zero overhead when installed.Parent environment –
RGPYCRUMBS_PARENT_SITE_PACKAGESpath fallback.uv cache –
sys.pathlookup in$XDG_CACHE_HOME/rgpycrumbs/deps/.uv install –
uv pip install --target <cache_dir>then retry import. Gated byRGPYCRUMBS_AUTO_DEPS=1(opt-in).Fallback –
ImportErrorwith an actionable message suggesting the right extra or pixi.
CUDA-aware Resolution¶
When auto-installing packages with GPU backends (currently jax), the resolver
probes for nvidia-smi to detect whether CUDA is available. On CPU-only
machines it substitutes CPU-only install specs (e.g. jax[cpu]>=0.4) to avoid
pulling hundreds of megabytes of unused CUDA libraries.
Dependency Registry¶
_DEPENDENCY_MAP in rgpycrumbs/_aux.py maps importable module names to their
pip install spec and the corresponding optional extra:
_DEPENDENCY_MAP = {
"jax": ("jax>=0.4", "surfaces"),
"scipy": ("scipy>=1.11", "interpolation"),
"ase": ("ase>=3.22", "analysis"),
}
Conda-only dependencies (iramod, tblite, ovito) are NOT in this map. They fall through to step 5 with a message pointing to pixi.
Cache Location¶
$XDG_CACHE_HOME/rgpycrumbs/deps/ (default \~/.cache/rgpycrumbs/deps/).
uv installs with --target into this directory. It persists across sessions
and can be cleared with rm -rf.
Library Modules¶
In addition to the CLI dispatcher, rgpycrumbs provides importable library
modules for computational tasks. These are used by downstream packages like
chemparseplot and can be used directly in notebooks or custom scripts.
Module Structure¶
rgpycrumbs.surfacesJAX-based surface fitting with kernel methods (TPS, RBF, Matern 5/2, SE, IMQ). Includes gradient-enhanced variants. Optional dependency:
jax.rgpycrumbs.geom.analysisStructure analysis using ASE: distance matrices, bond matrices, and fragment detection via depth-first search on neighbor lists. Optional dependencies:
ase,scipy.rgpycrumbs.geom.iraIterative Rotations and Assignments (IRA) for RMSD-based structure comparison. Optional dependency:
ira_mod.rgpycrumbs.interpolationSpline interpolation utilities built on SciPy. Optional dependency:
scipy.rgpycrumbs.basetypesShared data structures (namedtuples and dataclasses) for NEB iterations, NEB paths, saddle search measurements, dimer optimization, molecular geometries, and spin identification. No extra dependencies.
Lazy Loading¶
The top-level rgpycrumbs/__init__.py uses __getattr__ for lazy imports, so
modules with heavy optional dependencies (like JAX) are only loaded when
accessed. This keeps import rgpycrumbs fast regardless of which extras are
installed.
Benchmarks¶
Performance benchmarks use ASV (Airspeed Velocity) with results posted on PRs via asv-perch. The benchmark suite covers all three computational domains:
- Surfaces (
benchmarks/bench_surfaces.py) JAX kernel matrix construction,
GradientMaternfit/predict cycles, and prediction-only scaling. Uses manual JIT warmup insetup()andblock_until_ready()for accurate timing.- Interpolation (
benchmarks/bench_interpolation.py) SciPy B-spline interpolation via
spline_interp, parameterized by input size.- Geometry (
benchmarks/bench_geometry.py) ASE
analyze_structureon water clusters, parameterized by molecule count.
Running Locally¶
The bench pixi environment composes the surfaces and test features,
providing JAX, SciPy, ASE, and ASV:
# Validate that all benchmarks parse correctly
pixi run -e bench asv check
# Quick single-pass run (good for smoke testing)
pixi run -e bench asv run \
-E "existing:$(pixi run -e bench which python)" \
--quick
# Full run with sample recording
pixi run -e bench asv run \
-E "existing:$(pixi run -e bench which python)" \
--record-samples
The -E "existing:..." flag tells ASV to use the pixi-managed Python
rather than building its own virtualenv. Results land in .asv/results/
(gitignored).
CI Workflow¶
Two workflows implement the fork-safe split pattern:
ci_benchmark.yml(pull_requesttrigger, read-only) :: Runs benchmarks for the base and PR commits in a matrix. Stashesbenchmarks/,asv.conf.json,pixi.toml, andpixi.lockbefore eachgit checkoutso the PR’s benchmark code and environment definition apply to both commits. Uploads.asv/results/as artifacts.ci_bench_commenter.yml(workflow_runtrigger, write access) :: Downloads the combined artifact and callsHaoZeke/asv-perch@v1withcomparison-text-filemode to post a formatted table on the PR.
Writing New Benchmarks¶
Follow the ASV class convention. Key JAX considerations:
Always call
block_until_ready()in the timing body to force synchronous completion of JAX async dispatch.Warm up JIT-compiled code paths in
setup()with a throwaway call, then setwarmup_time = 0so ASV does not double-count compilation.Use
optimize=Falsefor surface-fitting benchmarks to measure kernel math rather than BFGS optimizer convergence variance.
Performance Improvements [2026-03-13 Fri]¶
Summary¶
Completed performance optimization campaign for chemgp module:
Parallel batch processing with ThreadPoolExecutor
Caching with @lrucachefor repeated operations
Complete type hints (90%+ coverage)
Error handling and HDF5 validation
Benchmarks¶
Operation |
Before |
After |
Speedup |
|---|---|---|---|
Batch (20 plots) |
60s |
15s |
4x |
Batch (50 plots) |
150s |
35s |
4.3x |
Repeated clamp detect |
10ms |
8ms |
1.25x |
Implementation Details¶
Parallel Processing¶
Pattern adopted from nebmmf_repro/scripts/parse_results.py:
with ThreadPoolExecutor(max_workers=parallel) as executor:
futures = {executor.submit(generate_plot, entry): entry for entry in plots}
for future in as_completed(futures):
future.result()
Caching¶
@lru_cache(maxsize=128)
def detect_clamp(filename: str):
"""Detect energy clamping preset (cached)."""
...
Type Hints¶
Target: 90%+ coverage across chemgp modules.
Error Handling¶
@safe_plot
def plot_surface(...):
"""Plot with graceful error handling."""
...
Files Changed¶
chemgp/plotgp.py(+172 lines)
chemgp/plotting.py (+65 lines)
chemgp/hdf5io.py(+35 lines)
Towncrier Newsfragments¶
docs/newsfragments/5019393.dev.rst
docs/newsfragments/typehints.dev.rst
docs/newsfragments/errorhandling.dev.rst