Describe your thing
Whatever is on your bench, a material, an electrode, a cell, a test, the data it produced, there is a record for it. Each example below is real authoring code and the record it produces, executed against the current library and kept in sync by the test suite.
A material
Any substance in the cell — actives, binders, salts, solvents — with its properties, reusable across every cell that contains it.
from battinfo.authoring import material
from battinfo.materials import material_spec_from_component
nmc = material(
"NMC811",
molecular_formula="LiNi0.8Mn0.1Co0.1O2",
mass_fraction={"value": 96, "unit": "%"},
)
# Lift it to a standalone, reusable material-spec record with its own IRI
record = material_spec_from_component(nmc, material_class="active_material")An electrode
A bill of materials plus the numbers that define the coating: loading, calendered density, current collector.
from battinfo.authoring import bom, electrode, material
cathode = electrode(
bom=bom(
active_material=material("NMC811", mass_fraction={"value": 96, "unit": "%"}),
binder=material("PVDF", mass_fraction={"value": 2, "unit": "%"}),
additive=material("Carbon black", mass_fraction={"value": 2, "unit": "%"}),
),
loading={"value": 18.5, "unit": "mg/cm2"},
calendered_density={"value": 3.4, "unit": "g/cm3"},
current_collector="Aluminium foil",
)
record = cathode.model_dump(exclude_none=True)An electrolyte
Salt, solvents, and additives — all the same material(...) form, fractions and concentrations inline.
from battinfo.authoring import electrolyte_recipe, material
lp30 = electrolyte_recipe(
family="organic",
salt=material("LiPF6", concentration={"value": 1.0, "unit": "mol/L"}),
solvents=[
material("EC", volume_fraction={"value": 50, "unit": "%"}),
material("DMC", volume_fraction={"value": 50, "unit": "%"}),
],
additives=[material("VC", mass_fraction={"value": 2.0, "unit": "%"})],
comment="LP30 + 2% VC",
)
record = lp30.model_dump(exclude_none=True)A separator
Base material, thickness, porosity — and free text for what the numbers can't hold.
from battinfo.authoring import properties, separator_spec
sep = separator_spec(
material="Polypropylene",
thickness={"value": 25.0, "unit": "µm"},
properties=properties(porosity={"value": 41, "unit": "%"}),
comment="Celgard 2500, single-layer PP",
)
record = sep.model_dump(exclude_none=True)A cell spec
The product datasheet, as data: identity, format, chemistry, and rated properties with units that resolve to EMMO.
from battinfo import CellSpec
spec = CellSpec(
# In the workspace flow the IRI is minted for you at ws.save();
# standalone, you set it (or let publish() mint it).
id="https://w3id.org/battinfo/spec/7d9k-2m4p-8t3x-6nq5",
manufacturer="Samsung SDI",
model="INR21700-50E",
format="cylindrical",
chemistry="Li-ion",
positive_electrode_basis="NMC",
negative_electrode_basis="graphite",
properties={
"nominal_capacity": {"value": 5.0, "unit": "Ah"},
"nominal_voltage": {"value": 3.6, "unit": "V"},
"mass": {"value": 68.0, "unit": "g"},
},
source={"type": "datasheet", "retrieved_at": 1750000000},
)
record = spec.to_record()A cell
One specific physical item with a serial number, permanently linked to the spec that describes it.
from battinfo import Cell, CellSpec
spec = CellSpec(
id="https://w3id.org/battinfo/spec/7d9k-2m4p-8t3x-6nq5",
manufacturer="Samsung SDI",
model="INR21700-50E",
format="cylindrical",
chemistry="Li-ion",
)
cell = Cell(
id="https://w3id.org/battinfo/cell/y9xy-kr0v-y5tn-dfj7",
cell_spec=spec, # every cell links to its spec
serial_number="LAB-2026-0001",
manufactured_at="2026-01-15",
)
record = cell.to_record()A test spec
The reusable procedure — steps in PyBaMM syntax a machine can re-run, conditions a human can check.
from battinfo import TestSpec
protocol = TestSpec(
id="https://w3id.org/battinfo/spec/kxwy-5f5f-f682-hhch",
name="1C cycle life at 25 °C",
kind="cycling",
experiment=[ # PyBaMM syntax — runnable as-is
"Charge at 1C until 4.2 V",
"Hold at 4.2 V until C/20",
"Discharge at 1C until 2.5 V",
"Rest for 10 minutes",
],
conditions={"ambient_temperature": {"value": 25.0, "unit": "degC"}},
)
record = protocol.to_record()Equipment + channels
The cycler on the bench: the model as a spec, the physical unit, and one channel per slot — so every test can say exactly where it ran.
from battinfo.api import create_channel, create_equipment, create_equipment_spec
spec = create_equipment_spec(
# Explicit IRIs here; ws.add("equipment", ...) mints them for you.
id="https://w3id.org/battinfo/spec/7d9k-2m4p-8t3x-6nq5",
name="SkyRC MC3000",
manufacturer="SkyRC",
model="MC3000",
equipment_class="cycler", # category is data, never a namespace
channel_count=4,
supported_chemistries=["NiMH", "Li-ion", "LiFePO4", "Na-ion"],
)
unit = create_equipment(
id="https://w3id.org/battinfo/equipment/y9xy-kr0v-y5tn-dfj7",
equipment_spec_id=spec["equipment_spec"]["id"],
serial_number="MC3K-2026-0001",
name="Cycler 1",
location="Lab B",
status="active",
)
channels = [ # uid deterministic from (unit, index)
create_channel(equipment_id=unit["equipment"]["id"], index=i,
label=f"MC3000-A/CH{i}")
for i in (1, 2, 3, 4)
]
record = {
"equipment_spec": spec["equipment_spec"],
"equipment": unit["equipment"],
"channels": [c["channel"] for c in channels],
}A test
One execution of a procedure on one cell: instrument, status, and the link every dataset hangs off.
from battinfo import Cell, CellSpec, Test
cell = Cell(
id="https://w3id.org/battinfo/cell/y9xy-kr0v-y5tn-dfj7",
cell_spec=CellSpec(
id="https://w3id.org/battinfo/spec/7d9k-2m4p-8t3x-6nq5",
manufacturer="Samsung SDI", model="INR21700-50E",
format="cylindrical", chemistry="Li-ion",
),
serial_number="LAB-2026-0001",
)
test = Test(
id="https://w3id.org/battinfo/test/6nec-h262-tthy-4rnt",
cell=cell, # what you did, to which cell
kind="capacity_check",
protocol="C/10 constant-current discharge",
instrument="Biologic VMP-300",
status="completed",
)
record = test.to_record()A dataset
The measured files, linked to the cell and test that produced them — the shape data portals understand (DCAT).
from battinfo import Cell, CellSpec, Dataset, Test
cell = Cell(
id="https://w3id.org/battinfo/cell/y9xy-kr0v-y5tn-dfj7",
cell_spec=CellSpec(
id="https://w3id.org/battinfo/spec/7d9k-2m4p-8t3x-6nq5",
manufacturer="Samsung SDI", model="INR21700-50E",
format="cylindrical", chemistry="Li-ion",
),
serial_number="LAB-2026-0001",
)
test = Test(
id="https://w3id.org/battinfo/test/6nec-h262-tthy-4rnt",
cell=cell, kind="cycling", status="completed",
)
dataset = Dataset(
id="https://w3id.org/battinfo/dataset/0rp6-kncv-cyem-qwcd",
path="data/cycle-life-run", # the folder with your measured files
cell=cell,
test=test,
name="INR21700-50E cycle-life dataset",
license="CC-BY-4.0",
access_url="https://doi.org/10.5281/zenodo.1234567", # where the data lives
)
record = dataset.to_record()Want to build one of these yourself? The Playground starts you from a working template and checks it as you type; Validate gives a finished record a pass/fail verdict.