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).