Reproducibility¶
A result is only worth something if it can be had again: by a colleague, by a
reviewer, or by you in a year. noodlelab records every run (see
noodlelab.core.provenance), and gives tools on top of that record.
Reproduce a run¶
noodlelab reproduce runs/20250102-093000-a1b2c3d4e5f6 # a run folder
noodlelab reproduce bundle.zip # a bundle downloaded from the editor
noodlelab reproduce bundle.zip --clean-env # in a fresh environment (needs uv)
It runs the graph again exactly as recorded: the same settings, the same run
seed, and nothing reused from caches or checkpoints. Then it compares every
result with the recorded one (--rtol, --atol; the defaults are 1e-9 and 0).
Exit code 0 means everything is the same, 1 that something differs, 2 that it
could not run. When results differ it says why they may differ: node code that
changed, input files that changed, other package versions, another Python or
platform, another git commit, or uncommitted changes when the run was made.
--clean-env builds a new virtual environment with the recorded Python and
package versions, and reproduces the run there. What is tested is then the
record, not the machine it was made on. Agents have the same tool over MCP
(reproduce, and diff_runs to compare two runs).
Reproducing a run: does it still give the same answer?
A result is only as good as the chance that someone else, or you in a year, gets it again. Most published research code does not even run for others, and when it runs it often gives other numbers, because a package changed, an input file was replaced, or a seed was different. Checking that by hand is a chore nobody does, so this makes it one command:
noodlelab reproduce runs/20250102-093000-a1b2c3d4e5f6
noodlelab reproduce bundle.zip --clean-env
It takes a run folder, its provenance.json or a reproducibility bundle
(see bundle), runs the graph exactly as recorded (same settings, same
run seed, nothing reused from caches or checkpoints), and compares every
result with the recorded ones within a tolerance (see baseline).
When results differ it says why they may differ, from the two provenance
records (see rundiff): node code that changed, input files that
changed, other package versions, another platform, uncommitted changes.
--clean-env goes further: it builds a fresh virtual environment with the
recorded Python and package versions (with uv) and reproduces the run in
it, so what is tested is the record, not the machine it was made on.
A reproduction is not a variant of the analysis (see variants): it
runs one that was already run.
Compare two runs¶
noodlelab diff runs/<this run> runs/<an earlier run> # folders or their provenance.json
noodlelab diff --json runs/<this run> runs/<an earlier run>
It says what changed from the earlier run to this one. For two records of a
script (noodlelab.verify), that is the inputs and results by name, the
requirements, each check’s verdict and how its margin moved, the script and
its git commit, and the files read and written. For two graph runs, it is the
settings and links, node code, input files and, when both runs kept their
values, the results. Python and the package versions are compared for both.
Exit code 0 means nothing differs (within --rtol and --atol), 1 that
something does, and 2 that a record could not be read, or that one is a
script’s and the other a graph run’s. In Python, nv.load(run).diff(earlier)
gives the same.
What changed between two runs, from their provenance records.
For a graph run, noodlelab.core.baseline compares the results; this
compares what produced them: settings and links, node code, input files, and
the software environment. Nodes match by id, or else by title and type, as in
noodlelab.core.baseline.compare().
A script’s record (noodlelab.verify.record()) holds its inputs, results
and checks themselves, so diff_records() compares those too, by name.
diff_runs() takes either kind, and is what noodlelab diff prints.
Run seeds¶
Random nodes (Monte Carlo, sampling designs, model training) take a seed with
a fixed default, so a graph gives the same numbers every time. To see whether a
result depends on the random numbers you happened to draw, run with other ones:
noodlelab run analysis.graph.json --seed 7
One seed for a whole run.
Random nodes (Monte Carlo, sampling designs, model training…) each take a
seed input with a fixed default, so a graph gives the same numbers every
time it runs. That settles “is it repeatable?”, but not “does the result
depend on the random numbers I happened to draw?”: to answer that, run again
with other random numbers, all of them.
A run seed does that (noodlelab run --seed 7): every seed input the
graph leaves at its default gets its own seed, derived from the run seed and
the node’s id, so two nodes never share a random stream and the same run seed
always gives the same seeds. Seeds set in the graph are kept: the graph’s
author chose them. Without a run seed nothing changes.
The run seed is recorded with the run (see provenance), and the seeds
it gave, so noodlelab reproduce runs with the same numbers.
Analysis variants¶
Every change to a graph’s analysis is logged, so the paths tried on the way to a result are not forgotten.
Analysis variants: every version of an analysis that was run.
An analysis rarely goes straight from question to answer. On the way, a filter is tried, an outlier dropped, a model swapped, a threshold nudged, a random seed changed, and each of those runs is a forking path: a choice that could have gone another way. The result finally reported is one of them. The more of them there were, and the less the reader knows about them, the less a significant-looking result means (Gelman and Loken, “The garden of forking paths”, 2013).
So every run of a graph is compared with the one before it, and when the
analysis changed, a line is added to
.noodlelab/variants/<graph>.jsonl in the workspace: a fingerprint of
every node (its type, settings, links and source code; see
fingerprints()), the run seed, and what changed since the previous
variant, by node title. Running the same analysis again adds nothing; runs
are not variants. The report’s analysis history (Add Analysis History)
and noodlelab verify (rule NL017) read it back, and so does a plan lock
(see locks), which compares the analysis with the one planned.
Unlike tracked values, variants stay when their runs are deleted: deleting a run does not undo having tried it. Report nodes are left out: rewording the report does not change the analysis.
Plans and sign-offs¶
noodlelab plan lock analysis.graph.json --hypothesis "the coating lowers wear by 10 %" --tag
noodlelab plan status analysis.graph.json
noodlelab plan amend analysis.graph.json --reason "sensor 3 failed; its data is dropped"
noodlelab signoff analysis.graph.json --role checker --rev B
noodlelab signoff analysis.graph.json --list
The report nodes Add Analysis History and Add Sign-off put the plan, its deviations, the versions of the analysis that were run and the signatures into the PDF.
Locks on an analysis: a plan fixed before the results, sign-offs after them.
Both answer the question a reader of a result cannot answer alone: was the analysis decided before its outcome was known, and has it changed since someone checked it?
A plan lock (preregistration) is taken before the data is in, or at least before the results are looked at:
noodlelab plan lock analysis.graph.json --hypothesis "the coating lowers wear by 10 %"
It records a snapshot of the analysis (see snapshot()): every node’s
fingerprint (type, settings, links, code; see variants), the
requirements with their limits, and a hash of every data file the graph
reads, or "absent" for a file not there yet, which shows the data was
not in when the plan was made. Later, noodlelab verify compares the
analysis with the plan: a node changed, a threshold moved or a data file
replaced since the lock is a deviation (rules NL012 to NL014). Deviations
are allowed, but they must be said: noodlelab plan amend --reason "..."
records them with the reason, and they are then reported, not failed. In
studies of preregistered research, most papers deviated from their plans and
many did not say so; this makes the saying cheap and the not saying visible.
A sign-off is the engineering form of the same idea: a preparer, a checker and an approver put their names to a run of the analysis:
noodlelab signoff analysis.graph.json --role checker --rev B
The sign-off holds the same snapshot. Any later change to the analysis, its
requirements or its data voids it (rule NL018), so a signed calculation
cannot quietly drift from what was checked. Only a person can sign as checker
or approver: the MCP tools refuse to (see noodlelab.agent.tools).
Both are plain JSON next to the graph (<graph>.plan.json and
<graph>.signoff.json), meant to be committed with it: git then holds the
proof of when each was made.
Results that are not numbers¶
Results that are not numbers.
A calculation that divides by zero, takes the log of a negative number or
overflows does not stop: numpy returns nan or inf and everything
downstream inherits it, often ending as a plot with a gap or a requirement
that “failed” for no visible reason. The executor looks at what every node
returns and warns at the node where such a value first appears (see
noodlelab.core.context.warn()); the run goes on.
Not every nan is a mistake, so the rules follow what nan means where
(and a node says that a result does not apply with None, not nan):
a single number (a float, a quantity, the value of an uncertain number) that is not finite is always reported;
inside arrays
nanusually marks missing data (a gap in a series, a raster’s nodata), so onlyinf, or an array that isnanthroughout, is reported;in tables
infis as often meant (the open ends of a set of classes, a bin edge), so only a numeric column that isnanthroughout is reported;an uncertainty of
nanmeans “not known” (seenoodlelab.core.uncertainty) and is never reported.
This must stay cheap: it runs on every result, so it looks at numeric dtypes with vectorised numpy and never walks Python containers.