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

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

  2. Named constants, never retyped digits. Use nv.const.c, nv.const.g0 or rec.constant("k_B") rather than 299792458 or 9.81. A constant carries its unit, its source and, for measured ones such as G or m_e, its CODATA uncertainty. pi, tau and e are the math module’s. A local value (the gravity at your site, a material’s density) is a measurement: measure() it with its source.

  3. 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 with monte_carlo(). Also 9.81 +/- 0.02 and the concise 9.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 as q("3+4j V") (a lowercase j right after the digits; 4J is 4 joules); records show it as "3+4j" with its parts in re and im. In symbolic expressions the imaginary unit is j too: I is an ordinary symbol (a second moment of area).

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

  5. Leave a record. Wrap the calculation in with nv.record(...), or build it as a graph. Every run then writes a provenance.json holding inputs, results, checks, the code (hash, git commit), files (SHA-256) and the environment.

  6. Verify before you say you are done. Run noodlelab verify <script.py | graph.json> --json and 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 constants lists 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) and R_E, GM_E, au. nv.define_constant("g_local", 9.8123, "m/s^2", uncertainty=5e-4, source=...) adds your own, and nv.load_constants("constants.toml") loads a file of them.

  • nv.q(text_or_number, unit) returns an exact quantity. nv.measure("x ± u unit") or nv.measure(x, u, unit, name=...) returns an uncertain one.

  • nv.requirements(text) and nv.verify(req, value) work outside a record too.

  • @nv.traced records every call of a function (arguments, result, source hash) inside a record.

  • nv.monte_carlo(model, trials), where model makes its inputs with measure() 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, CNY and INR, 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 in individual ("300 individuals"), a real dimension; acoustic absorption in sabin (ft²) and metric_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 in units.toml next to constants.toml, one name = "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 in delta_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 K or delta_degC: a degC limit reads a K value as an absolute temperature (a delta_degC value 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"}}}}
 ]}
  • nodes is the processing canvas and report is 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 in nodes, 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 log output 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", optional unit; unit "1" makes π a dimensionless quantity for Quantity Math). In a symbolic Values node, g = g0, or just the line c, 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"} (with edit_graph, {"op": "constant", "name": ..., "value": ...}). Constants for every graph in a workspace go in constants.toml at 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" with unit "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 = value per 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 use Sum(1/k^2, (k, 1, N)) and Product(...) once N has a whole value.

  • Fits give their parameters with their uncertainty: Linear Regression’s slope_q and intercept_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’s estimates (a dict by parameter name) carry the fit’s covariance. So slope_q·x + intercept_q has the right uncertainty, and Monte Carlo draws them jointly. Use these rather than slope ± slope_se typed 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, magnitude and conjugate keep the unit, phase is in rad. In expressions re(z), im(z), arg(z), abs(z) and conjugate(z) work on quantities (3 + 4*j); Values may hold 3+4j V. Iterate (newton) from a complex start finds a complex root. Matrix reads entries like 3+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_to compare |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’s relative tolerance 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_gum is true, false, or null when too few trials were run to tell (gum_comparison says how many would); verify warns (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 and peak_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

guide

this text

list_nodes, describe_node

find nodes and their inputs, outputs, units and options

list_examples, get_example

complete graphs to copy from

copy_example

copy an example into the workspace with its data files, to run or adapt it

list_graphs, get_graph

the workspace’s graphs

save_graph

write a whole graph (validated; the open editor reloads it)

edit_graph

add, set, remove, retitle or track nodes, or define the graph’s constants, without resending the graph

check_graph

problems before running: types, units, missing inputs

run_graph

run it; returns each node’s result (one line each), checks, requirements, files and provenance

get_output

a node’s output in full: an uncertainty budget, a fit’s parameters, a table’s rows (first 500), a dict; uncertain values as {value, u, unit}, figures as a PNG path. key digs into dicts and tables. A run_graph summary ending “(get_output for the full value)” has more to read

requirements

each requirement’s latest verdict and margin, and how many runs are recorded

verify

audit a script, graph or records in the workspace, as noodlelab verify --json does; a script is run to do it

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:

  1. guide, then list_examples / list_nodes.

  2. save_graph, then check_graph until there are no errors.

  3. run_graph, then read the checks and margins (get_output for any value you need in full).

  4. Fix and iterate. Finish with verify.

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