Skip to content

Real cycling data walkthrough

This notebook processes genuine battery-cycling data: the small vendored test fixture tests/data/cycler_cc_harmonized_raw.parquet (10 261 rows, 18 cycles of constant-current cycling, originally recorded at IFE, Norway). It uses the CellpyCellCore class, which orchestrates the engine and adds C-rates and half-cell cycle modes on top of the plain quickstart pipeline.

Run it from a checkout of the repository so the fixture path resolves.

from pathlib import Path

import matplotlib.pyplot as plt
import polars as pl

from cellpycore import CellpyCellCore, Data

FIXTURE = Path("../../tests/data/cycler_cc_harmonized_raw.parquet").resolve()
raw = pl.read_parquet(FIXTURE)
print(f"{raw.height} rows, {raw.width} columns")
10261 rows, 29 columns

1. A first look at the raw data

The frame is already in the harmonized raw schema: potential in volts, current in amperes, times in seconds, capacities cumulative per cycle and direction.

raw.select(
    "datapoint_num",
    "test_time",
    "cycle_num",
    "step_num",
    "current",
    "potential",
    "cumulative_charge_capacity",
).head()
shape: (5, 7)
datapoint_numtest_timecycle_numstep_numcurrentpotentialcumulative_charge_capacity
i64f64i64i64f64f64f64
1300.010482110.03.0976170.0
2600.014947110.03.100080.0
3900.026371110.03.1013120.0
41200.039773110.03.1028520.0
51500.051275110.03.103160.0
first_cycles = raw.filter(pl.col("cycle_num") <= 3)
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(9, 5), sharex=True)
ax1.plot(first_cycles["test_time"] / 3600, first_cycles["potential"], lw=0.8)
ax1.set_ylabel("potential (V)")
ax2.plot(
    first_cycles["test_time"] / 3600,
    first_cycles["current"] * 1000,
    lw=0.8,
    color="tab:orange",
)
ax2.set_ylabel("current (mA)")
ax2.set_xlabel("test_time (h)")
fig.suptitle("First three cycles")
fig.tight_layout()

png

2. Process with CellpyCellCore

The class handles the cycle_mode string for you. This cell is an anode half-cell, so coulombic efficiency is computed in the inverted direction (CE = 100 * charge / discharge). make_core_step_table also estimates a per-step C-rate from nom_cap (absolute, in the same unit as capacity — pass your cell's real value).

NOM_CAP = 0.001  # Ah — nominal capacity of this small test cell

core = CellpyCellCore()
core.data = Data.from_raw_frame(raw)
core.cycle_mode = "anode"

data = core.make_core_step_table(core.data, nom_cap=NOM_CAP)
data = core.make_core_summary(data, current_conversion_factor=1.0)

print(f"steps:   {data.steps.height} rows")
print(f"summary: {data.summary.height} cycles")
steps:   103 rows
summary: 18 cycles

3. The step table

One row per sequential step with per-step statistics, the classified step_type, and the estimated C-rate.

data.steps.select(
    "cycle_num",
    "step_num",
    "step_type",
    "c_rate",
    "current_mean",
    "potential_first",
    "potential_last",
).head(8)
shape: (8, 7)
cycle_numstep_numstep_typec_ratecurrent_meanpotential_firstpotential_last
i64i64strf64f64f64f64
11"ocvrlx_down"0.00.03.0976172.88916
12"ir"0.000666.6289e-72.89072.8907
13"discharge"0.15221-0.0001522.8398940.049894
14"ir"0.00004-4.3646e-80.0643660.064366
15"ocvrlx_up"0.00.00.06960.095465
16"charge"0.153620.0001540.1102451.000113
17"ir"0.000313.0962e-70.9958030.995803
18"ocvrlx_down"0.00.00.9905680.866787
data.steps.group_by("step_type").len().sort("len", descending=True)
shape: (5, 2)
step_typelen
stru32
"ir"33
"discharge"18
"ocvrlx_down"18
"ocvrlx_up"17
"charge"17

4. Capacity fade and coulombic efficiency

The per-cycle summary is where degradation trends live.

summary = data.summary
fig, ax1 = plt.subplots(figsize=(8, 4))
ax1.plot(
    summary["cycle_num"],
    summary["discharge_capacity"] * 1000,
    "o-",
    label="discharge capacity",
)
ax1.plot(
    summary["cycle_num"],
    summary["charge_capacity"] * 1000,
    "s-",
    label="charge capacity",
)
ax1.set_xlabel("cycle number")
ax1.set_ylabel("capacity (mAh)")
ax1.legend(loc="center right")

ax2 = ax1.twinx()
ax2.plot(
    summary["cycle_num"],
    summary["coulombic_efficiency"],
    "^--",
    color="tab:green",
    label="coulombic efficiency",
)
ax2.set_ylabel("coulombic efficiency (%)")
ax2.legend(loc="lower right")
ax1.set_title("Capacity fade and coulombic efficiency")
fig.tight_layout()

png

summary.select(
    "cycle_num",
    "charge_capacity",
    "discharge_capacity",
    "coulombic_efficiency",
    "charge_c_rate",
    "discharge_c_rate",
).head(6)
shape: (6, 6)
cycle_numcharge_capacitydischarge_capacitycoulombic_efficiencycharge_c_ratedischarge_c_rate
i64f64f64f64f64f64
10.0016250.00175592.6107910.153620.15221
20.00170.001567108.4268380.153610.15232
30.0017320.001586109.193730.153590.15232
40.0015760.001517103.866010.305530.30444
50.0015350.001471104.3581910.305490.30439
60.0015370.001471104.5176740.305470.30444

Next steps

  • core.add_scaled_summary_columns(...) adds gravimetric / areal / absolute variants of the capacity-like columns and a normalized (equivalent) cycle index — see the standalone-use guide.
  • make_core_summary(..., exclude_step_types=["cv_"]) builds a summary with the capacity contribution of matching step types subtracted.
  • The exact meaning of every output column is specified in the step table and cycle table documents.