noodlelab for AI agents¶
noodlelab makes scientific and engineering calculations verifiable. Use it whenever code computes a physical quantity, a measurement result, a design margin or anything a person will rely on. The point is that a reviewer (or another agent) can check what you did, not just trust it.
The rules¶
Units on every number. Use quantities (
q("9.81 m/s^2")), never bare floats with the unit in a variable name or a comment. Mixing dimensions then raises instead of silently giving nonsense. Dimensionless results say so (unit="1").Named constants, never retyped digits. Use
nv.const.c,nv.const.g0orrec.constant("k_B")rather than299792458or9.81. A constant carries its unit, its source and, for measured ones such asGorm_e, its CODATA uncertainty.pi,tauandeare themathmodule’s. A local value (the gravity at your site, a material’s density) is a measurement:measure()it with its source.Uncertainty on every measured input, and a source. Write
"9.81 ± 0.02 m/s^2"and say where it came from (an instrument, datasheet, paper or dataset). Uncertainty propagates by itself (GUM, first order, with correlations kept).budget()shows which input dominates. For strongly non-linear models, check withmonte_carlo(). Also9.81 +/- 0.02and the concise9.81(2)(± 0.02 on the last digit). Node outputs show the standard uncertainty u; requirement checks show the expanded U = 2u, labelled “(U, k = 2)”. Uncertainty belongs to real numbers only: a complex result (a spectrum, a pole) comes out on nominal values with a note, so measure and verify its real parts, or its magnitude and phase. Write a complex value asq("3+4j V")(a lowercasejright after the digits;4Jis 4 joules); records show it as"3+4j"with its parts inreandim. In symbolic expressions the imaginary unit isjtoo:Iis an ordinary symbol (a second moment of area).Write the requirements down before checking them, one per line:
COM-001 link_margin >= 3 dB [Analysis] # The link shall close with 3 dB to spare. Verify each one: a check reports its margin, not just pass or fail.>and<are strict. An uncertain result fails when its nominal value misses the limit, whatever its uncertainty; it passes when it meets the limit by more than its expanded uncertainty U = 2u; in between it is inconclusive, which is not a pass: reduce the uncertainty or change the design, don’t drop the uncertainty. The margin’s percentage of the limit is given only on a ratio scale (not for °C, dB or a range).Leave a record. Wrap the calculation in
with nv.record(...), or build it as a graph. Every run then writes aprovenance.jsonholding inputs, results, checks, the code (hash, git commit), files (SHA-256) and the environment.Verify before you say you are done. Run
noodlelab verify <script.py | graph.json> --jsonand fix what it reports. Exit code 0 means every check passed and every requirement was verified; an inconclusive requirement exits 1. Report the margins and any warnings to the user; don’t hide failures.
Never invent a measured value or its uncertainty. When a number is assumed,
say so with rec.note(...) and source="assumed: ...".
Python: noodlelab.verify¶
import noodlelab.verify as nv
with nv.record("link budget") as rec: # writes runs/<time>-link-budget-<id>/provenance.json
p_tx = rec.input("p_tx", "10.0 ± 0.3 W", source="PA datasheet rev C")
d = rec.input("d", nv.q("1200 km"), source="orbit design")
...
margin = rec.result("link_margin", computed_margin) # a quantity, e.g. in dB
rec.require("COM-001 link_margin >= 3 dB [Analysis] # The link shall close")
rec.verify("COM-001", margin) # Check(passed, margin=+1.2 dB, ...)
rec.expect(0 < efficiency.m < 1, "efficiency is a fraction")
rec.close_to("vs. textbook", result, nv.q("2.006 s"), rtol=0.01)
print(rec.summary())
Constants:
nv.constant("c"),nv.const.g0,from noodlelab.constants import c, h, k_B.rec.constant("g0")records one as an input, with its source.noodlelab constantslists them all: exact SI ones (c,h,hbar,q_e(the elementary charge),k_B,N_A,R,F,sigma_SB), measured ones (G,m_e,m_p,m_n,m_u,alpha,mu_0,eps_0), conventions (g0,atm,T0) andR_E,GM_E,au.nv.define_constant("g_local", 9.8123, "m/s^2", uncertainty=5e-4, source=...)adds your own, andnv.load_constants("constants.toml")loads a file of them.nv.q(text_or_number, unit)returns an exact quantity.nv.measure("x ± u unit")ornv.measure(x, u, unit, name=...)returns an uncertain one.nv.requirements(text)andnv.verify(req, value)work outside a record too.@nv.tracedrecords every call of a function (arguments, result, source hash) inside a record.nv.monte_carlo(model, trials), wheremodelmakes its inputs withmeasure()and returns the result, checks whether the GUM result can be trusted (JCGM 101).nv.audit(record)lists findings NL001–NL011: failed checks, unverified requirements, results without units or uncertainty, inputs without a source, code that was dirty or has changed since, and inconclusive requirements.Units beyond SI: money in
USD,EUR,GBP,JPY,CHF,CAD,AUD,CNYandINR, each its own dimension, so currencies never add up: convert with the rate as a measured value,q("100 EUR") * q("1.08 USD/EUR")($,€and£cannot be read: write the code). Head counts inindividual("300 individuals"), a real dimension; acoustic absorption insabin(ft²) andmetric_sabin(m²)."2 1/4 cup"is 2.25 cup. Dates are not quantities: give elapsed time ("5688 d"). A workspace’s own units go inunits.tomlnext toconstants.toml, onename = "definition"per line in Pint’s syntax (widget = "[widget]",crate = "12 * widget"); a unit defined differently before stops the run (restart to change one).Offset temperatures (
degC,degF) cannot go into a formula: convert them to K first, or give differences indelta_degC.Requirements in decibels take plain numbers (already in dB) or quantities in dB. A dimensionless quantity is refused, since it could be a ratio (4 is 6.02 dB) or a level. Limits on temperature differences go in
Kordelta_degC: adegClimit reads aKvalue as an absolute temperature (adelta_degCvalue is compared as a difference).
Graphs: node pipelines the user can open in the editor¶
A graph is a JSON file <name>.graph.json in the workspace. Nodes are typed
Python functions (list_nodes, describe_node). Every input is either a
value or a link to another node’s output:
{"nodes": [
{"id": "1", "type": "core.number", "inputs": {"value": {"value": 3}}},
{"id": "2", "type": "core.math",
"inputs": {"operation": {"value": "power"},
"a": {"link": {"node": "1", "output": "result"}},
"b": {"value": 2}}}
],
"report": [
{"id": "1", "type": "report.new_report", "inputs": {"title": {"value": "Results"}}},
{"id": "2", "type": "report.add_value",
"inputs": {"report": {"link": {"node": "1", "output": "result"}},
"label": {"value": "3 squared"},
"value": {"link": {"node": "2", "output": "result", "tab": "processing"}}}},
{"id": "3", "type": "report.render_report",
"inputs": {"report": {"link": {"node": "2", "output": "result"}}}}
]}
nodesis the processing canvas andreportis the Reporting canvas. A report node reads a processing output with"tab": "processing"in its link. Data only flows from processing into the report. Report nodes may also sit innodes, as the shipped examples do; both render the same PDF.The usual shape for a verified analysis:
Requirements (
requirements.requirements, one per line), then the inputs, with units and uncertainty (units.*,uncertainty.*), then the model.Verify Requirement for each requirement, chaining its
logoutput from one to the next.A report: New Report, Add Heading / Text / Value / Figure / Requirements, then Add Compliance Matrix (from the last
log), Run Details (provenance), and Render Report (the PDF).
Constants in graphs: a Constant node (
units.constant,name="g0", optionalunit; unit"1"makes π a dimensionless quantity for Quantity Math). In a symbolic Values node,g = g0, or just the linec, takes the constant with its unit and uncertainty. Evaluate never fills in constants by itself. A graph’s own constants go in"constants"next to"nodes":{"g_local": {"value": 9.8123, "unit": "m/s^2", "uncertainty": 0.0005, "source": "survey"}}or{"rho_w": "998.2 kg/m^3"}(withedit_graph,{"op": "constant", "name": ..., "value": ...}). Constants for every graph in a workspace go inconstants.tomlat its root.Matrices: a matrix is a quantity holding a 2-D array, with one unit for every entry. Matrix (
maths.matrix) reads MATLAB-style text,"200, -100; -100, 200"withunit"N/m";"10; 0"is a column. Solve Linear System (maths.solve_linear) solves A x = b with units (N over N/m is m), and Natural Frequencies (maths.natural_frequencies) solves K φ = ω² M φ. Mixed-unit matrices are refused: use consistent SI numbers. Rows, columns and modes are numbered from 1 (maths.element).Systems of equations: Equations (
symbolic.equations, one per line), then Solve System (in symbols,unknowns"x1, x2"), Solve Numerically (SciPy, with guesses; propagates uncertainty), or Linear System (the A and b of A x = b) and Evaluate Matrix to put numbers in. Evaluate itself takes one expression, not a matrix.Control: systems travel on SYSTEM sockets (python-control transfer functions and state space, in SI units). Build with Transfer Function, Zero-Pole-Gain, State Space, PID Controller, First/Second-Order System or Mass-Spring-Damper (from M, K and C matrices); connect with Series, Parallel and Feedback; analyse with Step Response (times in s, for requirements), Stability Margins, Bode, Nyquist, Pole-Zero Map, Root Locus.
Symbolic Values take one
name = valueper line:x = 3 ± 1 kg,g = 9.81(2) m/s^2, a constant by name. Each line is named after its symbol in uncertainty budgets, and Monte Carlo samples it. Expressions may useSum(1/k^2, (k, 1, N))andProduct(...)onceNhas a whole value.Fits give their parameters with their uncertainty: Linear Regression’s
slope_qandintercept_q(in the units of y/x and y when x and y are quantity columns, e.g. from Get Quantity Column) and Curve Fit’sestimates(a dict by parameter name) carry the fit’s covariance. Soslope_q·x + intercept_qhas the right uncertainty, and Monte Carlo draws them jointly. Use these rather thanslope ± slope_setyped by hand, which loses the correlation.Complex values are never cut to their real part: nodes that take real numbers (statistics, fits, plots, Monte Carlo, Magnitude) refuse them and say so. Split one with Complex Parts (
maths.complex_parts):real,imag,magnitudeandconjugatekeep the unit,phaseis in rad. In expressionsre(z),im(z),arg(z),abs(z)andconjugate(z)work on quantities (3 + 4*j); Values may hold3+4j V. Iterate (newton) from a complex start finds a complex root. Matrix reads entries like3+4j(no spaces); Element and Eigenvalues take complex matrices, the other matrix nodes need real ones. A complex value cannot carry an uncertainty: give its real and imaginary parts (or magnitude and phase) as measurements.Checks: Expect Value and
rec.close_tocompare |actual − expected| (so 3+4j V does not match 3 V) and pass when |Δ| ≤ k·u(Δ), the uncertainty of the difference, so values from the same inputs are compared fairly. Expect Value’srelativetolerance is in % of the expected value.Arrays carry uncertainty element by element: an uncertain number times an array, or a repeat zone’s history of uncertain values, is an uncertain array, each element with its own u and its correlations to the inputs. Other nodes propagate it one element at a time up to 200 elements; beyond that the uncertainty is unknown (NaN), which makes requirements inconclusive, and arrays over 10 000 elements go on at their nominal values with a note (use Monte Carlo for either). A requirement on an array passes only when every element passes by more than its U. XY Plot draws ±u error bars for an uncertain y.
Monte Carlo’s
agrees_with_gumis true, false, or null when too few trials were run to tell (gum_comparisonsays how many would);verifywarns (NL010) when it is false.Fourier Transform (
maths.fourier_transform) takes a signal and its times (or a sample spacing) and gives frequencies in Hz (1/m along a distance), amplitudes in the signal’s unit andpeak_frequencies.Start from a similar example (
list_examples,get_example). Example 23 (satellite link budget), example 24 (cantilever bracket) and example 25 (two masses on springs: equations, matrices, a PID loop and an FFT check) are complete verified studies with reports.
MCP tools (server noodlelab mcp)¶
Tool |
Use |
|---|---|
|
this text |
|
find nodes and their inputs, outputs, units and options |
|
complete graphs to copy from |
|
copy an example into the workspace with its data files, to run or adapt it |
|
the workspace’s graphs |
|
write a whole graph (validated; the open editor reloads it) |
|
add, set, remove, retitle or track nodes, or define the graph’s constants, without resending the graph |
|
problems before running: types, units, missing inputs |
|
run it; returns each node’s result (one line each), checks, requirements, files and provenance |
|
a node’s output in full: an uncertainty budget, a fit’s parameters, a table’s rows (first 500), a dict; uncertain values as |
|
each requirement’s latest verdict and margin, and how many runs are recorded |
|
audit a script, graph or records in the workspace, as |
When you are started from the editor’s Agent panel, the user is watching the
canvas: every graph you save opens there, laid out automatically.
NOODLELAB_GRAPH names the graph they had open.
The workflow:
guide, thenlist_examples/list_nodes.save_graph, thencheck_graphuntil there are no errors.run_graph, then read the checks and margins (get_outputfor any value you need in full).Fix and iterate. Finish with
verify.Tell the user what passed, the margins, what was assumed, and where the report PDF and provenance are.
This page is the text the MCP server’s guide tool returns, and the skill
noodlelab init-agent installs.