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 / 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"), 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