Prototype. BattINFO is in active development, so content and tools are still changing.
BattINFO

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.

you write
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.

you write
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.

you write
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.

you write
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.

you write
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.

you write
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.

you write
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.

you write
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.

you write
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).

you write
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.