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:

  1. Convert — instrument files → tidy BDF tables

  2. Identify — name the physical cells you tested

  3. Link — attach each test and its data to its cell

  4. Save & validate — canonical records with stable IRIs

  5. 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 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 page

  • ZENODO_SANDBOX_TOKEN — a Zenodo sandbox token for a dry-run DOI (use sandbox=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