Spaces:
Running on Zero
Running on Zero
| # --------------------------------------------------------------------------- | |
| # 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 | |