Metadata-Version: 2.5
Name: noodlelab
Version: 0.8.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://nkalis.github.io/noodlelab/
Project-URL: Documentation, https://nkalis.github.io/noodlelab/stable/
Project-URL: LLM index, https://nkalis.github.io/noodlelab/stable/llms.txt
Project-URL: Agent guide, https://nkalis.github.io/noodlelab/stable/agents/guide.html
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 :: 4 - Beta
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: packaging>=23
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.11,>=0.10.1; extra == 'engineering'
Requires-Dist: fastapi>=0.115; extra == 'engineering'
Requires-Dist: matplotlib>=3.9; extra == 'engineering'
Requires-Dist: noodlelab-engineering<2,>=1; extra == 'engineering'
Requires-Dist: noodlelab-maths<2,>=1; extra == 'engineering'
Requires-Dist: noodlelab-science<2,>=1; extra == 'engineering'
Requires-Dist: noodlelab-symbolic<2,>=1; extra == 'engineering'
Requires-Dist: pandas>=3.0; 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.11,>=0.10.1; 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: lightgbm>=4.3; extra == 'full'
Requires-Dist: matplotlib>=3.9; extra == 'full'
Requires-Dist: noodlelab-engineering<2,>=1; extra == 'full'
Requires-Dist: noodlelab-geo<2,>=1; extra == 'full'
Requires-Dist: noodlelab-maths<2,>=1; extra == 'full'
Requires-Dist: noodlelab-ml<2,>=1; extra == 'full'
Requires-Dist: noodlelab-science<2,>=1; extra == 'full'
Requires-Dist: noodlelab-symbolic<2,>=1; extra == 'full'
Requires-Dist: pandas>=3.0; 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: scikit-learn>=1.5; extra == 'full'
Requires-Dist: scipy>=1.12; extra == 'full'
Requires-Dist: shapely>=2.0; extra == 'full'
Requires-Dist: skops>=0.10; extra == 'full'
Requires-Dist: sympy>=1.12; extra == 'full'
Requires-Dist: typst>=0.13; extra == 'full'
Requires-Dist: uvicorn[standard]>=0.30; extra == 'full'
Requires-Dist: xgboost-cpu>=2.1; (sys_platform == 'linux' or sys_platform == 'win32') and extra == 'full'
Requires-Dist: xgboost>=2.1; (sys_platform == 'darwin') and 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.9; extra == 'geo'
Requires-Dist: noodlelab-geo<2,>=1; extra == 'geo'
Requires-Dist: noodlelab-maths<2,>=1; extra == 'geo'
Requires-Dist: noodlelab-science<2,>=1; extra == 'geo'
Requires-Dist: noodlelab-symbolic<2,>=1; extra == 'geo'
Requires-Dist: pandas>=3.0; 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.9; extra == 'maths'
Requires-Dist: noodlelab-maths<2,>=1; extra == 'maths'
Requires-Dist: noodlelab-symbolic<2,>=1; extra == 'maths'
Requires-Dist: pandas>=3.0; 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: ml
Requires-Dist: fastapi>=0.115; extra == 'ml'
Requires-Dist: lightgbm>=4.3; extra == 'ml'
Requires-Dist: matplotlib>=3.9; extra == 'ml'
Requires-Dist: noodlelab-maths<2,>=1; extra == 'ml'
Requires-Dist: noodlelab-ml<2,>=1; extra == 'ml'
Requires-Dist: noodlelab-science<2,>=1; extra == 'ml'
Requires-Dist: noodlelab-symbolic<2,>=1; extra == 'ml'
Requires-Dist: pandas>=3.0; extra == 'ml'
Requires-Dist: psutil>=5.9; extra == 'ml'
Requires-Dist: pyarrow>=15; extra == 'ml'
Requires-Dist: salib>=1.4; extra == 'ml'
Requires-Dist: scikit-learn>=1.5; extra == 'ml'
Requires-Dist: scipy>=1.12; extra == 'ml'
Requires-Dist: skops>=0.10; extra == 'ml'
Requires-Dist: sympy>=1.12; extra == 'ml'
Requires-Dist: typst>=0.13; extra == 'ml'
Requires-Dist: uvicorn[standard]>=0.30; extra == 'ml'
Requires-Dist: xgboost-cpu>=2.1; (sys_platform == 'linux' or sys_platform == 'win32') and extra == 'ml'
Requires-Dist: xgboost>=2.1; (sys_platform == 'darwin') and extra == 'ml'
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.9; extra == 'science'
Requires-Dist: noodlelab-maths<2,>=1; extra == 'science'
Requires-Dist: noodlelab-science<2,>=1; extra == 'science'
Requires-Dist: noodlelab-symbolic<2,>=1; extra == 'science'
Requires-Dist: pandas>=3.0; 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

<!-- The MCP Registry reads the next line from PyPI to confirm who owns the server in server.json. -->
<!-- mcp-name: io.github.nkalis/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)

**The scientific method, for code.**

Units, uncertainty, requirements and provenance on every calculation, whether a
student, a professor, an engineer or an AI wrote it.

Most scientific and engineering code passes bare floating-point numbers around:
`stress = 180.0`. Is that MPa or psi? Plus or minus what? Is it good enough, and by
how much? Which inputs produced it, and could anyone rerun it next year? A good lab
notebook answers these questions. Code usually doesn't. noodlelab makes it answer
them, by giving every calculation:

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

Then it **audits** the calculation and tells you what is missing, so the work can
be checked by the person who did it and by everyone who relies on it.

AI agents get the same discipline through a built-in **MCP server** (`noodlelab mcp`):
Claude Code, Codex CLI or any MCP client can build, run and verify analyses you can
open and review. See [Built for code written by AI](https://nkalis.github.io/noodlelab/stable/#built-for-code-written-by-ai).

![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](https://nkalis.github.io/noodlelab/stable/images/hero.png)
<sub>Example 24: sizing a cantilever bracket. Each requirement shows its margin and the run that verified it.</sub>

## A calculation that shows its work

In 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, 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
    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
    """)  # verified when the block ends, against the results of the same name
```

```console
$ noodlelab verify drop.py
✓ drop.py
  ✓ drop test: passed
    = fall time: (0.6386 ± 0.0017) s
    = impact speed: (6.264 ± 0.017) m/s
    ✓ DRP-001: 0.6386 ± 0.0034 s (U, k = 2) meets ≤ 1 s (margin +0.3614 s, +36.1 % of the limit, ...)
    ✓ DRP-002: 6.264 ± 0.034 m/s (U, k = 2) meets ≤ 7 m/s (margin +0.7358 m/s, +10.5 % of the limit, ...)
1 of 1 passed
```

What each piece does:
- **Units:** Pint quantities, so adding metres to seconds raises an error instead
  of giving a wrong number.
- **Uncertainty:** propagated as the GUM describes, with correlations kept, also
  through long arrays (a profile of 100 000 points from three measurements
  costs milliseconds). `nv.budget()` shows which input dominates;
  `nv.from_repeats()` and `nv.correlated()` make Type A and correlated inputs.
  `nv.monte_carlo()` checks whether the linear result holds, running trials
  until its results are stable (`workers=` for models that run one trial at a
  time, `qmc=True` for fewer trials).
- **Requirements:** written as text a person can read, and verified with margins:
  against the result of the same name when the record closes, or at once with
  `rec.require(line, value)`.
- **Constants:** `nv.const.c`, `nv.const.g0` or `rec.constant("k_B")` instead of
  retyped digits: each has its unit and source, and measured ones (`G`, `m_e`...)
  their CODATA uncertainty. Your own go in a workspace's `constants.toml` or a
  graph's Constants tab.
- **Checking a result:** `rec.expect()` tests any condition, `rec.close_to()`
  compares with a reference value, and `@nv.traced` records a function's calls.
- **Provenance:** every run writes a `provenance.json` with the inputs and their
  sources, the results, the checks, the code's hash and git commit, file SHA-256s
  and the environment. `nv.load()` reads one back, and `noodlelab diff` says
  what changed between two runs and how the margins moved.
- **Notebooks, reports and tests:** in Jupyter a record shows as tables;
  `rec.to_markdown()` and `rec.to_latex()` give a calculation sheet; and the
  `nv_record` pytest fixture fails a test when its calculation fails.

See the [quickstart](https://nkalis.github.io/noodlelab/stable/agents/quickstart.html).

## Who it's for

| | noodlelab gives you |
|---|---|
| **Students** | Lab reports with the units right and the error propagation done for you. Unit mistakes fail loudly before they reach your answer, and a PDF report shows the equations, figures and uncertainties. |
| **Researchers and professors** | Results you can reproduce: every run records its inputs, code and environment. Uncertainty budgets and Monte Carlo, and reports with a reproducibility appendix. `noodlelab verify` can check students' submissions the same way it checks your own. |
| **Engineers in industry** | Requirements traceability with margins, a compliance matrix, regression tests against a baseline run, and shared servers with roles and Slurm clusters. |

## The audit

A calculation that runs is not the same as a calculation that is right.
`noodlelab verify` runs a script or graph and audits it (NL001–NL019): it flags
requirements that were never verified or whose margin is within the result's
uncertainty, results without units or uncertainty, inputs without a source,
and uncommitted code. For graphs it also checks the analysis against its
locked plan (`noodlelab plan lock`, a preregistration), counts the versions of
the analysis that were run, says when an AI agent made the last change, and
voids sign-offs (`noodlelab signoff`) when anything changed after them.

```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 0 only when every check passed and every requirement was
verified, so the same command works in CI, in a grading script, or as the last
step before you call an analysis finished. `--json` gives the verdict as data.

## 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="https://nkalis.github.io/noodlelab/stable/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="https://nkalis.github.io/noodlelab/stable/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](https://nkalis.github.io/noodlelab/stable/images/reporting.png)

Rendering the report gives the equations, figures, compliance matrix and a
reproducibility appendix (run, software versions, input checksums, every step).
The same report can come out as an IEEE-style article, a calculation note, a test
report, a one-page summary or slides, or in your own template
([reports and templates](https://nkalis.github.io/noodlelab/stable/reports.html)):

![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](https://nkalis.github.io/noodlelab/stable/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. An uncertain result also gets the
  probability that it meets its requirement, and a requirement can carry a
  decision rule (`risk=2.5%`, `guard=1`) for when the margin is within the
  uncertainty ([requirements and conformity](https://nkalis.github.io/noodlelab/stable/requirements.html)).
- **Calculators**: any saved graph as a simple form for people who never open
  the editor, shareable with a link that opens nothing else
  ([calculators](https://nkalis.github.io/noodlelab/stable/calculators.html)).
- A **Constants** tab and a Constant node: π, c, g0, k_B... built in, plus the
  workspace's and the graph's own.
- 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, remote storage and Slurm clusters
  ([below](https://nkalis.github.io/noodlelab/stable/#one-server-for-the-whole-team)).

[docs/editor.md](https://nkalis.github.io/noodlelab/stable/editor.html) has the details, the **Examples** panel has 29
examples to open and run, and **Help › Tutorial** walks through it all.

## Built for code written by AI

More and more scientific code will be written by AI. It is written quickly and
reads fluently, which makes a quiet mistake easy to miss: a dropped unit, an
ignored uncertainty, a design that "works" without saying by how much. noodlelab
is designed to be the human side of that work. Whatever an agent builds, you can
read it: graphs open as nodes in the editor, every result carries its unit and
uncertainty, every run leaves a record, and `noodlelab verify` fails until the
work has actually been checked. The agent does the typing; you stay the reviewer.

### An MCP server, built in

`noodlelab mcp` is a [Model Context Protocol](https://modelcontextprotocol.io)
server that ships with the bare library, with no extra dependencies. Add it to
Claude Code, Codex CLI, Cursor or any other MCP client, and the agent works
*through* noodlelab instead of writing loose floats next to it:

```bash
claude mcp add noodlelab -- uvx noodlelab mcp   # Claude Code
codex mcp add noodlelab -- uvx noodlelab mcp    # Codex CLI
```

| The agent can… | with the tools |
|---|---|
| learn the rules before it starts | `guide` |
| find the right building blocks | `list_nodes`, `describe_node`, `list_examples`, `get_example`, `copy_example` |
| build and change graphs | `list_graphs`, `get_graph`, `save_graph`, `edit_graph` |
| check and run them | `check_graph`, `run_graph`, `get_output` |
| know whether it is done | `requirements`, `verify` |
| reproduce and compare runs | `reproduce`, `diff_runs` |
| follow a locked plan and sign off as preparer | `plan_status`, `signoff` |
| find and install plugins (removing one is for a person) | `list_plugins`, `search_plugins`, `install_plugin`, `remove_plugin` |

Every run the agent starts is a real run: it writes provenance and adds to the
graph's requirement and tracked-value history, like a run from the editor. And
when the server is connected to a running editor (as it is in the Agent panel
below), every graph the agent saves reloads on your canvas as it saves it, so you
watch the analysis being built node by node.

**The Agent panel in the editor.** Press **Agent** to open Claude Code or Codex
CLI in a terminal under the canvas, already connected to the MCP server. Sign in
as it asks and describe the analysis. It builds the graph, the requirements and
the report through noodlelab's tools, and each graph it saves appears on your
canvas, where you can inspect it.

![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](https://nkalis.github.io/noodlelab/stable/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>

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

```bash
uv add noodlelab && noodlelab init-agent   # AGENTS.md, CLAUDE.md, skill, .mcp.json
```

`init-agent` registers the MCP server in `.mcp.json` (which Claude Code reads)
alongside the instructions and skill.

**As a Claude Code plugin.** The skill and the MCP server in one install, for
every project:

```bash
claude plugin marketplace add nkalis/noodlelab
claude plugin install noodlelab@noodlelab
```

The MCP server is also listed in the [MCP Registry](https://registry.modelcontextprotocol.io)
as `io.github.nkalis/noodlelab`, for clients that install servers from it.
[docs/agents/adopt-in-existing-project.md](https://nkalis.github.io/noodlelab/stable/agents/adopt-in-existing-project.html)
is the step-by-step for *"make my project verifiable"*.

### Everything else is machine-readable too

An agent can't skip a step without it showing:

| An agent needs to… | noodlelab gives it |
|---|---|
| know it is done | `noodlelab verify --json` (or the `verify` tool): exit 0 only when every check passed and every requirement was verified |
| be told what it skipped | the same audit you see above |
| prove what it ran | a `provenance.json` for every run |
| build things people can review | graphs in plain JSON (with a [schema](https://nkalis.github.io/noodlelab/stable/reference/graph-schema.html)) that open as nodes in the editor and render PDF reports |
| find and learn the package | [`llms.txt`](https://nkalis.github.io/noodlelab/stable/llms.txt), [`AGENTS.md`](https://nkalis.github.io/noodlelab/stable/agents/agents.html), a Claude skill, docstrings written to be read |

## One server for the whole team

`noodlelab serve` on a laptop needs no setup. The same editor also runs as a
shared server for a lab, a group or a company, with an admin portal for the rest.

**Users and roles.** With `--auth users`, admins create accounts and everyone
signs in with a personal token. Each workspace is a project with its own members,
and each member has one of four roles, from viewer (look at graphs, runs and
reports) through runner and editor to manager (members, guest links, finalising
runs); [docs/server.md](https://nkalis.github.io/noodlelab/stable/server.html) has the table.

The server enforces the roles; viewers and runners get a read-only canvas.
Graphs reach only their own workspace's files and the admin's mounts, so one
project cannot read another's (a shared folder on the server is a `local://` mount).
**Guest links** give someone without an account view-only or view-and-run access
to one workspace until they expire or are revoked, and a **calculator link**
lets them fill in and run one graph's [calculator](https://nkalis.github.io/noodlelab/stable/calculators.html) and
nothing else.

**Remote storage.** An admin gives remote storage a short name, a *mount*, such as
`lab-s3` → `s3://my-bucket/raw`, and can limit it to some workspaces. Nodes then
read `lab-s3://2026/run1.csv` as if it were local, and people see the mount's
name, never its URL or credentials. Any [fsspec](https://filesystem-spec.readthedocs.io)
backend works: S3 (and MinIO, Ceph, R2), Google Cloud Storage, Azure, SFTP, SMB,
HTTP, or a network share on the server. Install the extras you need, for example
`"noodlelab[full,s3]"`.

**Slurm clusters.** Point the server at a Slurm cluster under **Admin → Cluster**
and people get a **Run on** menu: *On the server* or *On the cluster*. A cluster
run is a batch job, but it looks the same in the editor: live progress, previews,
reports, **Stop** to cancel. Its checkpoints and provenance land in the workspace,
so the next run reuses them wherever it runs, and the provenance records who ran
it.

[docs/server.md](https://nkalis.github.io/noodlelab/stable/server.html) covers roles, guest links and Docker, and
[docs/slurm.md](https://nkalis.github.io/noodlelab/stable/slurm.html) the cluster setup.

## Install

```bash
pip install noodlelab           # the library: noodlelab.verify, verify/run/test 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, headless runs, the MCP server. |
| `noodlelab[app]` | The editor and PDF reports. |
| `[maths]` · `[science]` · `[engineering]` · `[geo]` · `[ml]` · `[full]` | Node packs: arrays and fitting, tables, statistics and signals, structures and heat, maps and rasters, machine learning (scikit-learn, XGBoost, LightGBM), everything. |

[docs/installation.md](https://nkalis.github.io/noodlelab/stable/installation.html) 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 run analysis.graph.json -v                  # run a graph headless
noodlelab test *.graph.json --baseline final          # checks and regression against a final run
noodlelab reproduce runs/<run> --clean-env            # run it again, in a fresh env, and compare
noodlelab diff runs/<run> runs/<earlier run>          # what changed: inputs, results, margins, code
noodlelab export analysis.graph.json -o analysis.py   # a graph as a plain Python script
noodlelab serve .                                     # the editor
noodlelab init-agent                                  # set up a project for AI agents
noodlelab mcp                                         # the MCP server for agents (stdio)
```

## 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`. A node never changes what it is given: results are cached and
shared with every node linked to them, so arrays arrive read-only (copy one before
changing it, `x = x.copy()`), while tables and lists arrive as copies of their own.
Ship nodes as a plugin: `noodlelab new-plugin thermo` scaffolds one, and anyone
can install it from its git repository with `noodlelab plugins add`. See
[Plugins](https://nkalis.github.io/noodlelab/stable/plugins/index.html).

## More

- **Documentation: [nkalis.github.io/noodlelab](https://nkalis.github.io/noodlelab/)**
- [Installation tiers and every node](https://nkalis.github.io/noodlelab/stable/installation.html) ·
  [The editor](https://nkalis.github.io/noodlelab/stable/editor.html) ·
  [Requirements and conformity](https://nkalis.github.io/noodlelab/stable/requirements.html) ·
  [Reports and templates](https://nkalis.github.io/noodlelab/stable/reports.html) · [Calculators](https://nkalis.github.io/noodlelab/stable/calculators.html) ·
  [Shared servers, Slurm and Docker](https://nkalis.github.io/noodlelab/stable/server.html) ·
  [Remote storage](https://nkalis.github.io/noodlelab/stable/storage.html) ·
  [Slurm in detail](https://nkalis.github.io/noodlelab/stable/slurm.html) · [Releasing](https://nkalis.github.io/noodlelab/stable/releasing.html)
- For AI agents: [AGENTS.md](https://nkalis.github.io/noodlelab/stable/agents/agents.html), [llms.txt](https://nkalis.github.io/noodlelab/stable/llms.txt),
  [docs/llms-full.txt](https://nkalis.github.io/noodlelab/stable/llms-full.txt) (the whole API and every node in one
  file), [docs/agents](https://nkalis.github.io/noodlelab/stable/agents/)
- Development: `make install`, `make dev` (API on :8000, Vite on :5173),
  `make test lint`

## License

See [LICENSE](https://nkalis.github.io/noodlelab/stable/LICENSE.txt).
