CellTriage / configs /data.yaml
Sarvarbek13's picture
CellTriage QC operator console - inference only, CPU-bound classical ML
749bffa verified
Raw
History Blame Contribute Delete
21 kB
# ---------------------------------------------------------------------------
# Dataset definition, batch structure, continuation joins and exclusions.
#
# PROVISIONAL VALUES: every block carrying `verified: false` records the
# conventionally used value from the source release, NOT a value this project
# has confirmed against the actual files. Phase 2 must confirm each against the
# parsed data, flip the flag, and record the evidence in
# docs/01_dataset_and_eda.md. Cell counts and exclusion lists that are merely
# inherited are exactly the kind of unchecked assumption that produces a
# reconciliation table nobody can defend.
# ---------------------------------------------------------------------------
dataset:
name: "Severson-Attia LFP/graphite fast-charge cycling corpus"
chemistry: "LFP / graphite"
cell_model: "A123 APR18650M1A"
nominal_capacity_ah: 1.1
nominal_voltage_v: 3.3
cycling_temperature_c: 30
discharge_protocol: "identical 4C discharge to 2.0 V"
# End-of-life definition. Cycle life = first cycle at which discharge
# capacity falls below this fraction of nominal capacity.
# Phase 2 RECOMPUTES cycle life from the capacity series using this
# definition and compares against any precomputed field. A precomputed label
# is not trusted; every disagreement is reported.
eol_capacity_fraction: 0.80
source:
# TASK 2.0 COMPLETE. Each URL below was verified by request, not recalled:
# the server's own Content-Disposition header supplied the filename and
# Content-Length the exact size. The first 19 bytes of each file were read
# via a Range request and confirmed to be the ASCII string
# "MATLAB 7.3 MAT-file", which is why h5py is required and scipy.io cannot
# be used.
#
# The host sends no Accept-Ranges header but DOES honour Range requests,
# answering HTTP 206 with a correct Content-Range. Resume is therefore
# implemented in src/data/downloader.py on empirically confirmed behaviour
# rather than on an advertised capability.
#
# `verified` stays false until sha256 digests are computed from the
# downloaded bytes; the schema in src/utils/config.py refuses to accept
# verified: true while any digest is missing.
verified: true
verified_on: "2026-08-03"
landing_page: "https://data.matr.io/1/projects/5c48dd2bc625d700019f3204"
primary_citation: >-
Severson, K.A. et al. Data-driven prediction of battery cycle life before
capacity degradation. Nature Energy 4, 383-391 (2019).
extension_citation: >-
Attia, P.M. et al. Closed-loop optimization of fast-charging protocols for
batteries with machine learning. Nature 578, 397-402 (2020).
files:
- name: "2017-05-12_batchdata_updated_struct_errorcorrect.mat"
url: "https://data.matr.io/1/api/v1/file/5c86c0b5fa2ede00015ddf66/download"
size_bytes: 3025320241
batch: "batch1"
sha256: 9d928ab978f0e3c70b31cb833a749fedd35094d01af76475d69b40aa3497f5ba
- name: "2017-06-30_batchdata_updated_struct_errorcorrect.mat"
url: "https://data.matr.io/1/api/v1/file/5c86bf13fa2ede00015ddd82/download"
size_bytes: 2007331155
batch: "batch2"
sha256: 63ab200d09ecb237fee5ef3a5c5db76e3212e3206a0bd92f769e1427fed338b8
- name: "2018-04-12_batchdata_updated_struct_errorcorrect.mat"
url: "https://data.matr.io/1/api/v1/file/5c86bd64fa2ede00015ddbb2/download"
size_bytes: 3236690412
batch: "batch3"
sha256: 62c30e413b63e6144720e016deed3661fac8468641794a5807b123fe84717998
total_size_bytes: 8269341808
format: "MATLAB v7.3 (HDF5) - must be read with h5py, NOT scipy.io"
# The Attia et al. (2020) extension (batch 4 onward, taking the corpus to
# 169 cells) lives in a DIFFERENT matr.io project. Candidate file IDs probed
# during Task 2.0 returned zero-length responses, so no URL is recorded here.
# Phase 2 targets the 124-cell primary corpus; the extension is deferred
# rather than guessed at.
extension_located: false
paths:
raw: "data/raw"
interim: "data/interim"
processed: "data/processed"
cells: "data/interim/cells"
# ---------------------------------------------------------------------------
# Batch structure. Batch identity is treated as a proxy for production
# campaign; see docs/01_dataset_and_eda.md for the manufacturing-analogue
# mapping and its honest limitations.
# ---------------------------------------------------------------------------
batches:
batch1:
date: "2017-05-12"
role: "in_distribution"
expected_cells_raw: 46
expected_cells_used: 41
# VERIFIED in Phase 2: parser found exactly 46 records; 5 excluded; 41 used.
verified: true
note: "Five cells continue into batch 2 and are joined, not double-counted."
batch2:
date: "2017-06-30"
role: "in_distribution"
expected_cells_raw: 48
expected_cells_used: 43
# VERIFIED in Phase 2: 48 records found; 5 consumed by continuation joins; 43 used.
verified: true
note: >-
Five records are continuations of batch-1 cells and are consumed by the
join, so they are not counted as independent cells here.
batch3:
date: "2018-04-12"
role: "out_of_distribution"
expected_cells_raw: 46
expected_cells_used: 40
# VERIFIED in Phase 2: 46 records found; 6 excluded; 40 used.
verified: true
note: >-
Generated later under different conditions. This is the natural
out-of-distribution split for RQ4 (process-recipe / campaign shift).
# ---------------------------------------------------------------------------
# Batch-1 cells whose cycling continued as a batch-2 record. These must be
# JOINED into a single physical cell, never counted twice.
#
# WHY this matters beyond bookkeeping: a cell split across two records has its
# true cycle life truncated in the batch-1 record. Failing to join would inject
# five artificially short-lived cells into training and corrupt the label.
#
# `known_extra_cycles` records the continuation length cited in the source
# release. Phase 2 must RECOMPUTE the joined length from the data and treat any
# disagreement as a parser bug, not as a value to overwrite.
# ---------------------------------------------------------------------------
continuations:
- {primary: "b1c0", continued_as: "b2c7", known_extra_cycles: 662, verified: true}
- {primary: "b1c1", continued_as: "b2c8", known_extra_cycles: 981, verified: true}
- {primary: "b1c2", continued_as: "b2c9", known_extra_cycles: 1060, verified: true}
- {primary: "b1c3", continued_as: "b2c15", known_extra_cycles: 208, verified: true}
- {primary: "b1c4", continued_as: "b2c16", known_extra_cycles: 482, verified: true}
# ---------------------------------------------------------------------------
# Exclusion list. EVERY excluded cell carries an ID and a reason -- the schema
# in src/utils/config.py rejects an entry without one, which makes the project
# rule "never silently drop a cell" structurally enforced rather than a matter
# of discipline.
#
# `verified: false` means: this exclusion is inherited from the source release
# and Phase 2 must independently confirm the stated reason holds in the parsed
# data (e.g. actually confirm the cell never reaches the 80% threshold) before
# the cell is dropped from the analysis.
# ---------------------------------------------------------------------------
exclusions:
- {cell_id: "b1c8", batch: "batch1", reason: "Right-censored: discharge capacity never reaches the end-of-life threshold across 878 cycles, so no cycle-life label exists.", verified: true}
- {cell_id: "b1c10", batch: "batch1", reason: "Right-censored: discharge capacity never reaches the end-of-life threshold across 905 cycles, so no cycle-life label exists.", verified: true}
- {cell_id: "b1c12", batch: "batch1", reason: "Right-censored: discharge capacity never reaches the end-of-life threshold across 901 cycles, so no cycle-life label exists.", verified: true}
- {cell_id: "b1c13", batch: "batch1", reason: "Right-censored: discharge capacity never reaches the end-of-life threshold across 896 cycles, so no cycle-life label exists.", verified: true}
- {cell_id: "b1c22", batch: "batch1", reason: "Right-censored: discharge capacity never reaches the end-of-life threshold across 890 cycles, so no cycle-life label exists.", verified: true}
- {cell_id: "b3c2", batch: "batch3", reason: "Noisy channel, independently confirmed: trajectory roughness ranks 3rd of 46 in batch 3, above the 90th percentile of its peers.", verified: true}
- {cell_id: "b3c23", batch: "batch3", reason: "Noisy channel, independently confirmed: roughest trajectory of all 46 batch-3 cells, and additionally right-censored across 2189 cycles.", verified: true}
- {cell_id: "b3c32", batch: "batch3", reason: "Right-censored across 2237 cycles, so no cycle-life label exists. NOTE: the source release cites a noisy channel, which this project could NOT confirm (roughness ranks only 15 of 46); the censoring is the defensible reason.", verified: true}
- {cell_id: "b3c37", batch: "batch3", reason: "Excluded by the source release as a noisy channel. NOT INDEPENDENTLY CONFIRMED: roughness ranks 20 of 46 and the cell yields a valid cycle life of 1389. Retained in the exclusion list only for comparability with the standard 124-cell corpus.", verified: false}
- {cell_id: "b3c42", batch: "batch3", reason: "Excluded by the source release as a noisy channel. NOT INDEPENDENTLY CONFIRMED: roughness ranks 22 of 46 and the cell yields a valid cycle life of 1641. Retained in the exclusion list only for comparability with the standard 124-cell corpus.", verified: false}
- {cell_id: "b3c43", batch: "batch3", reason: "Excluded by the source release as a noisy channel. NOT INDEPENDENTLY CONFIRMED: roughness ranks 23 of 46 and the cell yields a valid cycle life of 1045. Retained in the exclusion list only for comparability with the standard 124-cell corpus.", verified: false}
reconciliation:
# Phase 2 prints a table: found / joined / excluded / used, and asserts the
# final count matches. 124 cells is the figure reported for the primary
# dataset; the 45-cell Attia extension would bring the corpus to 169.
expected_cells_used: 124
expected_cells_used_with_extension: 169
# VERIFIED in Phase 2: 140 raw records -> 5 consumed by joins -> 135 physical
# cells -> 11 excluded -> exactly 124 used. Every per-batch count matches too.
# The 169-cell figure remains unverified: the Attia extension is not located.
verified: true
# ---------------------------------------------------------------------------
# Parsing and storage policy (Phase 2).
# ---------------------------------------------------------------------------
parsing:
# Summary series (one row per cycle: capacity, IR, temperature, charge time)
# are retained for EVERY cycle. Cycle life, capacity-fade trajectories and
# resistance evolution all need the full trajectory to end-of-life.
#
# Within-cycle traces (V, I, Q, T, t and the interpolated Q(V) family) are
# retained only up to this cycle. WHY this is methodologically safe rather
# than a shortcut: the leakage rule caps every feature at budget N <= 100, so
# no feature may legitimately read a within-cycle trace beyond cycle 100, and
# the Phase 3 figures that use within-cycle data (Q(V) at cycles 10 and 100,
# the DeltaQ(V) curve family) also stop at 100. Retaining all ~96,700 cycles
# of raw traces would cost several GiB to store data the project's own rules
# forbid using.
#
# The value carries 20 cycles of headroom above the maximum budget so that a
# budget of exactly 100 has room for baseline/late cycle pairs.
within_cycle_max_cycle: 120
# Written with zstd: these frames are large and read many times downstream.
parquet_compression: "zstd"
# ---------------------------------------------------------------------------
# Validator bounds (Phase 2). These checks FAIL LOUDLY -- they do not warn and
# continue. A physically impossible value means the parser misread the file
# layout, and every downstream number would be fiction.
# ---------------------------------------------------------------------------
validation:
# 0 < Q <= 1.2 * nominal. The 1.2 headroom accommodates genuine early-life
# capacity above nameplate without admitting a unit-conversion error.
capacity_max_fraction_of_nominal: 1.2
# The CHARGING PROTOCOL setpoints are a 2.0 V discharge cutoff and a 3.6 V
# charge cutoff. Measured samples legitimately sit outside them: the cycler
# samples at a finite rate, so the last sample of a discharge overshoots the
# cutoff before the step terminates. Measured across the corpus, voltage
# spans [1.919, 3.614] V.
#
# The validation bound is therefore widened past the protocol setpoints.
# This does NOT weaken the check: its purpose is to catch a parser that
# misread the file layout, and such an error produces zeros, negatives, or
# values orders of magnitude off -- not an 80 mV cutoff overshoot. Recording
# both values keeps the distinction explicit.
protocol_voltage_range_v: [2.0, 3.6]
voltage_range_v: [1.85, 3.70]
# Cells were cycled in a 30 degC chamber. Measured span across the corpus is
# [27.3, 40.4] degC (cells self-heat under 4C discharge), comfortably inside
# this bound, which is left at its original value.
temperature_range_c: [20, 50]
require_monotonic_cycle_index: true
forbid_duplicate_cycle_index: true
# Compare recomputed cycle life against any precomputed field and report
# every disagreement larger than this many cycles.
cycle_life_disagreement_tolerance: 0
# Cycle life is RECOMPUTED from the discharge-capacity series; the
# precomputed field in the .mat file is treated as a claim to be checked,
# never as the label. WHY: the entire target variable of the project depends
# on this definition, and inheriting it unexamined would make every
# downstream number rest on an assumption nobody verified.
cycle_life_recompute:
# Cycle at which discharge capacity first falls to eol_capacity_fraction
# of nominal (0.80 * 1.1 Ah = 0.88 Ah).
method: "first_below_threshold"
# Rolling-median window applied before thresholding. Measured capacity is
# noisy cycle-to-cycle, and a single spurious dip would otherwise truncate
# a cell's life by hundreds of cycles. 1 disables smoothing.
smoothing_window: 5
# Ignore threshold crossings before this cycle. Early-life measurement
# artefacts occasionally produce an implausible first-cycle reading.
min_cycle: 10
# ---------------------------------------------------------------------
# EMPIRICALLY DERIVED, NOT ASSUMED. Measured in Phase 2 across all 46
# batch-1 cells (see docs/01_dataset_and_eda.md).
#
# These cells were cycled UNTIL they reached 80% of nominal, at which
# point the experiment stopped. Consequently capacity never actually
# crosses below 0.88 Ah -- the record simply ends just above it. A naive
# "first cycle strictly below 0.88 Ah" rule therefore classifies EVERY
# cell as never reaching end of life, which is plainly wrong.
#
# The observed minima separate cleanly into two groups:
# 36 cells that reached end of life : min QD in [0.8801, 0.8834] Ah
# 10 cells that did not : min QD in [0.9131, 1.0433] Ah
# (the 10 being the 5 continuation cells, whose records are truncated
# because cycling continues in batch 2, and the 5 excluded cells).
#
# A cell is treated as having reached end of life if its capacity comes
# within this fraction of the threshold.
#
# CALIBRATED IN SMOOTHED SPACE, which is where the decision is actually
# made. The rolling median lifts a cell's minimum slightly, because a
# monotonically fading series has a trailing-median above its final value,
# so a tolerance calibrated on raw minima wrongly censors cells that fade
# to just above threshold. Measured separation of smoothed minima:
# window 1: EOL max 0.8834 | non-EOL min 0.9131
# window 3: EOL max 0.8846 | non-EOL min 0.9142
# window 5: EOL max 0.8859 | non-EOL min 0.9150 <- window in use
# The gap is ~0.029 Ah at every window. 0.02 puts the effective threshold
# at 0.8976 Ah (81.6% of nominal), mid-gap, and classifies all 46 batch-1
# cells correctly at ALL three smoothing windows -- so the result does not
# depend on the smoothing choice.
#
# Calibrated on batch 1; re-verified against batches 2 and 3 in Phase 2.
eol_tolerance_fraction: 0.02
# ---------------------------------------------------------------------
# PLACEHOLDER ROW -- A REAL STRUCTURAL DIFFERENCE BETWEEN BATCHES,
# measured in Phase 2, not assumed.
#
# batch1: ALL 46 cells begin with a row in which every summary field is
# exactly zero. It is a structural artefact, not a measured cycle,
# and is why the source literature takes "discharge capacity at
# cycle 2" as the first usable value.
# batch2: 0 of 43 cells have one.
# batch3: 0 of 46 cells have one.
#
# CONSEQUENCE, and it matters for Phase 4: raw cycle numbering does NOT mean
# the same thing across batches. Cycle 2 is the first measured cycle in
# batch 1 but the second in batches 2 and 3. Slicing "cycles 1..N" on the raw
# index would therefore give batch-1 cells one fewer measured cycle than the
# others at every budget -- a systematic, batch-correlated bias in exactly
# the variable RQ4 tests for.
#
# RESOLUTION: the placeholder row is RETAINED in the stored parquet, so the
# data stays faithful to the source and cycle numbering remains comparable
# with published work. A separate `cycle_measured` column numbers only
# measured cycles from 1, consistently across every batch, and that is what
# feature budgets slice on.
placeholder_row:
expect_at_most_one: true
expect_at_first_cycle: true
batches_with_placeholder: ["batch1"]
# ---------------------------------------------------------------------
# DISCRIMINATING A PARSER BUG FROM AN INSTRUMENT ARTEFACT.
#
# These are different failures and only one invalidates the pipeline:
# * A parser bug is SYSTEMATIC ACROSS CELLS. Misreading the file layout
# corrupts every cell the same way.
# * An instrument artefact is LOCALISED TO A CELL. Measured here: 4 of 135
# cells (3.0%) contain any implausible capacity reading, and 3 of those
# are a single cycle. The fourth, b1c18, shows q_charge ramping
# monotonically 1.335 -> 1.968 Ah across cycles 16-40 while q_discharge
# stays flat at 1.070 Ah, which is a coulomb-counter reset failure on the
# charge channel of one cell -- not a mis-parse, which could not leave
# the discharge channel of the same cell intact.
#
# So the run FAILS only if the fraction of AFFECTED CELLS exceeds
# systematic_cell_fraction. Per-cell violations are reported as WARN and the
# cell is flagged in the cohort so Phase 4 handles it deliberately rather
# than inheriting it silently.
systematic_violation_fraction: 0.01 # within a cell: escalates the message
systematic_cell_fraction: 0.20 # across cells: fails the run
# ---------------------------------------------------------------------
# MAXIMUM PLAUSIBLE DURATION OF ONE CYCLE, in minutes. Used by the Phase 4
# feature builder to mask cycles whose recorded elapsed time cannot be real.
#
# DERIVED FROM THE DATA, measured across all 135 cells over cycles 1-100:
# median cycle duration, median across cells : 51.8 min
# 118 of 135 cells never exceed : 78.6 min
# 16 cells show ONE isolated long cycle : ~460 min (plausibly a real
# rest, and 1 cycle in 100)
# b1c18 has a MEDIAN cycle duration of : 2236 min (37 hours)
#
# These cells are charged at 3.6-8C and discharged at 4C, so a genuine cycle
# takes tens of minutes. A cell whose TYPICAL cycle reads 37 hours has a
# broken time channel, not a long experiment -- and because thermal exposure
# is an integral over time, that error propagates directly into Group D:
# b1c18's thermal_exposure came out 71x the cohort median, which alone drove a
# cross-validated ridge prediction to -50.9 on a target of 2.84.
#
# 300 minutes sits ~6x above the median and ~4x above the ceiling of the 118
# clean cells, so it masks the broken channel without touching normal cycling.
# Applied per CYCLE, not per cell: a cell with one long rest keeps its other
# 99 cycles.
max_cycle_duration_minutes: 300