Adopting noodlelab in an existing project¶
These are the steps for an agent asked something like “extend my project to use noodlelab so I can program with AI” or “make these calculations verifiable”. Each step is small and keeps the project working. Stop after any of them if the user only wants that much.
1. Install and set up the agents¶
uv add noodlelab # or: pip install noodlelab, or add it to requirements.txt
noodlelab init-agent # AGENTS.md section, CLAUDE.md import, skill, .mcp.json
init-agent can be run again safely: it updates its own section of
AGENTS.md and leaves the rest of the file alone. Commit what it wrote so
that every agent and every person working on the project gets the same rules.
Codex CLI users also run codex mcp add noodlelab -- noodlelab mcp.
2. Find the calculations that matter¶
Look for code whose numbers someone relies on:
physical formulas;
constants with units in their names (
speed_mps,mass_kg,# in metres), and retyped physical constants (3e8,6.674e-11,9.80665);tolerances, thresholds and limits (
if margin < 3:);results written to reports, dashboards or files for other people.
List them for the user, and suggest starting with the one whose error would cost the most.
3. Put units and uncertainty on the inputs¶
Replace bare floats at the edges (config, constants, measured data) with quantities. Keep the function bodies as they are: Pint quantities go through ordinary arithmetic and NumPy.
# before
GRAVITY = 9.81 # m/s^2
def fall_time(height_m): # returns seconds
return (2 * height_m / GRAVITY) ** 0.5
# after
import noodlelab.verify as nv
GRAVITY = nv.measure("9.81 ± 0.02 m/s^2", name="g") # source: local survey 2024
def fall_time(height):
return (2 * height / GRAVITY) ** 0.5 # a quantity in seconds, with its uncertainty
Library functions that drop units and uncertainty¶
Code from scipy, scikit-learn and the like works on bare numbers. nv.wrap puts
units and uncertainty around such a function without changing it: quantities go
in, are converted to units_in, and the result comes back in units_out with
the inputs’ uncertainty propagated (first order, GUM 5.1). The call is recorded
like @nv.traced. Check a strongly nonlinear case with nv.monte_carlo.
import scipy.optimize
import noodlelab.verify as nv
root = nv.wrap(
lambda lo, hi, k: scipy.optimize.brentq(lambda x: x * x - k, lo, hi),
units_in={"lo": "m", "hi": "m", "k": "m**2"},
units_out="m",
)
root(0, 10, nv.measure("4.00 ± 0.04 m^2")) # 2.000 ± 0.010 m
A function that returns a tuple (scipy.integrate.quad gives value and error
estimate) takes a tuple of units: units_out=("m", "m"), with None for an
element to leave bare. First order is wrong for a strongly nonlinear function,
so nv.wrap(..., validate=True) checks each call against nv.monte_carlo and
warns when they disagree (it needs a single-number result and independent
measure inputs, and calls the function many times).
Two more decorators replace the with block and the manual check:
@nv.checked("R1 distance <= 50 m") # a failing check warns; strict=True raises
@nv.wrap(units_in={"g": "m/s**2", "T": "s"}, units_out="m")
def distance(g, T): ...
@nv.recorded("fall study") # the run leaves its provenance record
def main(): ...
In a test suite, @pytest.mark.noodlelab on a test gives it a record, so a
test that only calls @nv.checked functions fails when a requirement fails.
A physical constant is not a measurement, so it doesn’t need a survey. Take
it by name instead of retyping its digits, with its unit and (for measured
ones such as G) its CODATA uncertainty:
from noodlelab.constants import G, c, g0 # `noodlelab constants` lists them all
SPEED_OF_LIGHT = 299_792_458 # m/s -> SPEED_OF_LIGHT = c
STANDARD_G = 9.80665 # m/s^2 -> STANDARD_G = g0 (not the local g above)
Project-wide values that everyone should share (a reference density, a site’s
surveyed gravity) go in a constants.toml at the workspace root, which
graphs read automatically, and scripts load with nv.load_constants(...).
Where the code must hand plain numbers to a library, convert at the boundary
with value.m_as("s") (the magnitude in seconds). Don’t strip units any
earlier than that.
If you don’t know a value’s uncertainty, ask the user or look for it in a
datasheet, calibration certificate or paper. Never invent one. An assumed
value is recorded as source="assumed: ..." and listed for the user.
4. Write the requirements down¶
Put them where people will read them, for example requirements.txt-style in
spec/requirements.txt or next to the code, one per line:
PER-001 period <= 2.1 s [Analysis] # The swing shall take at most 2.1 s
TMP-001 temperature between -20 and 60 degC [Test]
Load them with nv.requirements(Path("spec/requirements.txt").read_text()).
CSV or YAML rows (id, quantity, op, limit, unit, text, method) work too.
5. Record and verify each calculation¶
Wrap the entry point (a script, a CLI command or a pipeline step) in a record:
def main():
with nv.record("pendulum sizing") as rec:
L = rec.input("L", cfg.length, source="design v3")
rec.result("period", period(L))
# PER-001 constrains `period`: verified against that result when the block ends
rec.require(Path("spec/requirements.txt").read_text())
return 0 if rec.passed else 1
A requirement whose quantity names no result is left unverified, and a
NoodlelabWarning says so: verify it with rec.verify("PER-001", value).
Mark helper functions whose calls should be in the audit trail with
@nv.traced.
6. Check it in CI and in tests¶
# .github/workflows/verify.yml (a step)
- run: uv run noodlelab verify scripts/sizing.py --json
# tests/test_sizing.py
import noodlelab.verify as nv
def test_period_meets_its_requirement(nv_record): # the fixture comes with noodlelab
nv_record.require("PER-001 period <= 2.1 s", period(nv.q("1 m")))
The test fails, with the record’s summary, when a check fails or is
inconclusive or a requirement goes unverified; pytest --nv-strict fails on
the audit’s warnings too (results without units or uncertainty).
Graphs are checked the same way: noodlelab verify analysis.graph.json. For
checks plus regression against a run marked final, use noodlelab test *.graph.json --baseline final.
To see what changed between two runs of a script, noodlelab diff runs/<new> runs/<old>.
7. Optional: graphs and reports¶
If the user wants a visual pipeline or a PDF report, build it as a graph with
the MCP tools (see graphs-and-reports.md). The user
opens it with uvx "noodlelab[full]" . and sees the same analysis as nodes.
Report back¶
Tell the user:
what now carries units and uncertainty;
which requirements are verified, with their margins;
what was assumed, and which audit warnings remain;
where the records are (
runs/, which may belong in.gitignore).