Guide 5 — Cell descriptors#
Cell descriptors go beyond datasheet-level specifications. They capture electrode composition, electrolyte formulation, separator properties, and cell construction — the detail needed for research-grade records and curated cell libraries.
Estimated time: 40 minutes Prerequisites: Guide 2 — Describing a cell
Everything writes under _scratch/guide-05/.
[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-05").resolve()
SCRATCH.mkdir(parents=True, exist_ok=True)
Step 1: Individual materials#
[2]:
from battinfo.authoring import material, properties
# Positive electrode
lfp = material("LFP", mass_fraction={"value": 90, "unit": "%"},
comment="LiFePO4, olivine structure, D50 ~ 3 µm (Aleees GEN2)")
pvdf = material("PVDF", mass_fraction={"value": 5, "unit": "%"})
c65 = material("Carbon black", mass_fraction={"value": 5, "unit": "%"},
comment="Imerys C65")
# Negative electrode
graphite = material("Graphite", mass_fraction={"value": 95.5, "unit": "%"},
comment="Natural graphite, D50 ~ 20 µm")
cmc = material("CMC", mass_fraction={"value": 1.5, "unit": "%"})
sbr = material("SBR", mass_fraction={"value": 2.5, "unit": "%"})
c_ng = material("Carbon black", mass_fraction={"value": 0.5, "unit": "%"})
print("Materials defined.")
Materials defined.
Step 2: Bills of materials#
[3]:
from battinfo.authoring import bom
positive_bom = bom(active_material=lfp, binder=pvdf, additive=c65)
negative_bom = bom(active_material=graphite, binder=[cmc, sbr], additive=c_ng)
print("Positive active:", positive_bom.active_material[0].name)
print("Negative binders:", [m.name for m in negative_bom.binder])
Positive active: LFP
Negative binders: ['CMC', 'SBR']
Step 3: Electrodes#
[4]:
from battinfo.authoring import electrode
positive_electrode = electrode(
bom=positive_bom,
loading={"value": 12.5, "unit": "mg/cm2"},
calendered_density={"value": 2.4, "unit": "g/cm3"},
current_collector="Aluminium foil",
current_collector_thickness={"value": 16.0, "unit": "µm"},
coating_comment="NMP-cast, roll-calendered to 85% theoretical density",
)
negative_electrode = electrode(
bom=negative_bom,
loading={"value": 7.8, "unit": "mg/cm2"},
calendered_density={"value": 1.55, "unit": "g/cm3"},
current_collector="Copper foil",
current_collector_thickness={"value": 10.0, "unit": "µm"},
)
print("Positive loading:", positive_electrode.coating.property.get("loading"))
print("Negative density:", negative_electrode.coating.property.get("calendered_density"))
Positive loading: {'value': 12.5, 'unit': 'mg/cm2'}
Negative density: {'value': 1.55, 'unit': 'g/cm3'}
Step 4: Electrolyte#
[5]:
from battinfo.authoring import electrolyte_recipe, material as mat
# Salts are materials like any other constituent: the same material(...) form
# as solvents, with the concentration carried inline.
lp30 = electrolyte_recipe(
family="organic",
salt=mat("LiPF6", concentration={"value": 1.0, "unit": "mol/L"}),
solvents=[
mat("EC", volume_fraction={"value": 50, "unit": "%"}),
mat("DMC", volume_fraction={"value": 50, "unit": "%"}),
],
additives=[
mat("VC", mass_fraction={"value": 2.0, "unit": "%"}, comment="SEI-forming additive"),
mat("FEC", mass_fraction={"value": 1.0, "unit": "%"}, comment="Fluoroethylene carbonate"),
],
comment="LP30 + 2% VC + 1% FEC",
)
print("Salt:", lp30.salt.name, "at", lp30.salt.property.get("concentration"))
print("Solvents:", [s.name for s in lp30.solvent_mixture.component])
print("Additives:", [a.name for a in lp30.additive])
Salt: LiPF6 at {'value': 1.0, 'unit': 'mol/L'}
Solvents: ['EC', 'DMC']
Additives: ['VC', 'FEC']
Step 5: Separator#
[6]:
from battinfo.authoring import 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",
)
print("Material:", sep.material)
print("Thickness:", sep.property.get("thickness"))
print("Porosity:", sep.property.get("porosity"))
Material: Polypropylene
Thickness: {'value': 25.0, 'unit': 'µm'}
Porosity: {'value': 41, 'unit': '%'}
Step 6: Construction#
[7]:
from battinfo.authoring import construction
build = construction(
assembly_type="wound",
layering="single_layer",
layer_count=1,
comment="Single jelly-roll, wound on 4 mm mandrel",
)
print("Assembly:", build.assembly_type, "/", build.layering)
Assembly: wound / single_layer
Step 7: Provenance#
[8]:
from battinfo.authoring import source
prov = source(
type="datasheet",
name="ANR26650M1-B Product Datasheet Rev B",
url="https://example.com/a123-anr26650m1-b.pdf",
retrieved_at=1746230400,
curated_by="Jane Smith",
)
print("Source type:", prov.type)
print("Curated by:", prov.curated_by)
Source type: datasheet
Curated by: Jane Smith
Step 8: Cell description#
[9]:
from battinfo.authoring import cell_description
# Use the known IRI for the A123 ANR26650M1-B example cell spec.
# For new records: publish a CellSpec first to get the IRI, then use it here.
A123_IRI = "https://w3id.org/battinfo/spec/7d9k-2m4p-8t3x-6nq5"
spec = cell_description(
id=A123_IRI,
manufacturer="A123 Systems",
model="ANR26650M1-B",
format="cylindrical",
chemistry="Li-ion",
positive_electrode_basis="LFP",
negative_electrode_basis="graphite",
size_code="R26650",
positive_electrode=positive_electrode,
negative_electrode=negative_electrode,
electrolyte=lp30,
separator=sep,
construction=build,
source=prov,
comment=[
"Descriptor curated from manufacturer datasheet and published literature.",
"Electrode loadings estimated from capacity balance and assumed density.",
],
)
print("Cell ID:", spec.id)
print("Model:", spec.model)
Cell ID: https://w3id.org/battinfo/spec/7d9k-2m4p-8t3x-6nq5
Model: ANR26650M1-B
Step 9: Materials as first-class records#
Every material embedded above can also stand alone. extract_material_specs walks the descriptor — electrode coatings, electrolyte, separator — and lifts each distinct material into a standalone material-spec record with a deterministic IRI (the same material always mints the same IRI, so specs deduplicate across cells). Components then reference their spec via material_spec_id — the same spec-and-instance pattern cells use:
[10]:
from battinfo.materials import extract_material_specs
material_specs = extract_material_specs(spec.model_dump())
print(f"{len(material_specs)} standalone material spec(s):")
for record in material_specs:
m = record["material_spec"]
print(f" {m['name']:14s} {m.get('material_class', '?'):22s} {m['id']}")
14 standalone material spec(s):
LFP active_material https://w3id.org/battinfo/spec/s6y8-5mne-94gx-e5ve
PVDF binder https://w3id.org/battinfo/spec/fm9p-sqkk-tbx3-rr66
Carbon black conductive_additive https://w3id.org/battinfo/spec/xz03-r714-wpa0-bwpb
Aluminium foil current_collector https://w3id.org/battinfo/spec/92ny-p3qm-6j9n-vrym
Graphite active_material https://w3id.org/battinfo/spec/sm7b-n4y5-2hrk-dv6h
CMC binder https://w3id.org/battinfo/spec/t4wz-ff8s-6vp6-af48
SBR binder https://w3id.org/battinfo/spec/7p3d-2e22-7yae-spyb
Copper foil current_collector https://w3id.org/battinfo/spec/ftdw-5z5h-mbn7-x13m
LiPF6 electrolyte_salt https://w3id.org/battinfo/spec/xcv1-hpy1-b0bw-z5s2
EC electrolyte_solvent https://w3id.org/battinfo/spec/z6sr-a9yw-gryf-3q55
DMC electrolyte_solvent https://w3id.org/battinfo/spec/va82-gnpn-v0bt-3qed
VC electrolyte_additive https://w3id.org/battinfo/spec/nbz6-pn8h-4458-dh91
FEC electrolyte_additive https://w3id.org/battinfo/spec/t59c-yz22-48as-tdkx
Polypropylene separator_material https://w3id.org/battinfo/spec/n24p-ymxz-gj1j-q6pb
Step 10: Serialise the descriptor#
[11]:
# Write the descriptor to disk for inspection and downstream use.
desc_dir = SCRATCH / "descriptors"
desc_dir.mkdir(parents=True, exist_ok=True)
desc_path = desc_dir / "a123-anr26650m1-b.descriptor.json"
spec_dict = {"schema_version": "1.0.0", "specification": spec.to_json()}
desc_path.write_text(json.dumps(spec_dict, indent=2, ensure_ascii=False), encoding="utf-8")
print("Written:", desc_path)
print("Top-level keys:", list(spec_dict.keys())[:8])
Written: <repo>\docs\guides\_scratch\guide-05\descriptors\a123-anr26650m1-b.descriptor.json
Top-level keys: ['schema_version', 'specification']
Step 11: Map to domain-battery JSON-LD#
[12]:
from battinfo.transform.json_to_jsonld import to_jsonld
descriptor_doc = json.loads(desc_path.read_text(encoding="utf-8"))
desc_jsonld = to_jsonld(descriptor_doc, target="domain-battery")
battery = desc_jsonld["@graph"][0]
print("@type:", battery["@type"])
print()
pos = battery.get("hasPositiveElectrode", {})
print("Positive electrode @type:", pos.get("@type"))
neg = battery.get("hasNegativeElectrode", {})
print("Negative electrode @type:", neg.get("@type"))
@type: ['BatteryCellSpecification', 'schema:CreativeWork']
Positive electrode @type: LithiumIronPhosphateElectrode
Negative electrode @type: GraphiteElectrode
[13]:
# Export the domain-battery JSON-LD to disk
desc_jsonld_path = SCRATCH / "descriptors" / "a123-anr26650m1-b.domain-battery.jsonld"
desc_jsonld_path.write_text(
json.dumps(desc_jsonld, indent=2, ensure_ascii=False),
encoding="utf-8",
)
print(f"Domain-battery JSON-LD exported to: {desc_jsonld_path}")
print()
print(json.dumps(desc_jsonld, indent=2))
Domain-battery JSON-LD exported to: <repo>\docs\guides\_scratch\guide-05\descriptors\a123-anr26650m1-b.domain-battery.jsonld
{
"@context": [
"https://w3id.org/emmo/domain/battery/context",
{
"schema": "https://schema.org/",
"dcterms": "http://purl.org/dc/terms/"
}
],
"@graph": [
{
"@type": [
"BatteryCellSpecification",
"schema:CreativeWork"
],
"isDescriptionFor": {
"@type": [
"BatteryCell",
"CylindricalBattery",
"LithiumIonBattery",
"LithiumIonIronPhosphateBattery",
"LithiumIonGraphiteBattery"
]
},
"@id": "https://w3id.org/battinfo/spec/7d9k-2m4p-8t3x-6nq5",
"schema:name": "A123 Systems ANR26650M1-B",
"schema:model": "ANR26650M1-B",
"schema:manufacturer": {
"@type": "schema:Organization",
"schema:name": "A123 Systems"
},
"schema:size": "R26650",
"hasPositiveElectrode": {
"hasCoating": {
"@type": "ElectrodeCoating",
"hasActiveMaterial": {
"@type": [
"LithiumIronPhosphate",
"ActiveMaterial"
],
"schema:name": "LFP",
"hasProperty": {
"@type": [
"MassFraction",
"ConventionalProperty"
],
"hasNumericalPart": {
"@type": "RealData",
"hasNumberValue": 0.9
},
"hasMeasurementUnit": "https://w3id.org/emmo#EMMO_5ebd5e01_0ed3_49a2_a30d_cd05cbe72978"
},
"schema:description": "LiFePO4, olivine structure, D50 ~ 3 \u00b5m (Aleees GEN2)"
},
"hasBinder": {
"@type": [
"PolyvinylideneFluoride",
"Binder"
],
"schema:name": "PVDF",
"hasProperty": {
"@type": [
"MassFraction",
"ConventionalProperty"
],
"hasNumericalPart": {
"@type": "RealData",
"hasNumberValue": 0.05
},
"hasMeasurementUnit": "https://w3id.org/emmo#EMMO_5ebd5e01_0ed3_49a2_a30d_cd05cbe72978"
}
},
"hasConductiveAdditive": {
"@type": [
"CarbonBlack",
"ConductiveAdditive"
],
"schema:name": "Carbon black",
"hasProperty": {
"@type": [
"MassFraction",
"ConventionalProperty"
],
"hasNumericalPart": {
"@type": "RealData",
"hasNumberValue": 0.05
},
"hasMeasurementUnit": "https://w3id.org/emmo#EMMO_5ebd5e01_0ed3_49a2_a30d_cd05cbe72978"
},
"schema:description": "Imerys C65"
},
"hasProperty": [
{
"@type": [
"CalenderedDensity",
"ConventionalProperty"
],
"skos:prefLabel": "CalenderedDensity",
"hasNumericalPart": {
"@type": "RealData",
"hasNumberValue": 2.4
},
"hasMeasurementUnit": "https://w3id.org/emmo#GramPerCubicCentiMetre"
},
{
"@type": [
"ActiveMassLoading",
"ConventionalProperty"
],
"skos:prefLabel": "ActiveMassLoading",
"hasNumericalPart": {
"@type": "RealData",
"hasNumberValue": 12.5
},
"hasMeasurementUnit": "https://w3id.org/emmo#MilliGramPerSquareCentiMetre"
}
],
"schema:description": "NMP-cast, roll-calendered to 85% theoretical density"
},
"hasCurrentCollector": {
"@type": [
"CurrentCollector",
"Aluminium",
"Foil"
],
"schema:name": "Aluminium foil",
"hasProperty": {
"@type": [
"Thickness",
"ConventionalProperty"
],
"skos:prefLabel": "Thickness",
"hasNumericalPart": {
"@type": "RealData",
"hasNumberValue": 16.0
},
"hasMeasurementUnit": "https://w3id.org/emmo#MicroMetre"
}
},
"@type": "LithiumIronPhosphateElectrode"
},
"hasNegativeElectrode": {
"hasCoating": {
"@type": "ElectrodeCoating",
"hasActiveMaterial": {
"@type": [
"Graphite",
"ActiveMaterial"
],
"schema:name": "Graphite",
"hasProperty": {
"@type": [
"MassFraction",
"ConventionalProperty"
],
"hasNumericalPart": {
"@type": "RealData",
"hasNumberValue": 0.955
},
"hasMeasurementUnit": "https://w3id.org/emmo#EMMO_5ebd5e01_0ed3_49a2_a30d_cd05cbe72978"
},
"schema:description": "Natural graphite, D50 ~ 20 \u00b5m"
},
"hasBinder": [
{
"@type": [
"CarboxymethylCellulose",
"Binder"
],
"schema:name": "CMC",
"hasProperty": {
"@type": [
"MassFraction",
"ConventionalProperty"
],
"hasNumericalPart": {
"@type": "RealData",
"hasNumberValue": 0.015
},
"hasMeasurementUnit": "https://w3id.org/emmo#EMMO_5ebd5e01_0ed3_49a2_a30d_cd05cbe72978"
}
},
{
"@type": [
"StyreneButadiene",
"Binder"
],
"schema:name": "SBR",
"hasProperty": {
"@type": [
"MassFraction",
"ConventionalProperty"
],
"hasNumericalPart": {
"@type": "RealData",
"hasNumberValue": 0.025
},
"hasMeasurementUnit": "https://w3id.org/emmo#EMMO_5ebd5e01_0ed3_49a2_a30d_cd05cbe72978"
}
}
],
"hasConductiveAdditive": {
"@type": [
"CarbonBlack",
"ConductiveAdditive"
],
"schema:name": "Carbon black",
"hasProperty": {
"@type": [
"MassFraction",
"ConventionalProperty"
],
"hasNumericalPart": {
"@type": "RealData",
"hasNumberValue": 0.005
},
"hasMeasurementUnit": "https://w3id.org/emmo#EMMO_5ebd5e01_0ed3_49a2_a30d_cd05cbe72978"
}
},
"hasProperty": [
{
"@type": [
"CalenderedDensity",
"ConventionalProperty"
],
"skos:prefLabel": "CalenderedDensity",
"hasNumericalPart": {
"@type": "RealData",
"hasNumberValue": 1.55
},
"hasMeasurementUnit": "https://w3id.org/emmo#GramPerCubicCentiMetre"
},
{
"@type": [
"ActiveMassLoading",
"ConventionalProperty"
],
"skos:prefLabel": "ActiveMassLoading",
"hasNumericalPart": {
"@type": "RealData",
"hasNumberValue": 7.8
},
"hasMeasurementUnit": "https://w3id.org/emmo#MilliGramPerSquareCentiMetre"
}
]
},
"hasCurrentCollector": {
"@type": [
"CurrentCollector",
"Copper",
"Foil"
],
"schema:name": "Copper foil",
"hasProperty": {
"@type": [
"Thickness",
"ConventionalProperty"
],
"skos:prefLabel": "Thickness",
"hasNumericalPart": {
"@type": "RealData",
"hasNumberValue": 10.0
},
"hasMeasurementUnit": "https://w3id.org/emmo#MicroMetre"
}
},
"@type": "GraphiteElectrode"
},
"hasElectrolyte": {
"@type": "OrganicElectrolyte",
"hasSolute": {
"@type": [
"LithiumHexafluorophosphate",
"Solute"
],
"schema:name": "LiPF6",
"hasProperty": {
"@type": [
"AmountConcentration",
"ConventionalProperty"
],
"skos:prefLabel": "AmountConcentration",
"hasNumericalPart": {
"@type": "RealData",
"hasNumberValue": 1.0
},
"hasMeasurementUnit": "https://w3id.org/emmo#MolePerLitre"
}
},
"hasSolvent": [
{
"@type": [
"EthyleneCarbonate",
"Solvent"
],
"schema:name": "EC",
"hasProperty": {
"@type": [
"VolumeFraction",
"ConventionalProperty"
],
"hasNumericalPart": {
"@type": "RealData",
"hasNumberValue": 0.5
},
"hasMeasurementUnit": "https://w3id.org/emmo#EMMO_5ebd5e01_0ed3_49a2_a30d_cd05cbe72978"
}
},
{
"@type": [
"DimethylCarbonate",
"Solvent"
],
"schema:name": "DMC",
"hasProperty": {
"@type": [
"VolumeFraction",
"ConventionalProperty"
],
"hasNumericalPart": {
"@type": "RealData",
"hasNumberValue": 0.5
},
"hasMeasurementUnit": "https://w3id.org/emmo#EMMO_5ebd5e01_0ed3_49a2_a30d_cd05cbe72978"
}
}
],
"hasAdditive": [
{
"@type": [
"VinyleneCarbonate",
"ElectrolyteAdditive"
],
"schema:name": "VC",
"hasProperty": {
"@type": [
"MassFraction",
"ConventionalProperty"
],
"hasNumericalPart": {
"@type": "RealData",
"hasNumberValue": 0.02
},
"hasMeasurementUnit": "https://w3id.org/emmo#EMMO_5ebd5e01_0ed3_49a2_a30d_cd05cbe72978"
},
"schema:description": "SEI-forming additive"
},
{
"@type": [
"FluoroethyleneCarbonate",
"ElectrolyteAdditive"
],
"schema:name": "FEC",
"hasProperty": {
"@type": [
"MassFraction",
"ConventionalProperty"
],
"hasNumericalPart": {
"@type": "RealData",
"hasNumberValue": 0.01
},
"hasMeasurementUnit": "https://w3id.org/emmo#EMMO_5ebd5e01_0ed3_49a2_a30d_cd05cbe72978"
},
"schema:description": "Fluoroethylene carbonate"
}
],
"schema:description": "LP30 + 2% VC + 1% FEC"
},
"hasSeparator": {
"@type": [
"Polypropylene",
"Separator"
],
"schema:name": "Polypropylene",
"hasProperty": [
{
"@type": [
"Porosity",
"ConventionalProperty"
],
"hasNumericalPart": {
"@type": "RealData",
"hasNumberValue": 0.41
},
"hasMeasurementUnit": "https://w3id.org/emmo#EMMO_5ebd5e01_0ed3_49a2_a30d_cd05cbe72978"
},
{
"@type": [
"Thickness",
"ConventionalProperty"
],
"skos:prefLabel": "Thickness",
"hasNumericalPart": {
"@type": "RealData",
"hasNumberValue": 25.0
},
"hasMeasurementUnit": "https://w3id.org/emmo#MicroMetre"
}
],
"schema:description": "Celgard 2500, single-layer PP"
},
"schema:additionalProperty": [
{
"@type": "schema:PropertyValue",
"schema:propertyID": "construction.assembly_type",
"schema:name": "Assembly Type",
"schema:value": "wound"
},
{
"@type": "schema:PropertyValue",
"schema:propertyID": "construction.layering",
"schema:name": "Layering",
"schema:value": "single_layer"
},
{
"@type": "schema:PropertyValue",
"schema:propertyID": "construction.layer_count",
"schema:name": "Layer Count",
"schema:value": 1
},
{
"@type": "schema:PropertyValue",
"schema:propertyID": "construction.comment",
"schema:name": "Construction Comment",
"schema:value": "Single jelly-roll, wound on 4 mm mandrel"
}
],
"schema:description": [
"Descriptor curated from manufacturer datasheet and published literature.",
"Electrode loadings estimated from capacity balance and assumed density."
],
"schema:schemaVersion": "1.0.0"
}
]
}
Step 12: Validate the descriptor JSON-LD#
[14]:
from battinfo.validate.jsonld import validate_jsonld_report
# Validate the domain-battery JSON-LD produced from the descriptor
report = validate_jsonld_report(desc_jsonld)
print("ok:", report.ok, "| errors:", len(report.errors), "| warnings:", len(report.warnings))
for issue in report.issues:
print(f" [{issue.severity}] {issue.code}: {issue.message}")
ok: True | errors: 0 | warnings: 0
Step 13: Publish#
The descriptor is the cell-spec record — publish it directly and the composition travels with it (note the positive_electrode key in the saved record, and that the IRI set in Step 8 is preserved):
[15]:
from battinfo import publish
from battinfo.api import publish_record
pub = publish(spec, destination="local", root=SCRATCH / "published")
record = json.loads(Path(pub.debug_paths["canonical_record_path"]).read_text(encoding="utf-8"))
print("IRI: ", pub.canonical_iri)
print("Record keys: ", list(record.keys()))
IRI: https://w3id.org/battinfo/spec/7d9k-2m4p-8t3x-6nq5
Record keys: ['schema_version', 'cell_spec', 'properties', 'provenance', 'notes', 'construction', 'positive_electrode', 'negative_electrode', 'electrolyte', 'separator']
[16]:
output = publish_record(
pub.debug_paths["canonical_record_path"],
target_root=SCRATCH / "resolver",
)
jsonld_out = json.loads(
Path(output["output_dir"], "index.jsonld").read_text(encoding="utf-8")
)
print("@type:", jsonld_out["@type"])
@type: ['BatteryCellSpecification', 'schema:CreativeWork']
Next#
Return to Guide 3 — Linked records to connect this descriptor cell spec to instances, tests, and datasets.