File size: 21,033 Bytes
749bffa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# ---------------------------------------------------------------------------
# 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