Quickstart: a verified calculation

pip install noodlelab        # or: uv add noodlelab
# drop.py
import noodlelab.verify as nv

with nv.record("drop test") as rec:
    h = rec.input("h", "2.00 ± 0.01 m", source="tape measure, lab book p. 4")
    g = rec.input("g", "9.81 ± 0.02 m/s^2", source="local gravity survey")
    t = rec.result("fall time", (2 * h / g) ** 0.5)  # (0.6386 ± 0.0017) s
    v = rec.result("impact speed", (2 * g * h) ** 0.5)  # (6.264 ± 0.017) m/s

    rec.require("""
        DRP-001 fall_time <= 1 s [Analysis]      # The drop shall take at most 1 s
        DRP-002 impact_speed <= 7 m/s [Analysis] # The part shall land below 7 m/s
    """)
    rec.verify("DRP-001", t)
    rec.verify("DRP-002", v)
    rec.close_to("v = g·t", v, g * t, rtol=1e-9)  # two routes to the same answer

print(rec.summary())
$ noodlelab verify drop.py
✓ drop.py
  ✓ drop test: passed
    ✓ DRP-001: 0.6386 s meets ≤ 1 s (margin +0.3614 s, +36.1 %)
    ✓ DRP-002: 6.264 m/s meets ≤ 7 m/s (margin +0.7358 m/s, +10.5 %)
    ✓ v = g·t: (6.264 ± 0.017) m/s agrees with (6.264 ± 0.017) m/s (|Δ| = 0 m/s, ...)
    record: runs/20260926-101500-drop-test-1a2b3c4d/provenance.json
1 of 1 passed

What you get

  • Units: h / g is in s², and (2 * h / g) ** 0.5 is in seconds. Adding metres to seconds raises an error. Convert with .to("ms").

  • Uncertainty: GUM propagation, with correlations kept (t and v both depend on g and h). nv.budget(t) shows each input’s share, and nv.fmt(t) rounds as GUM 7.2.6 says.

  • Requirements with margins: each check says how far inside or outside the limit the result is, in the requirement’s unit and as a percentage.

  • A record: runs/<time>-drop-test-<id>/provenance.json holds the inputs and their sources, the results, requirements, checks, the script’s SHA-256, its git commit and whether it was dirty, the Python version and platform, and every installed package.

  • An audit: noodlelab verify exits with 1 when a check failed or a requirement was never verified. It warns about results without units or uncertainty, inputs without a source, and code that was not committed. --strict makes those warnings fail too. --json gives the same verdict in a form an agent can parse.

More

nv.q("3 dB"), nv.q(12, "W")  # exact quantities
nv.measure(9.81, 0.02, "m/s^2", name="g")  # measured, labelled for budgets
nv.measure("1.2 ± 0.1 mm", distribution="rectangular")  # Type B, for Monte Carlo
nv.const.c, nv.constant("g0")  # named constants, with units (G, m_e...: CODATA ±)
g0 = rec.constant("g0")  # recorded as an input, its source filled in
nv.define_constant("rho_w", "998.2 kg/m^3", source="CRC Handbook")  # your own


@nv.traced  # calls recorded inside a record
def drag(rho, v, cd, area): ...


mc = nv.monte_carlo(lambda: 1 / nv.measure("1.0 ± 0.4"), 20_000)
mc.agrees, mc.message  # is the linear GUM result good enough?

rec.expect(0 < eff.m < 1, "efficiency is a fraction")  # a sanity check
rec.read("data/calibration.csv")  # its SHA-256 goes in the record
rec.output("plot.png")  # a path in the record's folder
rec.note("assumes small angles")  # caveats for the reader
nv.audit(nv.load("runs/.../provenance.json"))  # re-check a record later