Metadata-Version: 2.5
Name: noodlelab
Version: 0.1.1
Summary: Verifiable science for humans and AI agents: units, GUM uncertainty, requirements with margins, checks and provenance, as a Python library, a CLI, an MCP server and a node editor.
Project-URL: Homepage, https://github.com/nkalis/noodlelab
Project-URL: Documentation, https://github.com/nkalis/noodlelab/tree/main/docs/agents
Project-URL: LLM index, https://github.com/nkalis/noodlelab/blob/main/llms.txt
Project-URL: Agent guide, https://github.com/nkalis/noodlelab/blob/main/src/noodlelab/agent/guide.md
Project-URL: Issues, https://github.com/nkalis/noodlelab/issues
Author: Nicolas Kalis
License-Expression: MIT
License-File: LICENSE
Keywords: ai-agents,audit,dataflow,gum,llm,mcp,node-editor,provenance,reproducibility,requirements,scientific-computing,uncertainty,units,verification
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.11
Requires-Dist: numpy>=1.26
Requires-Dist: pint>=0.24
Requires-Dist: pydantic>=2.7
Requires-Dist: uncertainties>=3.2
Provides-Extra: app
Requires-Dist: fastapi>=0.115; extra == 'app'
Requires-Dist: psutil>=5.9; extra == 'app'
Requires-Dist: typst>=0.13; extra == 'app'
Requires-Dist: uvicorn[standard]>=0.30; extra == 'app'
Provides-Extra: azure
Requires-Dist: adlfs>=2024.2; extra == 'azure'
Requires-Dist: fsspec>=2024.2; extra == 'azure'
Provides-Extra: engineering
Requires-Dist: control>=0.10; extra == 'engineering'
Requires-Dist: fastapi>=0.115; extra == 'engineering'
Requires-Dist: matplotlib>=3.8; extra == 'engineering'
Requires-Dist: numpy>=1.26; extra == 'engineering'
Requires-Dist: pandas>=2.2; extra == 'engineering'
Requires-Dist: psutil>=5.9; extra == 'engineering'
Requires-Dist: pyarrow>=15; extra == 'engineering'
Requires-Dist: salib>=1.4; extra == 'engineering'
Requires-Dist: scipy>=1.12; extra == 'engineering'
Requires-Dist: sympy>=1.12; extra == 'engineering'
Requires-Dist: typst>=0.13; extra == 'engineering'
Requires-Dist: uvicorn[standard]>=0.30; extra == 'engineering'
Provides-Extra: full
Requires-Dist: control>=0.10; extra == 'full'
Requires-Dist: fastapi>=0.115; extra == 'full'
Requires-Dist: fsspec>=2024.2; extra == 'full'
Requires-Dist: geopandas>=1.0; extra == 'full'
Requires-Dist: matplotlib>=3.8; extra == 'full'
Requires-Dist: numpy>=1.26; extra == 'full'
Requires-Dist: pandas>=2.2; extra == 'full'
Requires-Dist: psutil>=5.9; extra == 'full'
Requires-Dist: pyarrow>=15; extra == 'full'
Requires-Dist: pyogrio>=0.9; extra == 'full'
Requires-Dist: pyproj>=3.6; extra == 'full'
Requires-Dist: rasterio>=1.3; extra == 'full'
Requires-Dist: salib>=1.4; extra == 'full'
Requires-Dist: scipy>=1.12; extra == 'full'
Requires-Dist: shapely>=2.0; extra == 'full'
Requires-Dist: sympy>=1.12; extra == 'full'
Requires-Dist: typst>=0.13; extra == 'full'
Requires-Dist: uvicorn[standard]>=0.30; extra == 'full'
Provides-Extra: gcs
Requires-Dist: fsspec>=2024.2; extra == 'gcs'
Requires-Dist: gcsfs>=2024.2; extra == 'gcs'
Provides-Extra: geo
Requires-Dist: fastapi>=0.115; extra == 'geo'
Requires-Dist: geopandas>=1.0; extra == 'geo'
Requires-Dist: matplotlib>=3.8; extra == 'geo'
Requires-Dist: numpy>=1.26; extra == 'geo'
Requires-Dist: pandas>=2.2; extra == 'geo'
Requires-Dist: psutil>=5.9; extra == 'geo'
Requires-Dist: pyarrow>=15; extra == 'geo'
Requires-Dist: pyogrio>=0.9; extra == 'geo'
Requires-Dist: pyproj>=3.6; extra == 'geo'
Requires-Dist: rasterio>=1.3; extra == 'geo'
Requires-Dist: salib>=1.4; extra == 'geo'
Requires-Dist: scipy>=1.12; extra == 'geo'
Requires-Dist: shapely>=2.0; extra == 'geo'
Requires-Dist: sympy>=1.12; extra == 'geo'
Requires-Dist: typst>=0.13; extra == 'geo'
Requires-Dist: uvicorn[standard]>=0.30; extra == 'geo'
Provides-Extra: maths
Requires-Dist: fastapi>=0.115; extra == 'maths'
Requires-Dist: matplotlib>=3.8; extra == 'maths'
Requires-Dist: numpy>=1.26; extra == 'maths'
Requires-Dist: pandas>=2.2; extra == 'maths'
Requires-Dist: psutil>=5.9; extra == 'maths'
Requires-Dist: scipy>=1.12; extra == 'maths'
Requires-Dist: sympy>=1.12; extra == 'maths'
Requires-Dist: typst>=0.13; extra == 'maths'
Requires-Dist: uvicorn[standard]>=0.30; extra == 'maths'
Provides-Extra: remote
Requires-Dist: fsspec>=2024.2; extra == 'remote'
Provides-Extra: report
Requires-Dist: typst>=0.13; extra == 'report'
Provides-Extra: s3
Requires-Dist: fsspec>=2024.2; extra == 's3'
Requires-Dist: s3fs>=2024.2; extra == 's3'
Provides-Extra: science
Requires-Dist: fastapi>=0.115; extra == 'science'
Requires-Dist: matplotlib>=3.8; extra == 'science'
Requires-Dist: numpy>=1.26; extra == 'science'
Requires-Dist: pandas>=2.2; extra == 'science'
Requires-Dist: psutil>=5.9; extra == 'science'
Requires-Dist: pyarrow>=15; extra == 'science'
Requires-Dist: salib>=1.4; extra == 'science'
Requires-Dist: scipy>=1.12; extra == 'science'
Requires-Dist: sympy>=1.12; extra == 'science'
Requires-Dist: typst>=0.13; extra == 'science'
Requires-Dist: uvicorn[standard]>=0.30; extra == 'science'
Provides-Extra: sftp
Requires-Dist: fsspec>=2024.2; extra == 'sftp'
Requires-Dist: sshfs>=2024.2; extra == 'sftp'
Provides-Extra: units
Description-Content-Type: text/markdown

# noodlelab

[![Try the live demo](https://img.shields.io/badge/Try_the_live_demo-noodlelab--demo.onrender.com-4772b3?logo=render&logoColor=white)](https://noodlelab-demo.onrender.com)
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/nkalis/noodlelab?quickstart=1)

**Verifiable science for AI agents, and for the people who check their work.**

AI agents write scientific and engineering code quickly, but nothing makes them
show their work. A number comes back without a unit, a measured input without an
uncertainty, a design "works" without saying by how much, and nobody can say later
what was run. noodlelab gives every calculation:

- units on every number;
- uncertainty on every measured input;
- requirements that are checked, with margins;
- a provenance record of every run.

Then it **audits** the calculation, so an agent can prove its work and you can
check it.

![A cantilever bracket in the editor: material trade, beam and stress nodes with plots on them, and the Requirements tab listing three requirements passed with their margins](docs/images/hero.png)
<sub>Example 24: sizing a cantilever bracket. Each requirement shows its margin and the run that verified it.</sub>

## Designed for AI agents

noodlelab is built around the loop a coding agent works in: write, run, check,
fix. Each part of it is machine-readable, and the agent can't skip a step
without it showing.

| An agent needs to… | noodlelab gives it |
|---|---|
| know it is done | `noodlelab verify` exits with 0 only when every check passed and every requirement was verified. `--json` gives the verdict as data. |
| be told what it skipped | the audit (NL001–NL010) flags unverified requirements, results without units or uncertainty, inputs without a source, and uncommitted code |
| prove what it ran | a `provenance.json` for every run: inputs, results, checks, the code's hash and git commit, file SHA-256s, the environment |
| build things people can review | graphs in plain JSON (with a [schema](docs/agents/graph.schema.json)) that open as nodes in the editor and render PDF reports |
| find and learn the package | [`llms.txt`](llms.txt), [`AGENTS.md`](AGENTS.md), a Claude skill, an MCP server with a `guide` tool, docstrings written to be read |
| fit into your project | `noodlelab init-agent` sets it all up in one command |

This is what an agent sees when it cuts corners:

```console
$ noodlelab verify sloppy.py
✗ sloppy.py
  ✗ beam sizing: incomplete
    ✓ STR-001: 180 MPa meets ≤ 250 MPa (margin +70 MPa, +28.0 %)
    ! NL002 DEF-001 was never verified
    ! NL004 result 'stress' has no unit (say unit='1' if it is dimensionless)
    ! NL005 result 'stress' has no uncertainty (give its inputs one, or exact=True)
    · NL006 input 'load' has no source
    record: runs/20260926-100232-beam-sizing-4eece145/provenance.json
0 of 1 passed
```

The exit code is 1, so the agent doesn't report "done" until it has fixed these.

### Three ways in

**1. The Agent panel in the editor.** Press **Agent** to open Claude Code or Codex
CLI in a terminal under the canvas, sign in as it asks, and describe the analysis.
It builds the graph, the requirements and the report through noodlelab's tools,
and every graph it saves appears on your canvas.

![The Agent panel: a terminal under the canvas where the agent called guide, list_nodes, save_graph, run_graph and verify; the drop-test graph it built is on the canvas and the Requirements tab shows both requirements passed](docs/images/agent.png)
<sub>A drop test built by an agent: fall time and impact speed from √(2h/g) and √(2gh) with propagated uncertainty, two requirements verified, a PDF report. Here a scripted agent calls the same MCP tools Claude Code would.</sub>

**2. Your own agent, in your own project.**

```bash
uv add noodlelab && noodlelab init-agent   # AGENTS.md, CLAUDE.md, skill, .mcp.json
codex mcp add noodlelab -- noodlelab mcp   # Codex CLI (Claude Code reads .mcp.json)
```

The MCP server has tools for:
- finding nodes and examples;
- saving and editing graphs;
- checking and running them;
- reading requirement verdicts;
- verifying scripts and graphs.

[docs/agents/adopt-in-existing-project.md](docs/agents/adopt-in-existing-project.md)
is the step-by-step for *"make my project verifiable"*.

**3. Plain Python**, for scripts, notebooks and tests (`pip install noodlelab`):

```python
import noodlelab.verify as nv

with nv.record("drop test") as rec:  # writes runs/<...>/provenance.json
    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)
```

```console
$ 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 %)
1 of 1 passed
```

What each piece does:
- **Units:** Pint quantities, so adding metres to seconds raises an error.
- **Uncertainty:** propagated as the GUM describes, with correlations kept.
  `nv.budget()` shows which input dominates, and `nv.monte_carlo()` checks
  whether the linear result holds.
- **Requirements:** written as text a person can read, and verified with margins.
- **Checking a result:** `rec.expect()` tests any condition, `rec.close_to()`
  compares with a reference value, and `@nv.traced` records a function's calls.

See the [quickstart](docs/agents/quickstart.md).

## The editor

Every node is a type-hinted Python function. You wire them up in the browser, see
previews on the nodes while you edit, and write the results up as a PDF.

<table>
<tr>
<td width="50%"><img src="docs/images/satellite.png" alt="Example 23, a satellite downlink link budget, with five requirements passed in the Requirements tab"><br><sub><b>A link budget</b> (example 23): symbolic equations with units, five requirements, a trade over candidate radios.</sub></td>
<td width="50%"><img src="docs/images/groundwater.png" alt="Example 20, groundwater nitrate: wells reprojected, joined with districts and interpolated, with map previews on the nodes"><br><sub><b>A geospatial study</b> (example 20): well data joined with districts, interpolated and summarised, with a map preview on every node.</sub></td>
</tr>
</table>

The **Reporting** canvas builds the report from what Processing computed. The
**Outputs** list has every figure, table and number, ready to drag in (⧉ shows
Processing and Reporting side by side):

![The Reporting canvas of example 23: report nodes for the title, equations, text, values and the requirements, with the Outputs list of figures and tables](docs/images/reporting.png)

Rendering the report gives the equations, figures, compliance matrix and a
reproducibility appendix (run, software versions, input checksums, every step):

![Four pages of the satellite link report: the requirements and link budget equations, the bit error rate, the margin plots and compliance matrix, and the reproducibility appendix](docs/images/report.png)

Also in the editor:
- **Requirements** and **Tracked** tabs, which follow verdicts and values from
  run to run, with where each came from.
- Checkpoints, so runs resume where they left off.
- Sweep, optimize and repeat zones.
- Uncertainty budgets and Monte Carlo.
- Export to plain Python.
- Shared servers with roles, and Slurm clusters.

[docs/editor.md](docs/editor.md) has the details, and the **Help** tab has 24
examples to open and run.

## Install

```bash
pip install noodlelab           # the library: noodlelab.verify, verify/run/test/mcp on the command line
uvx "noodlelab[full]" .         # the editor with every node pack, on this folder
```

| Tier | Adds |
|---|---|
| `noodlelab` | The library: units, uncertainty, requirements, checks, provenance, audit, the MCP server, headless runs. |
| `noodlelab[app]` | The editor and PDF reports. |
| `[maths]` · `[science]` · `[engineering]` · `[geo]` · `[full]` | Node packs: arrays and fitting, tables, statistics and signals, structures and heat, maps and rasters, everything. |

[docs/installation.md](docs/installation.md) lists every node by tier, and
explains adding packs and remote storage.

## Command line

```bash
noodlelab verify model.py analysis.graph.json --json  # run and audit: exit 1 if anything fails
noodlelab mcp                                         # the MCP server for agents (stdio)
noodlelab init-agent                                  # set up a project for agents
noodlelab run analysis.graph.json -v                  # run a graph headless
noodlelab test *.graph.json --baseline final          # checks and regression against a final run
noodlelab export analysis.graph.json -o analysis.py   # a graph as a plain Python script
noodlelab serve .                                     # the editor
```

## Writing a node

```python
from noodlelab import Quantity, node


@node(category="Engineering/Structures")
def bending_stress(moment: Quantity["N*m"], section_modulus: Quantity["mm^3"]) -> Quantity["MPa"]:
    """Bending stress at the outer fibre: M / Z."""
    return (moment / section_modulus).to("MPa")
```

The type hints decide the sockets, units and widgets. Links convert units on the
way in, and the editor refuses to link incompatible dimensions. The docstring is
the help text that people and agents read. Uncertainty propagates through the node
without any code in it: give it `1200 ± 30 N·m` and it returns
`(150.0 ± 3.7) MPa`. Ship nodes as a pack with a `noodlelab.nodes` entry point: see
[`examples/noodlelab-example-pack`](examples/noodlelab-example-pack), or run
`noodlelab new-pack`.

## More

- For agents: [AGENTS.md](AGENTS.md), [llms.txt](llms.txt),
  [docs/llms-full.txt](docs/llms-full.txt) (the whole API and every node in one
  file), [docs/agents](docs/agents/)
- [Installation tiers and every node](docs/installation.md) ·
  [The editor](docs/editor.md) ·
  [Shared servers, Slurm and Docker](docs/server.md) ·
  [Slurm in detail](docs/slurm.md) · [Releasing](docs/releasing.md)
- Development: `make install`, `make dev` (API on :8000, Vite on :5173),
  `make test lint`

## License

See [LICENSE](LICENSE).
