This is the primary reference for any AI agent working on this repo. Read it fully before making changes. Everything here was learned the hard way.
q2mm is a modern Python rewrite of Q2MM (Quantum-guided Molecular Mechanics). It optimizes molecular mechanics force field parameters to match quantum mechanical reference data. The codebase supports multiple computational backends (OpenMM, Tinker, JAX, JAX-MD, Psi4) and provides optimizers, objective functions, and evaluation tools for force field development.
⚠️ This project is in alpha. The rules below are non-negotiable.
Q2MM has a specific scientific lineage. Before changing anything in
q2mm/models/seminario.py, q2mm/models/forcefield.py, the
parametrization loaders in q2mm/benchmarks/systems/, or the
optimizers in q2mm/optimizers/, look at the right paper. Agents have
repeatedly cited the wrong reference here.
| Paper | DOI | What it governs |
|---|---|---|
| Farrugia, Helquist, Norrby & Wiest 2025 — "Rapid FF Generation via Hessian-Informed Initial Parameters and Automated Refinement", J. Chem. Theory Comput. 22, 469. | 10.1021/acs.jctc.5c01751 | QFUERZA — the methodology of record for q2mm/models/seminario.py. Defines the FUERZA + H-angle-substitution variant we ship; documents that torsions are intentionally zeroed at initial-parameter time; explains the "mixing literature + custom params" workflow our loaders implement. |
| Seminario 1996 — "Calculation of intramolecular force fields from second-derivative tensors", Int. J. Quantum Chem. 60, 1271. | 10.1002/(SICI)1097-461X(1996)60:7%3C1271::AID-QUA8%3E3.0.CO;2-W | Original FUERZA projection. Background only; QFUERZA above supersedes it for us. |
| Limé & Norrby 2014 (vol. 36, 2015) — "Improving the Q2MM method for transition state force field modeling", J. Comput. Chem. 36, 244–250. | 10.1002/jcc.23797 | TS Hessian inversion (invert_ts_curvature=True) and Method E2. See the "TS Hessian Inversion" warning below. |
| Donoghue, Helquist, Norrby & Wiest 2008 — J. Chem. Theory Comput. 4, 1313. | 10.1021/ct800132a | Rh-enamide TSFF reference paper. Governs examples/publication/rh-enamide/, load_rh_enamide, and the 9-molecule benchmark system. |
| Rosales, Helquist, Norrby & Wiest 2020 — J. Am. Chem. Soc. 142, 9700. | 10.1021/jacs.0c01979 | Heck-relay TSFF reference paper. Governs load_heck_relay and the 23-molecule benchmark system. |
| Wahlers, Ph.D. Dissertation, University of Notre Dame, 2021. | 10.7274/k930bv76q4n | pd-allyl, pd 1,4-conjugate-addition, rh 1,4-conjugate-addition TSFFs, plus the Chapter 4 Ferrocene GSFF. |
If you propose changes to the parametrization or loader code without having consulted Farrugia 2025, stop and read it first. Many of the loader-API decisions (frozen params, OPT-only re-estimation, the distinction between literature and custom parameter values) are deliberate implementations of the QFUERZA workflow and will not make sense otherwise.
Q2MM handles transition state (TS) systems by inverting the QM Hessian's
negative eigenvalues before Seminario projection (invert_ts_curvature=True).
This produces a force field with all-positive force constants where the TS
geometry is a proper minimum on the MM potential energy surface — not a
saddle point.
Consequences that agents repeatedly get wrong:
-
Unconstrained geometry relaxation IS correct for TS systems.
_relax_coords()uses unconstrained L-BFGS to find the MM minimum. After Hessian inversion, that minimum should be near the QM saddle-point geometry. Do NOT claim that "relaxation is wrong for TS" or "TS structures sit at saddle points on the MM PES" — that is false after inversion. -
When relaxation diverges, the problem is the starting FF, not the method. If Seminario produces deeply negative R² (e.g., heck-relay bond_length R² in the tens of negatives), the force constants are so wrong that the MM PES has its minimum far from the QM geometry. The fix is to improve the starting FF, not to change the relaxation approach.
-
Reference: Limé & Norrby, J. Comput. Chem. 2014 (vol. 36, 244–250, 2015). 10.1002/jcc.23797
This is a pre-release codebase aiming for lean perfection. Zero tolerance for bloat, duplication, or deprecated artifacts.
- One canonical location for each kind of data. Before creating a new directory or file, check if one already exists. If it does, use it.
- No duplicate directories. If two directories serve the same purpose, merge them immediately. Do not leave both "until later."
- No deprecated artifacts. If something is superseded, delete the old version in the same commit. Do not keep it "just in case."
- Every file earns its place. If you can't explain why a file exists and what would break without it, it probably shouldn't be there.
- Data that is documented must be tracked. If a number appears in documentation, the data backing it must be committed to the repo. Untracked data does not exist for publication purposes.
- Know before you write. Before rewriting a page or creating a file, gather the full picture — check all related directories, issues, PRs, and prior work. A rewrite based on partial context will introduce errors.
- No one-off or timestamped directories. Output goes to the canonical location. If the canonical location doesn't exist yet, create it with a permanent name.
- Every claim must be grounded in evidence. Treat all documentation as if it will be published in a scientific journal. Every number needs a traceable source (a JSON file, a log, a paper). Do not embellish, glorify, or editorialize — a fork is a fork, not "a fork that preserves the earlier implementation." If you cannot cite evidence for a claim, do not make it.
- Every comparison claim must link to its substantiation. In tables or lists that compare features, each row must link to the documentation page or section that provides the full details. A claim without a link to supporting detail has no value — the reader cannot verify it.
- Write for a first-year graduate student. The target audience is a 22-year-old who has never used Q2MM or force field parameterization tools. Every page must open with what this is and why anyone should care, before diving into details. Avoid jargon without explanation. If a page references concepts from other pages (e.g., "Check 1"), define them inline or link to the definition. Never assume the reader has context you haven't provided on the page itself.
Run the exact same lint and format checks that CI runs. If either fails, fix the issues before committing.
python -m ruff check q2mm/ test/ scripts/python -m ruff format --check q2mm test scripts examplespython -m mypy q2mm/mypy is enforced repo-wide in the CI Lint job via a grandfathered ratchet
(see the [[tool.mypy.overrides]] block in pyproject.toml): the modules
listed there with pre-existing type errors are exempt, so any new module —
or any module removed from that grandfather list — must be mypy-clean.
[tool.mypy] pins python_version = "3.12" (the CI Lint job's Python) as a
single source of truth, so the command above matches CI with no extra flags.
3.12 is required because numpy ≥2.5 ships PEP 695 type aliases in its stubs
that mypy cannot parse under a 3.10 target; the ≥3.10 runtime floor is still
enforced by ruff (target-version=py310) and the 3.10 test-core matrix.
python -m pytest test/ -x -q -m "not (openmm or tinker or jax or jax_md or psi4)"By default, all tests run on CPU only — no GPU memory is allocated. This prevents JAX CUDA initialization (~25 GiB VRAM) and OpenMM CUDA platform selection from locking up the developer's machine.
To opt into GPU execution for benchmarks or GPU-specific tests:
python -m pytest --gpu # CLI flag
Q2MM_USE_GPU=1 python -m pytest # environment variableuv sync --extra docs # install docs deps once
uv run properdocs serve -f properdocs.yml # live preview
uv run properdocs build -f properdocs.yml # static build to site/
⚠️ Always useuv run properdocs, not a globally-installed one. A standalone install at~/.local/bin/properdocsmay be missing transitive deps (notablyyaml_env_tag→pyyaml-env-tag) and will fail on import. Thedocsextra pulls the full toolchain (properdocs,mkdocs-material,mkdocstrings[python],mkdocs-gen-files,mkdocs-literate-nav,mkdocs-section-index, and transitive deps).
scripts/ci_local.sh --allThis runs the full CI matrix locally inside Docker containers.
This is where agents keep failing. Read this section carefully.
- Editing, linting, formatting, and non-GPU tests all work.
- OpenMM CPU works.
- JAX CPU works.
- JAX CUDA and JAX-MD are NOT available on Windows — they are excluded in
pyproject.toml. Do not attempt to install or use them.
-
Full GPU stack is available: OpenMM CUDA, JAX CUDA (5.6× speedup), JAX-MD.
-
All verified GPU benchmarks were run here.
-
To enter the WSL2 GPU environment:
wsl -d Ubuntu-24.04 source /home/eric/repos/q2mm/.venv/bin/activate
Always run these checks before any benchmark or GPU-dependent work:
# Must show "CUDA" in the platform list
python -c "import openmm; [print(openmm.Platform.getPlatform(i).getName()) for i in range(openmm.Platform.getNumPlatforms())]"
# Must show CudaDevice (not CpuDevice)
python -c "import jax; print(jax.devices())"⛔ If OpenMM shows OpenCL instead of CUDA, STOP. Do not run benchmarks on OpenCL. Install
openmm-cuda-12or switch to WSL2.
- Never push directly to
mainormaster— always use a feature branch + PR. - Branch naming:
<type>/<short-description>(e.g.,feat/jax-optimizer,fix/openmm-parity). - Conventional commit prefixes:
feat,fix,docs,refactor,chore,test,ci,perf.
- Verify GPU platform first — see §4. No exceptions.
- Use WSL2 for all GPU benchmarks.
- Output is always persisted — the
q2mm-benchmarkrunner has no--no-save; every candidate lands under<output>/candidates/and each accepted candidate is promoted to<output>/accepted/and<output>/forcefields/. Pass--outputto choose the location. - Save outputs to a local results directory such as
results/<system>/(e.g.,results/ch3f/,results/rh-enamide/). Archive the canonical benchmark artifacts inericchansen/q2mm-dataunderbenchmarks/<system>/. Never create one-off or timestamped directories — keep one canonical location per system. - Benchmark data is tracked in git — commit benchmark artifacts to the
separate
ericchansen/q2mm-datarepo, not this code repo. Any data referenced in documentation must be tracked there. If it's not tracked, it doesn't exist for publication purposes. - Run sequentially on an idle system for consistent timing.
For the frozen repository compatibility objective, use
ObservationSet.from_molecules() (q2mm.models.observations). This
auto-populates bond lengths, bond angles, and Hessian eigenmatrix references
under repository-geometry-eigenmatrix-v1. That objective is a partial
repository reproduction: the governing sources additionally use
electrostatic, torsional, relative-energy/enthalpy, scan, or parameter-tether
targets depending on the system. See
validation/published_ffs/README.md.
_build_frequency_reference() builds frequency-only references. This is
appropriate for the CH₃F full-matrix benchmark and ground-state force
fields, but not for TS publication reproduction. The papers use a
multi-target penalty (geometry + Hessian + charges + energies), not
frequency RMSD.
q2mm.benchmarks.systems.load_system() and the q2mm-benchmark runner
accept a starting_point of either "qfuerza" (canonical default) or
"published":
"qfuerza"(default, Farrugia 2025) — the FF skeleton (atom types, OPT-substructure topology, frozen/active partition, vdW, stretch-bend) comes from the literature.fldfile; QFUERZA fills in the bond and angle force constants and equilibria from the QM Hessian. This is the standard QFUERZA workflow: the chemist provides the skeleton (no tool can automate the decisions of which atom types to use or which substructure rows need OPT parameters); QFUERZA fills in the Hessian-derivable scalars."published"— the literature OPT values are retained verbatim; no QFUERZA overwrite. Pass this to reproduce historical publication-baseline runs.
The runner is invoked as q2mm-benchmark single/batch/matrix with the
same --starting-point flag; the RunProfile.starting_point field records
which path a run used, and the resolved run provenance embeds it.
All TS system loaders must pass invert_ts_curvature=True when
building the QFUERZA starting force field (q2mm.models.seminario.qfuerza_fresh/
qfuerza_into). Without this, the reaction-coordinate eigenvalue stays
negative, producing a saddle-point force field where geometry
minimization blows up. If you see "negative FC (TS reaction
coordinate?)" warnings, TS inversion is missing.
Published papers only optimize the OPT substructure parameters near
the metal center (~182–488 params), not the full composed force field
(~2,700–3,200). Use q2mm.models.parameters.opt_substructure_membership(ff, opt_ff)
ActiveParameterSpace.from_membership(layout, ff, membership)to derive which base-FF parameters are frozen — frozen/active state lives entirely inActiveParameterSpace, never on the (immutable)ForceFielditself. All three optimizers (SciPy, Optax, JaxOpt) take thisActiveParameterSpaceas their required secondoptimize()argument and respectspace.n_active.
q2mm separates the immutable fitting plan from the backend-specific executor:
build an ObjectivePlan, attach either a PythonObjectiveExecutor or a
JaxObjectiveExecutor, then pass that evaluator plus the
ActiveParameterSpace to optimizer.optimize(evaluator, space). Gradient
selection is explicit — there is no jac auto-detection and no silent
finite-difference fallback.
| Pipeline | Executor and optimizer use | Geometry gradients | When to use |
|---|---|---|---|
| SciPy + JAX objective executor (recommended) | JaxObjectiveExecutor(plan, JaxBackend(), ff) with ScipyOptimizer(method="L-BFGS-B").optimize(evaluator, space) |
Analytical (per-case JIT) | All TS systems — fastest, most robust |
| Python executor, scalar-only | PythonObjectiveExecutor(plan, backend, ff, gradient_mode=GradientMode.NONE) with SciPy's internal finite differences |
SciPy FD (slow) | Frequency-only objectives, small systems, non-JAX backends |
| Python executor, explicit FD | PythonObjectiveExecutor(..., gradient_mode=GradientMode.FINITE_DIFFERENCE) |
Executor FD (slow) | When FD evaluations must be counted by the executor |
| JAX-native optimizer path | JaxObjectiveExecutor with JaxOpt or Optax |
Analytical (implicit diff / autodiff) |
JaxObjectiveExecutor compiles each case's loss function independently
(per-case JIT split), then dispatches them from Python and sums — no single
XLA program contains all molecules. This prevents compilation OOM on
multi-molecule systems.
- SciPy + JAX objective executor (recommended): build
JaxObjectiveExecutor(plan, backend, starting_ff)yourself and pass it toScipyOptimizer(method="L-BFGS-B").optimize(evaluator, space). The optimizer seesGradientMode.ANALYTICALand feeds analytical gradients toscipy.optimize.minimize(..., jac=True). - Python executor: build
PythonObjectiveExecutor(plan, backend, starting_ff, gradient_mode=GradientMode.NONE)to let SciPy compute internal finite differences, or chooseGradientMode.FINITE_DIFFERENCEfor executor-owned finite differences. - JaxOpt uses
jit=False+value_and_grad=Truesosolver.run()executes a Python while-loop, dispatching per-molecule compiled functions at each step. - Optax calls
JaxObjectiveExecutor.value_and_grad_jax()directly (Python dispatch) instead of wrapping injax.jit(jax.value_and_grad(...)).
linesearch="zoom" triggers 30–60 minutes of additional
XLA compilation beyond the JAX executor JIT phase. This makes jaxopt
impractical for convergence runs. Use scipy-lbfgsb-jax instead —
it completes in seconds post-JIT.
Runtimes on RTX 5090 GPU (WSL2). Each system's time is JIT + optimization.
| Benchmark | JIT | Optimization | Total |
|---|---|---|---|
| SciPy L-BFGS-B + JAX objective executor (GPU, recommended) | |||
| Rh-enamide (9 mol, 182 params) | 95 s | 6 s | ~2 min |
| Heck relay (23 mol, 462 params) | 249 s | 18 s | ~5 min |
| Pd-allyl (21 mol, 482 params) | 227 s | 13 s | ~4 min |
| Pd 1,4-conjugate (10 mol, 340 params) | 120 s | 9 s | ~2 min |
| Rh 1,4-conjugate (10 mol, 488 params) | 122 s | 10 s | ~2 min |
| All 5 systems sequentially | — | — | ~15 min |
| jaxopt L-BFGS (DO NOT USE) | 95–250 s | 30–60 min zoom linesearch JIT | hours |
| OpenMM OpenCL | — | — | DO NOT USE — 14% GPU utilization |
# Check OpenMM platforms (must show CUDA for GPU work)
python -c "import openmm; [print(openmm.Platform.getPlatform(i).getName()) for i in range(openmm.Platform.getNumPlatforms())]"
# Check JAX GPU (must show CudaDevice)
python -c "import jax; print(jax.devices())"
# Check GPU utilization
nvidia-smi
# Run core tests (no backends)
python -m pytest test/ -x -q -m "not (openmm or tinker or jax or jax_md or psi4)"
# Run integration tests (local only — too slow for GitHub-hosted CI)
python -m pytest --run-integration -q
# Run validation tests (local only — not in CI)
python -m pytest --run-validation -q
# Run nightly tests (local only — optimizer loops, full loops, slow)
python -m pytest --run-nightly -q
# Run lint + format checks
python -m ruff check q2mm/ test/ scripts/
python -m ruff format --check q2mm test scripts examples
# Generate golden fixture (opt-in, requires --run-validation)
Q2MM_UPDATE_GOLDEN=1 python -m pytest test/integration/test_published_ff_validation.py --run-validation -vAlways validate publication years and metadata via Zotero MCP, not
raw CrossRef API fields. CrossRef exposes multiple date fields
(issued, published-print, published-online, created) that can
disagree. The Zotero library is the authoritative source for citation
metadata in this project.
- Use
zotero_search_itemsorzotero_get_item_metadatato look up papers. - If a paper is not in Zotero, add it first (
zotero_add_by_doi), then use the Zotero-resolved metadata. - Never use CrossRef
published-printas the citation year — use theissueddate or, better, the year already recorded in Zotero.
| Pitfall | What Happens | Fix |
|---|---|---|
| Duplicate directories | Data gets fragmented, docs reference wrong source, context is lost | Check if a canonical location exists before creating anything new (§2) |
| Untracked data in docs | Numbers in docs can't be verified from the repo | Commit all data that documentation references (§2) |
| One-off / timestamped dirs | grad_simp_jax_fix_test/, results_2026-04-03/ — impossible to find later |
Use the canonical directory; delete one-offs immediately after merging |
| OpenCL ≠ CUDA | Benchmark shows OpenMM (OpenCL) — 14% GPU utilization, hours wasted |
Install openmm-cuda-12 or use WSL2 |
| JAX on Windows | JAX CPU works but JAX CUDA is excluded in pyproject.toml |
Use WSL2 for GPU |
| Assuming output is optional | Expecting a --no-save flag that no longer exists |
The q2mm-benchmark runner always persists every candidate; choose the location with --output (default ./results) |
| Long benchmarks | OpenMM L-BFGS-B can take hours | Check CPU/GPU utilization periodically with nvidia-smi |
| Rewriting without full context | Page rewrite introduces errors because not all data sources were checked | Gather ALL related dirs, issues, PRs, and prior work before rewriting (§2) |
| Wrong publication year | CrossRef has multiple date fields that disagree; using the wrong one corrupts citations | Always validate via Zotero MCP (§10) |
JIT-wrapping JaxObjectiveExecutor internals |
Wrapping the full multi-case objective in another jax.jit() or jax.vmap() re-inlines all molecules into one XLA program → compilation OOM |
Use JaxObjectiveExecutor.value_and_grad_jax() (Python dispatch) or JaxObjectiveExecutor.value_and_gradient() instead. See §6 Objective Executors and Gradient Selection. |
| Frequency-only refs for TS | Misses geometry + eigenmatrix targets that the papers actually used → wrong optimization | Use ObservationSet.from_molecules() + invert_ts_curvature=True for TS systems. See §6 Reference Data. |
| jaxopt zoom linesearch | jaxopt ≥ 0.8.5 LBFGS default linesearch="zoom" triggers 30–60 min extra XLA compilation post-JIT |
Use scipy-lbfgsb-jax instead — same L-BFGS-B algorithm, completes in seconds post-JIT |
| TS executor-ratio gate | Literature-scale TS systems can have JAX/Python executor ratios of 0.1–0.4 from poor starts; the optimizer no longer falls back silently | Pass --executor-ratio-tol none for documented TS runs when intentionally bypassing the gate. CLI key: scipy-lbfgsb-jax |
| Heck relay bounds | ±20% bounds cause 35–92% NaN rate due to fragile TS landscape with large negative FCs (−3753) | Use ±5% bounds for heck-relay specifically |
n_iterations<=2 silent exit |
L-BFGS-B "converges" after 0–2 iterations with negligible objective-executor score change — the optimizer didn't optimize. Common with from-poor-start runs and loose ftol. |
Tighten --ftol (e.g. 1e-12); apply --fc-fraction/--eq-fraction to keep optimizer in starting basin; check the executor_ratio. The diagnostic warning in scipy_opt._run_minimize flags this. |
| Default sanity bounds for from-poor-start runs | DEFAULT_BOUNDS (bond_k ±3600, bond_eq 0.5–3.0 Å) let L-BFGS-B escape the QFUERZA / random-default starting basin → final FF unrelated to start |
Use ScipyOptimizer(fc_fraction=0.20, eq_fraction=0.05) (or CLI flags --fc-fraction --eq-fraction) to bound each param to a ± fraction of its current value. |
| Benchmark batch never optimized | All systems exit at nfev≤2 with no OF change but the batch reports "success" |
The runner rejects no-progress candidates and run_profiles returns outcome.ok is False when optimizations ran but none was accepted (q2mm/benchmarks/runner.py). Re-tune ftol or bounds and re-run. See §11. |
validation/supporting-info/ (gitignored, ~1.9 GB) contains supporting data
from the Wahlers and Rosales Notre Dame dissertations: 996 Gaussian DFT logs,
12 published MM3* force fields, and 134 MacroModel structures across 10
reaction systems. See validation/supporting-info/README.md for provenance,
file inventories, and how to obtain the zip files.
validation/published_ffs/mm3_base.fld is the standard, unmodified MM3
force field (1,850 lines). It provides backbone parameters (bonds, angles,
torsions, vdW) that all Q2MM systems build on.
- Not committed — the header says "Copyright Columbia University 1990 — All rights reserved", which does not grant redistribution rights.
- Source: download from atlas-nano/ATLAS_toolkit
ff/macromodel/mm3.fldor extract from a MacroModel installation. - Identical to the base section of
examples/publication/rh-enamide/mm3.fld(the first ~1,850 lines before Rh-specific additions).
Some published FFs (Wahlers dissertation) are standalone OPT-substructure-only files (71–238 lines) containing only custom parameters, not the full MM3 backbone. To build a complete, evaluable force field:
- Start with
mm3_base.fld(gitignored, see above for how to obtain) - Insert metal-specific vdW entries before "END OF NONBONDED INTERACTIONS"
- Append the OPT substructure blocks from the standalone
.fldfile
Rosales FFs are already complete (full MM3 base + OPT substructures in one file, ~2,000 lines) and do not need composition.
- Write a
load()function in a new module underq2mm/benchmarks/systems/(e.g.q2mm/benchmarks/systems/my_system.py) following the pattern ofrh_enamide.py— load molecules, build anOptimizationProblem(viaq2mm.benchmarks.systems._shared'sassemble_published_case/assemble_qfuerza_fresh_casehelpers), and return aBenchmarkCase. - Register the key -> module mapping in
q2mm/benchmarks/systems/__init__.py's_REGISTRYdict. - Create a validation test under
test/integration/following the pattern oftest_published_ff_validation.py— load published FF viaq2mm.benchmarks.systems.load_system(...), evaluate withJaxBackend, compare QM vs MM, pin results in a golden fixture JSON. - Mark with
@pytest.mark.validationso it runs with--run-validation. - If the system depends on external (gitignored) data, the loader must skip gracefully when files are absent.
- Document in
validation/published_ffs/README.md.
See validation/published_ffs/README.md for the canonical source-linked table.
As of July 2026, no row is an exact publication reproduction. Five existing
publication systems retain their frozen partial objectives, the canonical
Wahlers Chapter 4 Ferrocene seven-ground-state-structure profile is
provisionable, and missing source categories/cases remain explicit blockers.
OsO₄, Ru ketone, and sulfone remain blocked historical records.
Before launching any q2mm batch >30 min (e.g.
q2mm-benchmark batch/matrixon >1 system, or any from-scratch FF generation), walk through every step below.
Many hours of GPU time have been wasted on batches where the optimizer never actually optimized. The pattern is silent — scipy reports success=True, the runner writes its JSON, and the misleading result is only caught during post-hoc analysis. This checklist prevents that.
-
Write a measurable success spec in your plan/PR description before launching:
- What metric defines "the optimizer worked"? Use objective movement, executor agreement, convergence, and weighted category losses — never an iteration count or a single favorable R².
- What does the comparison table look like? Mock the rows/columns now.
- If the user asked a specific scientific question (e.g. "do params end up near published?"), restate it verbatim and map each metric back to the question.
-
Sanity-check optimizer config for the starting point:
- Canonical default (QFUERZA-start,
starting_point="qfuerza"): use--ftol 1e-12and--fc-fraction 0.20 --eq-fraction 0.05(or--fc-fraction 0.05for heck-relay). - Publication-baseline (
--starting-point published):ftol=1e-8, sanity bounds → fine (starting FF is already in the published basin). - Always pass
--executor-ratio-tol nonefor TS systems when intentionally bypassing the executor-ratio gate (ratios can be 0.1–0.4).
- Canonical default (QFUERZA-start,
-
Verify GPU + device (§4 + §7):
python -c "import jax; print(jax.devices())"→ must showCudaDevice. -
Run the FIRST system alone. Do NOT launch all systems sequentially in one call.
The current publication SDK marks canonical relaxed-geometry optimization as
blocked_methodology. Until a separate scientific policy defines local-basin semantics, run only preparation/evaluation and bounded optimizer-entry/save proofs; do not launch the full smooth-gradient matrix. -
AUDIT GATE — read the first candidate's persisted record (
<output>/candidates/<stem>.json, itssummary) before launching the rest:improvement_pct >= 1%on the Python objective-executor score of record (notfinal_optimizer_score).- Both initial and final JAX/Python executor ratios are in
[0.1, 10]. convergedis true. A large objective decrease does not excuse an unconverged optimizer or inner geometry solve.- Compare
initial_category_scoreswithfinal_category_scores. No weighted objective category may regress by more than 1% of the initial total objective. R²/RMSD remain descriptive diagnostics; they do not vote independently of the objective weights, and off-diagonal eigenmatrix R² is unstable when reference variance is near zero. - The candidate
statusisaccepted(notrejected/skipped/error). - If ANY of these fail, STOP. Re-tune and re-run the single system. Do not launch the batch.
-
Launch remaining systems. Check
nvidia-smiperiodically (>50% utilization expected). -
Post-batch validation — read every candidate record under
<output>/candidates/, not just the console summary.run_profileslogs a batch-level failure and returnsoutcome.ok is Falsewhen optimizations ran but none was accepted; treat that as a hard failure even if individual files exist.
These exist to back up the checklist; do not rely on them alone:
q2mm/optimizers/scipy_opt.py::_run_minimize— WARNING whenn_iterations<=2and|delta|/init<0.01q2mm/objectives/jax.py::_relax_coords— rejects unconverged inner geometries; implicit differentiation is valid only at a stationary inner solutionq2mm/benchmarks/publications.py::PublicationOptimizationSuccessSpec— canonical publication runs require convergence, executor agreement, substantive total improvement, and bounded weighted-category regressionq2mm/benchmarks/acceptance.py::NoProgressPolicy.made_progress— the single no-progress decision that rejects a stalled candidateq2mm/benchmarks/runner.py::run_profiles— logs BATCH FAILURE and reportsoutcome.ok is Falsewhen optimizations ran but none was accepted.copilot/skills/q2mm-benchmark/SKILL.md— agent skill that walks through this checklist automatically before launching any batch.copilot/skills/q2mm-analysis-design/SKILL.md— agent skill that forces design-first analysis methodology before writing comparison docs
q2mm.backends.registry discovers backends lazily through one validator. Both
in-tree built-ins and out-of-tree plugins are declared as JSON-safe manifest
mappings with exactly backend_api_version, name, role,
capability_ceiling, functional_form_ceiling, factory, and optional
probe. They are validated by q2mm.backends.discovery.validate_manifest,
which produces a BackendDescriptor. This is the public, versioned
BACKEND_API_VERSION == 1 authoring contract. Pre-v1 field names, role qm,
BackendRole.QM, and QM*Request names have no compatibility aliases.
- Entry-point group: out-of-tree plugins advertise one entry point in the
q2mm.backendsgroup whose value targets a lightweight descriptor module (the manifest mapping or a zero-arg provider returning it). Enumeration and provider loading import only that descriptor module; the backend implementation is imported solely when the manifest'sfactorystring is resolved by an explicitload_backend/BackendDescriptor.load. - Lazy + cached: importing
q2mm.backends.registrydoes not enumerate entry points or import descriptor/implementation modules. The firstdescriptors/catalog/registered_backends/available_*/get_descriptor/load_backendcall builds one deterministic snapshot;registry.refresh()rebuilds it (tests, newly-installed plugins). - Failure isolation: missing dependency, descriptor import error,
incompatible API version, duplicate name, invalid descriptor/capability/form,
and broken factory become typed
DiscoveryRecords (seeDiscoveryIssueKind) and never hide a healthy built-in or external plugin. Built-in names win conflicts; two externals claiming one name are all rejected. A broken factory is discovered only on explicit load. - Strict validation: unknown keys, incompatible/non-integer versions, unsafe or entry-point-mismatched names, invalid roles/capabilities/forms, non-JSON-safe data, and malformed factory/probe values are rejected.
- Ceilings: descriptor capabilities/forms are static ceilings. Runtime
BackendInfogives authoritative exact subsets; role equality is mandatory and runtime overclaims are rejected. Provenance identifies descriptor name and role. - Conformance + example:
q2mm.backends.conformanceprovides immutable typed MM/reference cases and deterministic outcomes without optional runtime imports. The canonical independently installable plugin isexamples/backend-plugin/; it is excluded from Q2MM wheel and sdist.scripts/check_release_artifacts.pyinstalls it against a built wheel and printsexternal-plugin=ok.