notata
Structured filesystem logging for scientific runs
notata is a minimal library for structured filesystem logging of scientific runs.
Each Logbook creates a run directory with parameters, arrays, plots, artifacts, metadata, and a timestamped log. Explicit. Reproducible. Grep-friendly.
Unlike ML logging tools (e.g. W&B, TensorBoard, MLflow), notata is built for scientific workflows such as simulations, solvers, and numerical experiments, where file-based logging matters more than dashboards. No servers, no setup, no reinventing the wheel for each new project.
Installation
pip install notata
Quick Start
Context Manager
Logs a single simulation run to log_<run_id>/, including arrays, metadata, and messages.
from notata import Logbook
import numpy as np
with Logbook("oscillator_dt1e-3", params={"omega": 2.0, "dt": 1e-3, "steps": 10_000}) as log:
omega = 2.0
dt = 1e-3
steps = 10_000
x, v = 1.0, 0.0
xs = np.empty(steps)
vs = np.empty(steps)
E = np.empty(steps)
for n in range(steps):
a = -omega**2 * x
x += v*dt + 0.5*a*dt*dt
a_new = -omega**2 * x
v += 0.5*(a + a_new)*dt
xs[n], vs[n] = x, v
E[n] = 0.5*(v**2 + (omega*x)**2)
if (n+1) % 2000 == 0:
log.info(f"step={n+1} x={x:.4f} v={v:.4f} E={E[n]:.6f}")
log.arrays("trajectory", x=xs, v=vs)
log.array("energy", E)
log.json("final_state", {"x": float(x), "v": float(v), "E": float(E[-1])})
This creates a structured folder with logs, parameters, and output arrays for reproducibility.
Manual Lifecycle
For full control, create a Logbook manually and mark it complete when you're done.
from notata import Logbook
import numpy as np
Nx = Ny = 64
kappa = 0.01
dx = 1.0
dt = 0.2 * dx*dx / kappa
X, Y = np.meshgrid(np.linspace(-1,1,Nx), np.linspace(-1,1,Ny), indexing="ij")
T = np.exp(-6*(X**2 + Y**2))
log = Logbook("heat_eq")
log.params(Nx=Nx, Ny=Ny, kappa=kappa, steps=steps)
for step in range(500):
lap = (np.roll(T,1,0)+np.roll(T,-1,0)+np.roll(T,1,1)+np.roll(T,-1,1)-4*T)
T += kappa * dt * lap
if (step+1) % 100 == 0:
log.array(f"states/T_step{step+1}", T)
log.info(f"step={step+1} maxT={T.max():.4f}")
log.json("final_stats", {"max": float(T.max()), "mean": float(T.mean())})
log.mark_complete()
Parameter sweeps with Experiment
Automatically log multiple runs, each in its own directory, with structured metadata and failure tracking:
from notata import Experiment
import numpy as np
exp = Experiment("falling_ball")
for dt in [0.01, 0.5]: # stable vs unstable
log = exp.add(dt=dt, skip_existing=True)
if log is None:
continue
with log:
v, h = 0.0, 100.0
for _ in range(100):
v += 9.81 * dt
h -= v * dt
if h < 0:
raise RuntimeError(f"Object hit the ground (h={h:.2f})")
log.json("metrics", {"final_height": h, "final_speed": v})
Each run creates a log_<run_id>/ folder and appends a row to index.csv with parameters, status, and final metrics:
| run_id | dt | status | final_height | final_speed |
|---|---|---|---|---|
| falling_ball_dt_0.01 | 0.01 | complete | 95.04595 | 9.81 |
| falling_ball_dt_0.5 | 0.5 | missing |
Using ExperimentReader
Since notata log dirs are somewhat verbose, we also provided a utility wrapper to load and read the data.
This makes it more intuitive when you need to compare runs (e.g. in experiments), or you just don't want to deal with the files paths.
For single runs you can directly use notata.LogReader.
from notata import ExperimentReader
import matplotlib.pyplot as plt
# Load the experiment
exp = ExperimentReader("outputs/oscillator_sweep")
# Plot energy vs time for each run
fig, ax = plt.subplots()
for run in exp:
omega, dt = run.params['omega'], run.params['dt']
energy = run.load_array("energy")
label = f"omega={omega}, dt={dt}"
ax.plot(energy, label=label)
ax.set(xlabel="Time step", ylabel="Energy", title="Energy vs Time")
ax.legend()
plt.show()
# Print summary of runs
for run in exp:
status = run.meta.get('status', 'unkown')
duration = run.meta.get('runtime_sec', 'unknown')
print(f"Run ID: {run.run_id}, {status=}, {duration=}")
API quick reference
Four public names, all importable from notata. Everything below writes into
the run directory shown under Output format.
from notata import Logbook, Experiment, LogReader, ExperimentReader
# --- writing a run ---------------------------------------------------------
log = Logbook(run_id, base_dir="outputs", params=None,
overwrite=False, preallocate=False, callback=None)
log.params(ext="yaml", **kwargs) # params.yaml (or params.json)
log.meta(**fields) # merge fields into metadata.json
log.array("energy", arr) # data/energy.npy
log.arrays("state", x=x, v=v) # data/state.npz (compressed=True)
log.json("metrics", {"error": 1e-6}) # artifacts/metrics.json
log.text("stdout", "...") # artifacts/stdout.txt
log.pickle("model", obj) # artifacts/model.pkl
log.bytes("weights.bin", b"...") # artifacts/weights.bin
log.plot("energy", fig=None, dpi=200, formats=("png",)) # plots/energy.png
log["artifacts/custom.dat"] # Path inside the run, parents created
log.info(msg); log.warning(msg); log.error(msg); log.debug(msg)
log.elapsed # seconds since init
log.status # initialized | complete | failed
log.mark_complete(); log.mark_failed(reason)
# As a context manager: complete on clean exit, failed if an exception escapes.
with Logbook("run1", params={"dt": 1e-3}) as log:
...
# --- sweeping --------------------------------------------------------------
exp = Experiment("sweep", base_dir="outputs")
log = exp.add(skip_existing=False, **params) # None if skipped
exp.to_dataframe() # pandas view of index.csv
exp.select(dt=0.01) # filtered rows
# --- reading back ----------------------------------------------------------
run = LogReader("outputs/log_run1")
run.run_id, run.params, run.meta # dicts straight from disk
run.arrays # ["energy", "state:x", ...]
run.artifacts # ["metrics.json", "nested/config.json", ...]
run.plots # filenames under plots/
run.load_array("energy") # data/energy.npy
run.load_array("state:x") # key "x" inside state.npz
run.load_json("metrics") # artifacts/metrics.json (.json optional)
exp = ExperimentReader("outputs/sweep")
len(exp); exp["run_id"]; exp.params; exp.meta
for run in exp: # each item is a LogReader
...
Notes worth knowing: Logbook refuses to clobber an existing run directory
unless overwrite=True; plot needs matplotlib and to_dataframe needs
pandas, both optional; and methods are not thread- or process-safe, so
coordinate externally when logging from multiple workers.
Output format
Data is stored as following in order to be intuitive to explore:
log_<run_id>/
log.txt
metadata.json
params.yaml
data/
plots/
artifacts/
where the files follow:
| Path / Pattern | Purpose / Format |
|---|---|
log.txt |
Plain text log; lines: [YYYY-MM-DDTHH:MM:SS] LEVEL message |
metadata.json |
Run metadata: status, start_time, optional end_time, runtime_sec, optional failure_reason, run_id |
params.yaml / params.json |
Parameter snapshot (latest saved form) |
data/*.npy / data/*.npz |
.npy for single arrays (array()); .npz for multi-array bundles (arrays(...)) |
plots/*.(png|pdf|svg) |
Saved figures (save_plot) |
artifacts/*.txt |
Text artifacts (save_text) |
artifacts/*.json |
JSON artifacts (save_json) |
artifacts/*.pkl |
Pickled objects (save_pickle) |
artifacts/* (other) |
Raw bytes (save_bytes) |
artifacts/**/ |
Nested artifact categories |
Documentation
Full documentation, tutorials, and examples are available at: https://notata.readthedocs.io/en/latest/
Citation
You don't have to, but if you use notata in your research and need to reference it, please cite it as follows:
@software{notata_2025,
author = {Albert Alonso},
title = {notata: Structured Filesystem Logging for Scientific Runs},
url = {https://github.com/alonfnt/notata},
version = {0.3.0},
year = {2025}
}
License
MIT License
Metadata
Release files for notata 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| notata-0.3.0.tar.gz | 24.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| notata-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 41.0 kB
Release files / notata-0.3.0.tar.gz
| Download URL | notata-0.3.0.tar.gz |
|---|---|
| Size | 24.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6fe087296678c6b721ab2110ac579eccc9b64c8972607fd2deb131ce739eb300
|
|
BLAKE2b-256 checksum How to use checksums |
fd4dc323cd0fa121ffe261e1e06b9a706d8ebc77b9f7ac2b607dbaac197748cd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 26, 2026.
Transparency logRelease files / notata-0.3.0-py3-none-any.whl
| Download URL | notata-0.3.0-py3-none-any.whl |
|---|---|
| Size | 16.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
77f244d4d30dfef675b626ae47c814c3456a80c1852ea1747c50e507b9d3e2b7
|
|
BLAKE2b-256 checksum How to use checksums |
8a3313c502995895563e38e03ad220b9cdec3d798f89a15a94950307b87d49d3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 26, 2026.
Transparency log