exptoolkit
A small, typed toolkit for representing, processing, indexing, and plotting experimental data in Python.
exptoolkit provides a common data model for experimental measurements: define the columns that belong to a data type, attach physical units and normalization behavior, keep metadata with the data, and build reusable processing and plotting code on top of that model.
The library is intentionally domain-agnostic. Instrument- or experiment-specific logic can live in separate packages while sharing the same data and processing interfaces.
Features
- Schema-defined experimental data built on Polars
- Physical units and unit conversion with Pint
- Normalization-aware columns for extensive, intensive, and inverse-extensive quantities
- JSON-compatible metadata stored alongside measurement data
- Portable save/load format using Parquet data plus a JSON manifest
- Reusable processing interfaces for modifiers, converters, combiners, and featurizers
- Resource indexing by measurement ID and sample name
- Directory scanning with caching for incrementally updating resource indexes
- Backend-independent plotting with adapters for Matplotlib, Plotly, PyQtGraph, OpenPyXL, and XlsxWriter
- Type information included via
py.typed
Installation
pip install exptoolkit
Optional plotting dependencies can be installed with:
pip install "exptoolkit[plotting]"
For PyQtGraph/PySide6 support:
pip install "exptoolkit[qt]"
Quick start
Define an experimental data type
Columns are declared once on a BaseData subclass. Each column has a Polars dtype, a base unit, and optionally a physical role used during normalization.
import polars as pl
from exptoolkit.data import BaseData, Column, Role
class MeasurementData(BaseData):
time = Column(pl.Float64, "s")
voltage = Column(pl.Float64, "V")
capacity = Column(pl.Float64, "mAh", Role.EXTENSIVE)
data = MeasurementData(
{
"time": [0.0, 1.0, 2.0],
"voltage": [3.10, 3.25, 3.40],
"capacity": [0.0, 12.5, 25.0],
},
metadata={"sample": "sample-001"},
)
print(data.table)
print(data.voltage)
The declared schema is available on the class:
print(MeasurementData.schema)
Extra input columns are dropped by default, and missing schema columns are represented as null columns. This keeps instances of the same BaseData subclass structurally consistent.
Work with units
Column values can be requested in compatible units without changing the stored data:
capacity_ah = data.col_to_unit("capacity", "Ah")
time_min = data.col_to_unit("time", "min")
Normalize by sample amount
Experimental quantities often depend on how much material was measured. For example,
capacity may be recorded in mAh, but comparing samples of different mass is usually more
useful after expressing it per unit mass, such as mAh/g.
BaseData.normalize() represents this operation explicitly. A normalization amount and its
unit are stored with the data, and each column declares how it should respond through its
Role:
Role.EXTENSIVE— proportional to sample amount; divided by the normalization amount (for example,mAh→mAh/g)Role.INTENSIVE— independent of sample amount; left unchanged (for example, voltage or temperature)Role.INVERSE_EXTENSIVE— inversely proportional to sample amount; multiplied by the normalization amount
For example, if a measurement was obtained from a 25 mg sample:
normalized = data.normalize(25, "mg")
An extensive capacity column is then stored as capacity divided by 25, while intensive
columns such as time and voltage are unchanged. The normalization unit becomes part of
the effective physical unit:
print(normalized.get_unit("capacity"))
# mAh / mg
capacity_per_g = normalized.col_to_unit("capacity", "mAh/g")
Normalization is therefore not a generic rescaling of every numeric column. It is a
sample-amount transformation defined by the physical role of each column and tracked as
part of the BaseData state. denormalize() can restore the original extensive values
when the normalization amount is known.
Save and load
BaseData.save() writes a portable ZIP container containing the table as Parquet and metadata as JSON.
data.save("measurement.zip")
loaded = MeasurementData.load("measurement.zip")
Metadata is restricted to JSON-compatible values so saved data remains portable and inspectable.
Processing
exptoolkit.processing provides lightweight interfaces for reusable operations on BaseData objects:
Modifier— modifies one data object in placeConverter— converts one data object to anotherCombiner— combines multiple data objectsFeaturizer— converts experimental data into a fixed-schema numeric feature vector
Resource repository
ResourceRepo is a small in-memory index that associates external resources with measurement IDs and sample names.
from exptoolkit.repository import ResourceRepo
repo = ResourceRepo()
repo.add(
"/data/run001/sample_a.csv",
measurement_id="run001",
samples=["sample_a"],
data_type="csv",
)
print(repo.by_sample("sample_a"))
print(repo.by_measurement("run001"))
The repository can be serialized to JSON and restored later.
Scan a directory
DirectoryScanner can build and synchronize a repository from a directory layout in which each measurement has its own directory.
data/
├── run001/
│ ├── sample_a.csv
│ └── sample_b.csv
└── run002/
└── sample_c.csv
from exptoolkit.repository import DirectoryScanner, ResourceRepo
scanner = DirectoryScanner(
"data",
dir_regex=r"run\d+",
file_regex=r".*\.csv$",
)
repo = ResourceRepo()
scanner.scan_and_sync(repo)
The scanner maintains a per-measurement cache so unchanged directories do not need to be rescanned. Cache files can be saved and loaded with save_cache() and load_cache().
For layouts that do not fit DirectoryScanner, subclass ResourceScanner and implement owns() and scan().
Plotting
The plotting layer separates what to plot from the plotting backend. A plotter operates on the Target protocol, while backend adapters translate those operations to supported plotting libraries.
For a simple x-y plot:
from exptoolkit.plotter import XyPlotter
plotter = XyPlotter("time", "voltage", xunit="s", yunit="V")
plotter.plot(data, ax) # for example, a Matplotlib Axes
Supported targets depend on installed optional dependencies and include Matplotlib, Plotly, PyQtGraph, OpenPyXL, and XlsxWriter objects.
This makes it possible for experiment-specific plotting code to remain largely independent of the final output backend.
Design scope
exptoolkit focuses on infrastructure shared across experimental domains:
- structured measurement data
- units and normalization metadata
- generic processing contracts
- resource discovery and indexing
- plotting abstraction
Domain-specific analysis algorithms are intentionally kept outside the core package. The goal is to provide a small common layer that experiment-specific packages can build on rather than a collection of unrelated analysis routines.
Status
exptoolkit is currently a beta project. The core concepts are in active use, but APIs may still change between minor releases while the public interface is refined.
Development
This project uses uv for dependency management and local development.
Clone the repository and create the development environment:
uv sync
Run the test suite:
uv run pytest
Run linting, formatting checks, and type checks:
uv run ruff check .
uv run ruff format --check .
uv run mypy src
The test suite is also configured with tox for Python 3.9 through 3.13:
uv run tox
The test suite is configured for Python 3.9 through 3.13 with tox.
License
MIT
Release files for exptoolkit 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| exptoolkit-0.3.1.tar.gz | 40.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| exptoolkit-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 76.7 kB
Release files / exptoolkit-0.3.1.tar.gz
| Download URL | exptoolkit-0.3.1.tar.gz |
|---|---|
| Size | 40.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
49177f38ae1c9c2cf7894ab3c3b9120f46866cec42320f4473e66c351ae93906
|
|
BLAKE2b-256 checksum How to use checksums |
22f6632410d7e1775ea87fe50d0c103cfb9ffdbb130240c22ba1d299c1c6161e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / exptoolkit-0.3.1-py3-none-any.whl
| Download URL | exptoolkit-0.3.1-py3-none-any.whl |
|---|---|
| Size | 36.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1b6844ef5551b8beeaff74a66dc9d93546ca5b0df378fb1386e19be155207446
|
|
BLAKE2b-256 checksum How to use checksums |
06a1bd4dc73c77456fe22d0f72556a07a8b1cdb8ea35cf326eeb0699414c8cb3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|