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 of emmo: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#