Skip to main content

notata

Structured filesystem logging for scientific runs

Tests Docs PyPI version Python Version Platform License Downloads codecov

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)

Source distribution for notata 0.3.0
File Size Uploaded
notata-0.3.0.tar.gz 24.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for notata 0.3.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page