Guide 1 — Concepts#
BattINFO is the implementation layer for the EMMO domain-battery ontology. It gives you JSON schemas, a Python library, and a CLI for authoring, validating, and publishing battery metadata as machine-readable Linked Data.
This guide explains the data model, record types, and the semantic layer.
Already have cycler data on disk? Jump straight to Guide 6 — Publish your first dataset and come back here for the concepts.
Estimated time: 10 minutes
[1]:
import json
from pathlib import Path
# This notebook runs from its own folder (docs/guides). Everything it
# writes lands in a throwaway scratch folder next to it.
SCRATCH = Path("_scratch/guide-01").resolve()
SCRATCH.mkdir(parents=True, exist_ok=True)
The data model#
Every battery experiment generates four kinds of record. BattINFO formalises them as a provenance chain:
CellSpec ─────────────────────────────────
│ "Panasonic NCR18650B is a cylindrical Li-ion cell, 3.4 Ah"
│
└─► Cell ───────────────────────────────
│ "This specific cell, serial number LAB-001"
│
└─► Test ──────────────────────────────
│ "1C cycle-life test at 25 °C on LAB-001"
│
└─► Dataset ──────────────────
"The voltage/current time-series"
Each record has a permanent, opaque IRI under https://w3id.org/battinfo/. The chain is machine-readable — a downstream tool can follow links from a dataset all the way back to the cell specification.
A real record#
[2]:
# Load the canonical A123 cell-spec record that ships with the repo
a123 = json.loads(
# Repo files sit two levels up from this notebook.
Path("../../examples/cell-spec/A123__ANR26650M1-B.json").read_text(encoding="utf-8")
)
# Top-level structure
list(a123.keys())
[2]:
['schema_version', 'cell_spec', 'properties', 'provenance']
[3]:
# The product identity block
a123["cell_spec"]
[3]:
{'id': 'https://w3id.org/battinfo/spec/7d9k-2m4p-8t3x-6nq5',
'short_id': '7d9k2m',
'identifier': 'cell-spec:7d9k-2m4p-8t3x-6nq5',
'name': 'A123 ANR26650M1-B',
'model': 'ANR26650M1-B',
'manufacturer': {'type': 'Organization',
'name': 'A123',
'id': 'https://w3id.org/battinfo/organization/9k3e-absq-8131-vyjd'},
'brand': {'type': 'Brand', 'name': 'A123'},
'category': 'battery cell',
'cell_format': 'cylindrical',
'chemistry': 'Li-ion',
'positive_electrode_basis': 'LFP',
'negative_electrode_basis': 'unknown',
'size_code': 'R26650',
'iec_code': 'IFpR26650',
'country_of_origin': 'United States',
'year': 2012}
[4]:
# Quantitative properties
a123["properties"]
[4]:
{'nominal_capacity': {'value': 2.5,
'unit': 'Ah',
'raw': {'text': 'Cell Capacity (nominal/minimum) (0.5C Rate) 2.5/2.4 Ah',
'page': 1,
'confidence': 0.8}},
'minimum_capacity': {'value': 2.4,
'unit': 'Ah',
'raw': {'text': 'Cell Capacity (nominal/minimum) (0.5C Rate) 2.5/2.4 Ah',
'page': 1,
'confidence': 0.8}},
'nominal_voltage': {'value': 3.3,
'unit': 'V',
'raw': {'text': 'Voltage (nominal) 3.3V', 'page': 1, 'confidence': 0.8}},
'internal_resistance': {'value': 6.0,
'unit': 'mΩ',
'raw': {'text': 'Internal Impedance (1kHz AC typical) 6mΩ',
'page': 1,
'confidence': 0.8}},
'mass': {'value': 76.0,
'unit': 'g',
'raw': {'text': 'Cell Weight 76g', 'page': 1, 'confidence': 0.8}},
'diameter': {'value': 26.0,
'unit': 'mm',
'raw': {'text': 'Cell Dimensions 26 x 65 mm', 'page': 1, 'confidence': 0.8}},
'height': {'value': 65.0,
'unit': 'mm',
'raw': {'text': 'Cell Dimensions 26 x 65 mm', 'page': 1, 'confidence': 0.8}},
'pulse_charging_current': {'value': 10.0,
'unit': 'A',
'raw': {'text': 'Recommended Fast Charge Method to 80% SOC 10A to 3.6V CC, 12 min',
'page': 1,
'confidence': 0.8}},
'nominal_continuous_charging_current': {'value': 2.5,
'unit': 'A',
'raw': {'text': 'Recommended Standard Charge Method 2.5A to 3.6V CCCV, 60 min',
'page': 1,
'confidence': 0.8}},
'maximum_continuous_charging_current': {'value': 2.5,
'unit': 'A',
'raw': {'text': 'Recommended Standard Charge Method 2.5A to 3.6V CCCV, 60 min',
'page': 1,
'confidence': 0.8}},
'maximum_continuous_discharging_current': {'value': 50.0,
'unit': 'A',
'raw': {'text': 'Maximum Continuous Discharge 50A',
'page': 1,
'confidence': 0.8}},
'minimum_discharging_temperature': {'value': -30,
'unit': '°C',
'raw': {'text': 'Operating Temperature -30°C to 55°C',
'page': 1,
'confidence': 0.8}},
'maximum_discharging_temperature': {'value': 55,
'unit': '°C',
'raw': {'text': 'Operating Temperature -30°C to 55°C',
'page': 1,
'confidence': 0.8}},
'minimum_storage_temperature': {'value': -40,
'unit': '°C',
'raw': {'text': 'Storage Temperature -40°C to 60°C',
'page': 1,
'confidence': 0.8}},
'maximum_storage_temperature': {'value': 60,
'unit': '°C',
'raw': {'text': 'Storage Temperature -40°C to 60°C',
'page': 1,
'confidence': 0.8}},
'cycle_life': {'value_text': '>1000',
'unit': 'count',
'raw': {'text': 'Cycle Life at 20A Discharge, 100% DOD >1,000 cycles',
'page': 1,
'confidence': 0.7}}}
Record types#
CellSpec — the specification#
A cell spec is a product specification: datasheet-level information about a battery model, not a physical item.
In RDF it carries two parallel type assertions:
EMMO:
BatteryCellSpecification(subclass ofemmo:Description— an information entity describing a class of cells)schema.org:
schema:CreativeWork(the specification document as a creative artifact)
The physical EMMO classes for format and chemistry (BatteryCell, CylindricalBattery, LithiumIonBattery, …) are placed on an isDescriptionFor anonymous node — not on the specification itself — because the specification is an information entity, not a physical cell.
Cell — the physical item#
A cell instance is a specific physical cell with a serial number. It always links to a cell spec.
In RDF it carries both BatteryCell (EMMO scientific type) and schema:IndividualProduct (schema.org type for a uniquely identifiable product item). The link back to the cell-spec record uses both hasDescription (EMMO) and schema:isVariantOf (schema.org) for full dual-vocabulary alignment.
TestSpec and Test#
A test spec is a reusable procedure definition. A test is one execution of a test spec on one cell instance. In RDF: BatteryTest.
Dataset#
A dataset links measured data files to the cell instance and test that produced them. In RDF: schema:Dataset.
Identifiers and IRIs#
[5]:
# Every BattINFO record has a stable, opaque IRI
iri = a123["cell_spec"]["id"]
print("IRI:", iri)
# The 16-character Crockford Base32 UID
uid = iri.split("/")[-1]
print("UID:", uid)
IRI: https://w3id.org/battinfo/spec/7d9k-2m4p-8t3x-6nq5
UID: 7d9k-2m4p-8t3x-6nq5
IRIs are:
Opaque — carry no embedded meaning
Stable — never change once minted
Deterministic — the same record inputs always produce the same IRI
This makes them safe to cite in papers, reference in databases, and dereference over HTTP.
The semantic layer — JSON-LD#
[6]:
from battinfo import CellSpec, publish
cell_spec = CellSpec(
manufacturer="A123 Systems",
model="ANR26650M1-B",
format="cylindrical",
chemistry="Li-ion",
positive_electrode_basis="LFP",
negative_electrode_basis="graphite",
properties={
"nominal_capacity": {"value": 2.5, "unit": "Ah"},
"nominal_voltage": {"value": 3.3, "unit": "V"},
},
)
result = publish(cell_spec, destination="local", root=SCRATCH)
print("IRI:", result.canonical_iri)
IRI: https://w3id.org/battinfo/spec/e4t5-w9re-mpq6-8g7e
[7]:
from battinfo.api import publish_record
output = publish_record(
result.debug_paths["canonical_record_path"],
target_root=SCRATCH / "resolver",
)
jsonld = json.loads(
Path(output["output_dir"], "index.jsonld").read_text(encoding="utf-8")
)
# EMMO @type stacking from format + chemistry + electrode basis
print("@type:", jsonld["@type"])
@type: ['BatteryCellSpecification', 'schema:CreativeWork']
[8]:
# Each quantitative spec becomes a ConventionalProperty node
print("Properties:")
for prop in jsonld.get("hasProperty", []):
class_name = prop["@type"][0] if isinstance(prop["@type"], list) else prop["@type"]
value = prop.get("hasNumericalPart", {}).get("hasNumberValue")
unit = prop.get("hasMeasurementUnit", "").split("#")[-1]
print(f" {class_name}: {value} [{unit}]")
Properties:
NominalCapacity: 2.5 [AmpereHour]
NominalVoltage: 3.3 [Volt]
Exported JSON-LD file#
publish_record() writes index.jsonld, index.json, and index.html to the resolver artifact directory. The JSON-LD file below is the one that would be served when a consumer dereferences the cell-spec IRI.
[9]:
jsonld_file = Path(output["output_dir"]) / "index.jsonld"
print(f"JSON-LD exported to: {jsonld_file}")
print()
print(json.dumps(jsonld, indent=2))
JSON-LD exported to: <repo>\docs\guides\_scratch\guide-01\resolver\spec\e4t5-w9re-mpq6-8g7e\index.jsonld
{
"@context": [
"https://w3id.org/emmo/domain/battery/context",
{
"schema": "https://schema.org/",
"csvw": "http://www.w3.org/ns/csvw#",
"battinfo": "https://w3id.org/battinfo/",
"skos": "http://www.w3.org/2004/02/skos/core#"
}
],
"@type": [
"BatteryCellSpecification",
"schema:CreativeWork"
],
"@id": "https://w3id.org/battinfo/spec/e4t5-w9re-mpq6-8g7e",
"schema:identifier": "e4t5-w9re-mpq6-8g7e",
"schema:name": "A123 Systems ANR26650M1-B",
"schema:model": "ANR26650M1-B",
"schema:manufacturer": {
"@type": "schema:Organization",
"schema:name": "A123 Systems"
},
"schema:url": "https://www.battery-genome.org/registry/spec/e4t5-w9re-mpq6-8g7e",
"isDescriptionFor": {
"@type": [
"BatteryCell",
"CylindricalBattery",
"LithiumIonBattery",
"LithiumIonIronPhosphateBattery",
"LithiumIonGraphiteBattery"
],
"skos:prefLabel": "A123 Systems ANR26650M1-B"
},
"schema:schemaVersion": "0.2.0",
"hasProperty": [
{
"@type": [
"NominalCapacity",
"ConventionalProperty"
],
"skos:prefLabel": "NominalCapacity",
"hasNumericalPart": {
"@type": "RealData",
"hasNumberValue": 2.5
},
"hasMeasurementUnit": "https://w3id.org/emmo#AmpereHour"
},
{
"@type": [
"NominalVoltage",
"ConventionalProperty"
],
"skos:prefLabel": "NominalVoltage",
"hasNumericalPart": {
"@type": "RealData",
"hasNumberValue": 3.3
},
"hasMeasurementUnit": "https://w3id.org/emmo#Volt"
}
],
"dcterms:source": {
"@type": "prov:Entity",
"dcterms:type": "datasheet",
"prov:generatedAtTime": "2026-07-08T22:01:57+00:00"
}
}
Validation layers#
Layer |
What it checks |
|---|---|
JSON Schema |
Field names, types, required fields, value constraints |
Pydantic |
Python-level type coercion and cross-field rules |
JSON-LD |
RDF parsability, URDNA2015 normalisability, no undefined type terms |
[10]:
from battinfo.validate.record import validate_record_report
record_path = Path(result.debug_paths["canonical_record_path"])
record_doc = json.loads(record_path.read_text(encoding="utf-8"))
report = validate_record_report(
record_doc,
source_root=SCRATCH,
)
print("ok:", report.ok, "| errors:", len(report.errors), "| warnings:", len(report.warnings))
ok: True | errors: 0 | warnings: 0
Next#
Guide 2 — Describing a cell: author and publish a cell spec
Guide 3 — Linked records: build the complete provenance chain and publish