Guide 6 — Publish your first dataset#
The tutorial version of the publishing journey: you start with a raw cycler export and finish with validated, linked records, a citable archive, and a place in the Battery Genome registry. Every code cell here executes in CI, against the sample Neware CSV that ships with the repository — swap in your own files and the recipe is identical.
The five stages:
Convert — instrument files → tidy BDF tables
Identify — name the physical cells you tested
Link — attach each test and its data to its cell
Save & validate — canonical records with stable IRIs
Publish — a DOI on Zenodo + the registry review queue
Prerequisites: ``pip install “battinfo[processing]”`` — the ``processing`` extra brings the BDF converter (``batterydf``) that Stage 1 uses. Stage 5 needs credentials and is guarded so the notebook runs fully offline without them.
[1]:
import os
import shutil
from pathlib import Path
# This notebook runs from its own folder (docs/guides); fresh scratch every run.
SCRATCH = Path("_scratch/guide-06").resolve()
if SCRATCH.exists():
shutil.rmtree(SCRATCH)
SCRATCH.mkdir(parents=True)
# Your starting point: a folder with raw cycler exports. Here, the sample
# Neware CSV that ships with the repo (one CC charge + discharge cycle).
shutil.copy(Path("data/neware-sample.csv"), SCRATCH / "neware-sample.csv")
import battinfo
ws = battinfo.workspace(root=SCRATCH)
Stage 1 — Convert#
Every instrument speaks its own format; BDF (Battery Data Format) is the one tidy, documented table they all become. NEWARE .ndax, Biologic .mpt, Excel and MATLAB exports are auto-detected by ws.convert(); CSV exports use an explicit pattern.
[2]:
converted = ws.convert("*.csv")
bdf_file = sorted((SCRATCH / "bdf").glob("*.bdf.csv"))[0]
print()
print("BDF columns:", bdf_file.read_text(encoding="utf-8").splitlines()[0])
neware-sample.csv -> neware-sample.bdf.csv (0.0 MB, via neware_csv)
Converted 1 file(s) -> <repo>\docs\guides\_scratch\guide-06\bdf
BDF columns: test_time_second,voltage_volt,current_ampere,unix_time_second,cycle_count,step_id,step_time_second,step_charging_capacity_ah,step_discharging_capacity_ah
Stage 2 — Identify#
Your measurements are about specific physical cells. Search the registry for the cell product you tested; if you are offline (or the cell is not registered yet), describe it yourself — the same object works either way.
[3]:
hits = ws.search("molicel inr21700 p45b")
if hits:
spec = hits[0] # found in the registry — reuse its identity
else:
# Offline / unregistered: describe the product yourself.
spec = battinfo.CellSpec(
manufacturer="Molicel",
model="INR21700-P45B",
format="cylindrical",
chemistry="Li-ion",
nominal_capacity={"value": 4.5, "unit": "Ah"},
)
cells = ws.add("cell", spec=spec, serial_numbers=["S1"])
No cell-spec match for 'molicel inr21700 p45b'.
Tip: ws.template('cell-spec', manufacturer='...', model='...')
cell: S1 (IRI auto-assigned)
Stage 3 — Link#
Data without its test conditions is trivia. The chain cell → test → dataset is what lets someone who was never in your lab reuse the numbers. ws.add("test", ...) creates the test record and a dataset record pointing at the converted file.
[4]:
ws.add("test", type="cycling", cell="S1", data=str(bdf_file))
test [cycling] on S1 +1 dataset(s) (+1 raw source)
[4]:
[Test(schema_version='0.2.0', kind='Test', id=None, name='S1 cycling', test_type=<BatteryTestType.CYCLING: 'cycling'>, protocol_id=None, cell_instance_id=None, description=None, status='completed', protocol=ProtocolInfo(name='cycling', url=None), instrument=None, equipment_id=None, channel_id=None, started_at=1772442000, ended_at=1772443380, dataset_ids=[], conformance=None, artifacts=[], source=ProvenanceInfo(type='measurement', name=None, file=None, url=None, citation=None, retrieved_at=None, workflow_version=None, file_hash=None, curated_by=None, comment=None, battinfo_version=None), comment=[])]
Stage 4 — Save & validate#
ws.save() validates every record against the canonical JSON Schemas and writes them with stable w3id.org/battinfo/... IRIs, minted deterministically from each record’s identity — re-running an identical save is a no-op, never a duplicate.
[5]:
ws.save()
records_root = SCRATCH / ".battinfo/records"
for record_file in sorted(records_root.rglob("*.json")):
print(f"{record_file.parent.name}/{record_file.name}")
Saved 4 record(s) under <repo>\docs\guides\_scratch\guide-06\.battinfo\records:
cell spec Molicel INR21700-P45B [created] .battinfo\records\cell-spec\cell-spec-fpdr-z1qt-23v0-stzx.json
cell S1 [created] .battinfo\records\cell-instance\cell-d6tp-jgqy-a3s9-4qem.json
test S1 cycling [created] .battinfo\records\test\test-9f4c-mxny-yhym-2zh4.json
dataset S1 data [created] .battinfo\records\dataset\dataset-6p2d-cy0w-25h0-2820.json
Next: ws.list(verbose=True) to inspect, or ws.publish() to publish.
cell-instance/cell-d6tp-jgqy-a3s9-4qem.json
cell-spec/cell-spec-fpdr-z1qt-23v0-stzx.json
dataset/dataset-6p2d-cy0w-25h0-2820.json
test/test-9f4c-mxny-yhym-2zh4.json
[6]:
# Look inside one: the cell record, linked to its spec, stamped with provenance.
import json
cell_record_file = next((records_root / "cell-instance").glob("*.json"))
cell_record = json.loads(cell_record_file.read_text(encoding="utf-8"))
print(json.dumps(cell_record["cell_instance"], indent=2)[:400])
print("...")
print("provenance:", json.dumps(cell_record["provenance"], indent=2))
{
"id": "https://w3id.org/battinfo/cell/d6tp-jgqy-a3s9-4qem",
"short_id": "d6tpjg",
"cell_spec_id": "https://w3id.org/battinfo/spec/fpdr-z1qt-23v0-stzx",
"name": "S1",
"serial_number": "S1"
}
...
provenance: {
"source_type": "measurement",
"retrieved_at": 1783617421,
"battinfo_version": "0.7.0"
}
Stage 5 — Publish#
The payoff: ws.publish() submits the records to the registry’s review queue (staged — a curator promotes them to the public index), and zenodo=True archives the dataset with a citable DOI. Both need credentials, so this cell is a guarded no-op until you provide them:
ws.login(api_key="bk_...")— registry key, from the registry settings pageZENODO_SANDBOX_TOKEN— a Zenodo sandbox token for a dry-run DOI (usesandbox=True); drop it for the real thing
[7]:
if os.environ.get("BATTINFO_API_KEY"):
outcomes = ws.publish(
note="My first BattINFO dataset",
zenodo=bool(os.environ.get("ZENODO_SANDBOX_TOKEN")),
sandbox=True,
)
ws.status()
else:
print("Records are saved and validated locally.")
print("To publish: ws.login(api_key='...') then re-run — see ws.quickstart().")
Records are saved and validated locally.
To publish: ws.login(api_key='...') then re-run — see ws.quickstart().
What you have now#
_scratch/guide-06/
├── neware-sample.csv # your raw export (untouched)
├── bdf/neware-sample.bdf.csv # the tidy, shareable table
└── .battinfo/records/ # validated, linked records with stable IRIs
├── cell-spec/ ├── cell-instance/ ├── test/ └── dataset/
Re-run this notebook from the top: same IRIs, no duplicates — that determinism is what makes the workflow safe to automate.
Next steps
Guide 1 — Concepts: the data model behind what you just did
Guide 3 — Linked records: the full provenance chain in depth
battinfo.org/validate: check any record in the browser
ws.quickstart()— this whole recipe, printed in your terminal