Guide 2 — Describing a cell#
A cell spec is the product datasheet, as data. This guide authors one at datasheet level and publishes it, then gives a taste of the material-level depth BattINFO can carry — Guide 5 goes all the way down.
Estimated time: 15 minutes Prerequisites: Guide 1 — Concepts
Everything this guide writes lands in _scratch/guide-02/.
[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-02").resolve()
SCRATCH.mkdir(parents=True, exist_ok=True)
Part 1 — The datasheet path#
Use this when you have a datasheet and want to describe a commercial cell quickly.
[2]:
from battinfo import CellSpec, publish
cell_spec = CellSpec(
manufacturer="Samsung SDI",
model="INR21700-50E",
format="cylindrical",
chemistry="Li-ion",
positive_electrode_basis="NMC",
negative_electrode_basis="graphite",
size_code="R21700",
source={
"type": "datasheet",
"name": "INR21700-50E Product Specification Rev 1.0",
"url": "https://example.com/samsung-inr21700-50e.pdf",
},
properties={
"nominal_capacity": {"value": 5.0, "unit": "Ah"},
"nominal_voltage": {"value": 3.6, "unit": "V"},
"rated_energy": {"value": 18.0, "unit": "Wh"},
"mass": {"value": 68.0, "unit": "g"},
"height": {"value": 70.15, "unit": "mm"},
"diameter": {"value": 21.0, "unit": "mm"},
"maximum_continuous_charging_current": {"value": 4.875, "unit": "A"},
"maximum_continuous_discharging_current":{"value": 9.8, "unit": "A"},
"maximum_charging_temperature": {"value": 45, "unit": "°C"},
"minimum_discharging_temperature": {"value": -20, "unit": "°C"},
"cycle_life": {"value_text": ">500", "unit": "count"},
},
)
result = publish(cell_spec, destination="local", root=SCRATCH)
print("IRI:", result.canonical_iri)
IRI: https://w3id.org/battinfo/spec/03yx-y1kk-q3mq-ck3w
<repo>\src\battinfo\transform\cell_spec_node.py:337: UserWarning: semantic.value_text_only: 'cycle_life' on https://w3id.org/battinfo/spec/03yx-y1kk-q3mq-ck3w carries only value_text - the JSON-LD export emits no numeric quantity value for it.
property_nodes = cell_spec_property_nodes(
<repo>\src\battinfo\transform\cell_spec_node.py:337: UserWarning: semantic.value_text_only: 'cycle_life' on https://w3id.org/battinfo/spec/03yx-y1kk-q3mq-ck3w carries only value_text - the JSON-LD export emits no numeric quantity value for it.
property_nodes = cell_spec_property_nodes(
[3]:
# Inspect the canonical record
record = json.loads(Path(result.debug_paths["canonical_record_path"]).read_text(encoding="utf-8"))
print(json.dumps(record, indent=2))
{
"schema_version": "0.2.0",
"cell_spec": {
"id": "https://w3id.org/battinfo/spec/03yx-y1kk-q3mq-ck3w",
"short_id": "03yxy1",
"identifier": "cell-spec:03yx-y1kk-q3mq-ck3w",
"name": "Samsung SDI INR21700-50E",
"model": "INR21700-50E",
"manufacturer": {
"type": "Organization",
"name": "Samsung SDI"
},
"cell_format": "cylindrical",
"chemistry": "Li-ion",
"positive_electrode_basis": "NMC",
"negative_electrode_basis": "graphite",
"size_code": "R21700"
},
"properties": {
"nominal_capacity": {
"value": 5.0,
"unit": "Ah"
},
"nominal_voltage": {
"value": 3.6,
"unit": "V"
},
"rated_energy": {
"value": 18.0,
"unit": "Wh"
},
"mass": {
"value": 68.0,
"unit": "g"
},
"height": {
"value": 70.15,
"unit": "mm"
},
"diameter": {
"value": 21.0,
"unit": "mm"
},
"maximum_continuous_charging_current": {
"value": 4.875,
"unit": "A"
},
"maximum_continuous_discharging_current": {
"value": 9.8,
"unit": "A"
},
"maximum_charging_temperature": {
"value": 45,
"unit": "\u00b0C"
},
"minimum_discharging_temperature": {
"value": -20,
"unit": "\u00b0C"
},
"cycle_life": {
"value_text": ">500",
"unit": "count"
}
},
"provenance": {
"source_type": "datasheet",
"source_name": "INR21700-50E Product Specification Rev 1.0",
"source_url": "https://example.com/samsung-inr21700-50e.pdf",
"retrieved_at": 1783548126,
"battinfo_version": "0.7.0"
}
}
[4]:
# Produce the resolver JSON-LD
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"))
print("@type:", jsonld["@type"])
print()
print("Properties:")
for prop in jsonld.get("hasProperty", []):
t = prop["@type"][0] if isinstance(prop["@type"], list) else prop["@type"]
v = prop.get("hasNumericalPart", {}).get("hasNumberValue", "—")
u = prop.get("hasMeasurementUnit", "").split("#")[-1]
print(f" {t}: {v} [{u}]")
<repo>\src\battinfo\transform\cell_spec_node.py:337: UserWarning: semantic.value_text_only: 'cycle_life' on https://w3id.org/battinfo/spec/03yx-y1kk-q3mq-ck3w carries only value_text - the JSON-LD export emits no numeric quantity value for it.
property_nodes = cell_spec_property_nodes(
<repo>\src\battinfo\transform\cell_spec_node.py:337: UserWarning: semantic.value_text_only: 'cycle_life' on https://w3id.org/battinfo/spec/03yx-y1kk-q3mq-ck3w carries only value_text - the JSON-LD export emits no numeric quantity value for it.
property_nodes = cell_spec_property_nodes(
@type: ['BatteryCellSpecification', 'schema:CreativeWork']
Properties:
NominalCapacity: 5.0 [AmpereHour]
NominalVoltage: 3.6 [Volt]
NominalEnergy: 18.0 [WattHour]
Mass: 68.0 [Gram]
Height: 70.15 [MilliMetre]
Diameter: 21.0 [MilliMetre]
MaximumContinuousChargingCurrent: 4.875 [Ampere]
MaximumContinuousDischargingCurrent: 9.8 [Ampere]
MaximumChargingTemperature: 45 [EMMO_36a9bf69_483b_42fd_8a0c_7ac9206320bc]
MinimumDischargingTemperature: -20 [EMMO_36a9bf69_483b_42fd_8a0c_7ac9206320bc]
CycleLife: — [EMMO_5ebd5e01_0ed3_49a2_a30d_cd05cbe72978]
Part 2 — A taste of material-level description#
Datasheet fields stop at the can. For research cells you often know what is inside — and BattINFO applies the same spec-and-instance thinking to materials that it applies to cells:
a component (
material(...)) describes a constituent in the context of this cell — its role and its fraction of a coating or a mixture;a material spec is a standalone, reusable record for the substance itself, with its own IRI, shared by every cell that uses it.
Salts are materials like any other constituent — they take the same material(...) form as solvents, with the concentration carried inline:
[5]:
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": "%"}),
],
comment="LP30",
)
print("Salt: ", lp30.salt.name, lp30.salt.property.get("concentration"))
print("Solvents:", [s.name for s in lp30.solvent_mixture.component])
Salt: LiPF6 {'value': 1.0, 'unit': 'mol/L'}
Solvents: ['EC', 'DMC']
Any component can be lifted into a standalone material spec — a record with a deterministic IRI (the same material always mints the same IRI, so specs deduplicate across cells) — and the component then references it:
[6]:
from battinfo.materials import link_component_to_spec, material_spec_from_component
lipf6 = material_spec_from_component(lp30.salt, material_class="electrolyte_salt")
lipf6_id = lipf6["material_spec"]["id"]
print("Material-spec IRI:", lipf6_id)
linked_salt = link_component_to_spec(lp30.salt, lipf6_id)
print("Component reference:", linked_salt["material_spec_id"])
Material-spec IRI: https://w3id.org/battinfo/spec/xcv1-hpy1-b0bw-z5s2
Component reference: https://w3id.org/battinfo/spec/xcv1-hpy1-b0bw-z5s2
That is the whole model — the full bottom-up build (electrode bills of materials, loadings, separator, construction, and the rich JSON-LD graph they produce) is Guide 5 — Cell descriptors.
Next#
Guide 3 — Linked records: record the physical cells, tests, and datasets behind this spec
Guide 5 — Cell descriptors: the complete material-level workflow