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, g = rec.add_inputs(
h=("2.00 ± 0.01 m", "tape measure, lab book p. 4"),
g=("9.81 ± 0.02 m/s^2", "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
# verified when the block ends, against the results of the same name
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.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
= fall time: (0.6386 ± 0.0017) s
= impact speed: (6.264 ± 0.017) m/s
✓ v = g·t: (6.264 ± 0.017) m/s agrees with (6.264 ± 0.017) m/s (|Δ| = 0 m/s, ...)
✓ DRP-001: 0.6386 ± 0.0034 s (U, k = 2) meets ≤ 1 s (margin +0.3614 s, +36.1 %, ...)
✓ DRP-002: 6.264 ± 0.034 m/s (U, k = 2) meets ≤ 7 m/s (margin +0.7358 m/s, +10.5 %, ...)
record: runs/20260926-101500-drop-test-1a2b3c4d/provenance.json
1 of 1 passed
What you get¶
Units:
h / gis in s², and(2 * h / g) ** 0.5is in seconds. Adding metres to seconds raises an error. Convert with.to("ms").Uncertainty: GUM propagation, with correlations kept (
tandvboth depend ongandh).nv.budget(t)shows each input’s share, andnv.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.jsonholds 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 verifyexits 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.--strictmakes those warnings fail too.--jsongives 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")) # until the results are stable
mc.agrees, mc.message # is the linear GUM result good enough?
nv.monte_carlo(model, workers=4) # a model with an `if`, over 4 processes; or qmc=True
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
rec.require("DRP-003 fall_time >= 0.1 s", t) # written down and verified at once
g = nv.from_repeats([9.79, 9.82, 9.80, 9.81], "m/s^2", name="g") # Type A: s/√n, n - 1 dof
a, b = nv.correlated([a, b], [[1, 0.8], [0.8, 1]]) # correlated inputs (GUM 5.2)
rows = nv.sweep(lambda L: period(L), L="0.5 .. 2 m", points=7) # rows.column("result")
old = nv.load("runs/.../provenance.json") # a record, read back
old.passed, old.checks, old.result("fall time"), nv.audit(old)
rec.to_markdown(), rec.to_latex(standalone=True) # for a report; Jupyter shows tables
$ noodlelab diff runs/<new run> runs/<old run> # what changed, and how the margins moved
In a test suite, the nv_record fixture is a record named after the test,
and the test fails when the record does:
def test_the_drop(nv_record):
h = nv_record.input("h", "2.00 ± 0.01 m", source="tape measure")
nv_record.require("DRP-001 fall_time <= 1 s", (2 * h / nv.const.g0) ** 0.5)