From 7cbd2c0a9703d71aa584fd4723b3ad5482614266 Mon Sep 17 00:00:00 2001 From: jepegit Date: Mon, 27 Jul 2026 22:03:43 +0200 Subject: [PATCH] Remove the headers_* legacy shim from CellpyCell (E3 final, #715) The per-instance `headers_normal` / `headers_summary` / `headers_step_table` attributes were a 2.0 deprecation shim (LegacyHeaderShim) mapping legacy `*_txt` attribute access to native cellpycore column names. The runtime was migrated off the shim in 2.1 (parts 1-3, #744/#745/#746); this removes the shim itself on both the native and legacy-bridge paths per the full-removal decision. - delete `cellpy/parameters/legacy_header_shim.py` (+ its test) - drop the shim wiring block from `CellpyCell.__init__` (both branches) - drop the shim's pending-deprecation seed; regenerate DEPRECATIONS.md - migrate the remaining `c.headers_*` accesses to `c.schema.raw/steps/summary` across tests and the maccor/custom `__main__` demo blocks; legacy-only step columns (`info`) become literals, and legacy-singleton usages keep `*_txt` - rewrite the shim tests in test_cell_schema.py as a shim-removed assertion `c.schema.raw` / `c.schema.steps` / `c.schema.summary` is the replacement. Co-Authored-By: Claude Opus 4.8 --- DEPRECATIONS.md | 1 - cellpy/_deprecation.py | 14 +- cellpy/parameters/cell_schema.py | 4 +- cellpy/parameters/legacy_header_shim.py | 193 ----------------------- cellpy/readers/cellreader.py | 31 ++-- cellpy/readers/instruments/custom.py | 12 +- cellpy/readers/instruments/maccor_txt.py | 52 +++--- tests/test_batmo.py | 24 +-- tests/test_cell_readers.py | 62 ++++---- tests/test_cell_schema.py | 45 ++---- tests/test_cellpy_file_roundtrip.py | 20 +-- tests/test_cellpy_splitting.py | 2 +- tests/test_cellpycell_add_to_summary.py | 14 +- tests/test_cv_partition.py | 2 +- tests/test_exporters_bdf.py | 35 ++-- tests/test_goldens.py | 4 +- tests/test_helpers.py | 2 +- tests/test_legacy_header_shim.py | 131 --------------- tests/test_ocv_relax.py | 14 +- tests/test_plotutils_summary_plot.py | 16 +- tests/test_slim.py | 12 +- 21 files changed, 159 insertions(+), 531 deletions(-) delete mode 100644 cellpy/parameters/legacy_header_shim.py delete mode 100644 tests/test_legacy_header_shim.py diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md index a80163ee..c9058ee7 100644 --- a/DEPRECATIONS.md +++ b/DEPRECATIONS.md @@ -8,4 +8,3 @@ uv run python -m cellpy._deprecation | Name | Replacement | Introduced | Removal | | --- | --- | --- | --- | -| `legacy header attribute access (headers_normal / _summary / _step_table)` | `c.schema.raw / c.schema.steps / c.schema.summary` | 2.0 | 2.1 | diff --git a/cellpy/_deprecation.py b/cellpy/_deprecation.py index 9083f410..52e861b4 100644 --- a/cellpy/_deprecation.py +++ b/cellpy/_deprecation.py @@ -94,16 +94,10 @@ def write_deprecations_md(path: str | Path) -> None: def _seed_known_deprecations() -> None: """Register deprecations that exist before any runtime call (for doc generation).""" # make_new_cell was removed in 2.1 (E3, #715) -- use CellpyCell.vacant. - # Legacy header attribute access (headers_normal.voltage_txt, hdr_steps.cycle, - # hdr_summary[...]) is shimmed to the native cellpycore schema names at the - # native-headers flip (D6). One summary row here; the shim warns per attribute - # at runtime (cellpy.parameters.legacy_header_shim). - _register( - "legacy header attribute access (headers_normal / _summary / _step_table)", - "c.schema.raw / c.schema.steps / c.schema.summary", - removal="2.1", - introduced="2.0", - ) + # Legacy header attribute access (headers_normal / _summary / _step_table, + # the legacy_header_shim) was removed in 2.1 (E3, #715) -- use c.schema.raw / + # c.schema.steps / c.schema.summary. No longer registered here. + # cellpy.utils.easyplot was removed in 2.0 (#544); it is no longer a # pending deprecation, so it is dropped from the registry / DEPRECATIONS.md. diff --git a/cellpy/parameters/cell_schema.py b/cellpy/parameters/cell_schema.py index fdd51561..cfc759fc 100644 --- a/cellpy/parameters/cell_schema.py +++ b/cellpy/parameters/cell_schema.py @@ -1,9 +1,9 @@ """The public column-name API for a cell (native-headers Phase 4, issue #558). ``CellpyCell.schema`` answers one question: *what is this column called on the -frames this cell is carrying?* It is the sanctioned replacement for the legacy +frames this cell is carrying?* It is the replacement for the legacy ``headers_normal`` / ``headers_step_table`` / ``headers_summary`` attributes, -which are now a deprecation shim (``legacy_header_shim``, D6). +which were a deprecation shim in 2.0 and were removed in 2.1 (E3, #715). >>> c = cellpy.get(...) # doctest: +SKIP >>> c.data.raw[c.schema.raw.potential] # doctest: +SKIP diff --git a/cellpy/parameters/legacy_header_shim.py b/cellpy/parameters/legacy_header_shim.py deleted file mode 100644 index 2c1a102b..00000000 --- a/cellpy/parameters/legacy_header_shim.py +++ /dev/null @@ -1,193 +0,0 @@ -"""Legacy-header attribute shim (native-headers flip, D6 / Stage 2). - -After the flip renames the on-frame columns to the native ``cellpycore`` -schema, code that still references headers **by legacy attribute** — -``headers_normal.voltage_txt``, ``hdr_steps.cycle``, -``hdr_summary["charge_capacity"]`` — must keep resolving to the (now native) -column name. This shim does that: it wraps a native ``config.Cols`` object for -one frame and, on a legacy attribute/key, returns the native column name and -emits a one-time ``DeprecationWarning``. Native-name access passes straight -through without warning. - -All legacy→native knowledge lives in ``cellpycore.legacy.mapping`` — this -module only adapts it to attribute/subscript access and owns the warning. The -mapping already handles the ``discharge_capacity`` / ``discharge_capacity_raw`` -shared-value pair. **Legacy-only** attributes (columns the flip does not rename) -resolve to their unchanged legacy name via the legacy ``BaseHeaders`` object — -``to_native`` passes those columns through untouched, so the native frame still -carries them under that name (e.g. ``headers_normal.datetime_txt`` → ``date_time``, -``headers_normal.test_id_txt`` → ``test_id``). - -**Wired into ``CellpyCell`` at the flip (Stage 5a).** When the runtime is native, -``CellpyCell`` substitutes these shims for the legacy ``Headers*`` objects so -legacy attribute access keeps resolving to the native column names. - -Scope note: statistic step columns composed by callers as ``f"{hdr.voltage}_avr"`` -are *not* interceptable here (they are built strings, not attribute access) — -those callers migrate in Stage 3. Summary specific columns accessed by key -(``hdr_summary["charge_capacity_gravimetric"]``) *are* handled, via postfix -decomposition. -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from cellpycore.legacy import mapping - -from cellpy._deprecation import warn_once - -if TYPE_CHECKING: - from cellpycore import config - -# HeadersSummary specific-column postfixes (native and legacy names match). -_SPECIFIC_MODES = ("gravimetric", "areal", "absolute") - -# Frame label -> the attribute name a warning should mention. -_FRAME_LABEL = { - "raw": "headers_normal", - "step": "headers_step_table", - "cycle": "headers_summary", -} - -# Frame label -> the public replacement frame on ``CellpyCell.schema`` (#558). -# Note the frames are spelled as they are on ``c.data`` (raw/steps/summary), -# not as cellpy-core spells them (raw/step/cycle). -_SCHEMA_FRAME = { - "raw": "raw", - "step": "steps", - "cycle": "summary", -} - - -class LegacyHeaderShim: - """Resolve legacy header attributes/keys to native column names, with a warning. - - Args: - frame: The Schema frame this shim covers — ``"raw"``, ``"step"`` or - ``"cycle"``. - native_cols: The native ``config.Cols`` object for that frame; native - attribute access passes through to it unchanged (no warning). - legacy_cols: The legacy ``BaseHeaders`` object for that frame - (``HeadersNormal`` / ``HeadersStepTable`` / ``HeadersSummary``). - Used to resolve **legacy-only** attributes: columns the flip does - not rename (``to_native`` passes them through unchanged), so the - native frame still carries them under their legacy name — the shim - returns that name rather than raising. - """ - - def __init__( - self, - frame: str, - native_cols: "config.Cols", - legacy_cols: object | None = None, - ) -> None: - if frame not in mapping.LEGACY_ATTR_TO_SCHEMA: - raise ValueError( - f"unknown frame {frame!r}; expected one of " - f"{sorted(mapping.LEGACY_ATTR_TO_SCHEMA)}" - ) - # Set via __dict__ so __getattr__ never sees these. - object.__setattr__(self, "_frame", frame) - object.__setattr__(self, "_native", native_cols) - object.__setattr__(self, "_legacy", legacy_cols) - - # -- resolution ----------------------------------------------------------- - def _resolve(self, name: str) -> str: - frame = self._frame - native = self._native - - # Native attribute -> pass through, no warning. - if not name.startswith("_") and hasattr(native, name): - return getattr(native, name) - - # Direct legacy attribute -> native name (+ warning). - try: - native_name = mapping.legacy_attr_to_native(frame, name) - except KeyError: - native_name = None - - if native_name is not None: - self._warn(name, native_name) - return native_name - - # Summary specific column by key: strip the postfix, resolve the base, - # re-attach the (identical) native postfix. - if frame == "cycle": - for mode in _SPECIFIC_MODES: - suffix = f"_{mode}" - if name.endswith(suffix): - base = name[: -len(suffix)] - try: - native_base = mapping.legacy_attr_to_native("cycle", base) - except KeyError: - break - self._warn(name, f"{native_base}{suffix}") - return f"{native_base}{suffix}" - - # Legacy-only attribute: the flip does not rename this column, so the - # native frame carries it under its (unchanged) legacy name. Return that - # name — no warning, because there is no native name to migrate to. - legacy = self._legacy - if legacy is not None and not name.startswith("_") and hasattr(legacy, name): - return getattr(legacy, name) - - raise KeyError( - f"{_FRAME_LABEL[frame]}: {name!r} is not a native column and has no " - f"legacy mapping (unknown attribute)." - ) - - def _warn(self, name: str, native_name: str) -> None: - label = _FRAME_LABEL[self._frame] - # Name the exact replacement, not just the concept: the user needs the - # attribute they should type, and we know it here (conventions plan §3). - replacement = f"c.schema.{_SCHEMA_FRAME[self._frame]}.{native_name}" - warn_once( - f"{label}.{name}", - replacement, - removal="2.1", - introduced="2.0", - stacklevel=4, - ) - - # -- access protocols ----------------------------------------------------- - def __getattr__(self, name: str) -> str: - # Never resolve dunder/private names: no legacy header attribute starts - # with "_", and resolving them would recurse on the shim's own private - # attrs (_frame/_native/_legacy) before they exist during copy/pickle - # reconstruction. Raising AttributeError lets copy/deepcopy fall back to - # the default reconstruction path. - if name.startswith("_"): - raise AttributeError(name) - try: - return self._resolve(name) - except KeyError as exc: - raise AttributeError(str(exc)) from None - - def __getitem__(self, key: str) -> str: - return self._resolve(key) - - -def build_legacy_shims(schema: "config.Schema") -> dict[str, LegacyHeaderShim]: - """Return the three legacy-header shims for a native ``config.Schema``. - - Keys are the ``CellpyCell`` header attribute names the flip (Stage 5) points - at these shims: ``headers_normal`` / ``headers_step_table`` / - ``headers_summary``. Each shim also gets the matching legacy ``BaseHeaders`` - object so legacy-only (un-renamed) columns keep resolving to their name. - """ - from cellpy.parameters.internal_settings import ( - get_headers_normal, - get_headers_step_table, - get_headers_summary, - ) - - return { - "headers_normal": LegacyHeaderShim("raw", schema.raw, get_headers_normal()), - "headers_step_table": LegacyHeaderShim( - "step", schema.step, get_headers_step_table() - ), - "headers_summary": LegacyHeaderShim( - "cycle", schema.cycle, get_headers_summary() - ), - } diff --git a/cellpy/readers/cellreader.py b/cellpy/readers/cellreader.py index 6a848321..0e094f62 100644 --- a/cellpy/readers/cellreader.py +++ b/cellpy/readers/cellreader.py @@ -240,9 +240,8 @@ def __init__( native_schema (bool): the runtime column schema (native-headers flip, Stage 5a). Defaults to True in cellpy 2: frames are kept in native cellpy-core column names and the polars engine runs - directly (no legacy rename sandwich); legacy attribute access - (``headers_normal.voltage_txt``, ...) is carried by the D6 shim - (``legacy_header_shim``). Set False for the legacy bridge + directly (no legacy rename sandwich); use ``self.schema`` for + column names. Set False for the legacy bridge (``OldCellpyCellCore``, legacy column names) — the path for v8 byte round-trips and ``merge`` until native merge lands (5b). ``get_cap`` / exporters / plotting work on a native cell via the @@ -320,24 +319,12 @@ def __init__( self.cellpy_datadir = internals.OtherPath(config.paths.cellpydatadir) self.auto_dirs = config.reader.auto_dirs # v2.0 - # - headers and instruments - # Native-headers flip (Stage 5a): on the native runtime the Data frames - # carry native cellpycore column names, so substitute the legacy - # Headers* objects with the D6 shims that resolve legacy attribute access - # (headers_normal.voltage_txt, ...) to the native column name — with a - # DeprecationWarning for renamed columns, silently for columns the flip - # leaves unchanged. The legacy path keeps the plain Headers* singletons. - if self.native_schema: - from cellpy.parameters.legacy_header_shim import build_legacy_shims - - _shims = build_legacy_shims(self.core.schema) - self.headers_normal = _shims["headers_normal"] - self.headers_summary = _shims["headers_summary"] - self.headers_step_table = _shims["headers_step_table"] - else: - self.headers_normal = headers_normal - self.headers_summary = headers_summary - self.headers_step_table = headers_step_table + # - instruments + # The per-instance ``headers_normal`` / ``headers_summary`` / + # ``headers_step_table`` attributes were a 2.0 deprecation shim mapping + # legacy attribute access (``headers_normal.voltage_txt``, ...) to native + # cellpycore column names. Removed in 2.1 — use ``self.schema`` (the + # ``CellSchema`` view over ``core.schema``) for column names instead. self.instrument_factory = instrument_factory # injected (#520) or None self.register_instrument_readers() self.set_instrument() @@ -4035,7 +4022,7 @@ def filtered_summary( """Return a filtered copy of the summary DataFrame. Thin wrapper around :func:`cellpy.filters.filter_summary` that - resolves the rate column names from ``self.headers_summary``. + resolves the rate column names from ``self.schema.summary``. See the underlying function for the full range semantics; in short ``(low, high)`` keeps rows where ``low < value <= high`` and ``{"value": v, "delta": d}`` keeps rows where diff --git a/cellpy/readers/instruments/custom.py b/cellpy/readers/instruments/custom.py index dd92bb20..1b0de596 100644 --- a/cellpy/readers/instruments/custom.py +++ b/cellpy/readers/instruments/custom.py @@ -271,7 +271,7 @@ def _process_cellpy_object(name, c, out): print(f"loaded the file - now lets see what we got") raw = c.data.raw - hdr = c.headers_normal + hdr = c.schema.raw print(raw.head()) c.make_step_table() @@ -289,14 +289,14 @@ def _process_cellpy_object(name, c, out): constrained_layout=True, sharex=True, ) - raw.plot(x=hdr.test_time_txt, y=hdr.voltage_txt, ax=ax1) - raw.plot(x=hdr.test_time_txt, y=hdr.current_txt, ax=ax2) + raw.plot(x=hdr.test_time, y=hdr.potential, ax=ax1) + raw.plot(x=hdr.test_time, y=hdr.current, ax=ax2) raw.plot( - x=hdr.test_time_txt, - y=[hdr.charge_capacity_txt, hdr.discharge_capacity_txt], + x=hdr.test_time, + y=[hdr.cumulative_charge_capacity, hdr.cumulative_discharge_capacity], ax=ax3, ) - raw.plot(x=hdr.test_time_txt, y=hdr.cycle_index_txt, ax=ax4) + raw.plot(x=hdr.test_time, y=hdr.cycle_num, ax=ax4) fig_1.suptitle(f"{name.name}", fontsize=16) n = c.get_number_of_cycles() diff --git a/cellpy/readers/instruments/maccor_txt.py b/cellpy/readers/instruments/maccor_txt.py index 615bfa91..72a84084 100644 --- a/cellpy/readers/instruments/maccor_txt.py +++ b/cellpy/readers/instruments/maccor_txt.py @@ -217,15 +217,15 @@ def _check_loader_from_outside(): steps.to_csv(r"C:\scripts\notebooks\Div\trash\steps.csv", sep=";") summary.to_csv(r"C:\scripts\notebooks\Div\trash\summary.csv", sep=";") - hdr = c.headers_normal + hdr = c.schema.raw fig_1, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(6, 10)) - raw.plot(x=hdr.test_time_txt, y=hdr.voltage_txt, ax=ax1) + raw.plot(x=hdr.test_time, y=hdr.potential, ax=ax1) raw.plot( - x=hdr.test_time_txt, - y=[hdr.charge_capacity_txt, hdr.discharge_capacity_txt], + x=hdr.test_time, + y=[hdr.cumulative_charge_capacity, hdr.cumulative_discharge_capacity], ax=ax3, ) - raw.plot(x=hdr.test_time_txt, y=hdr.current_txt, ax=ax2) + raw.plot(x=hdr.test_time, y=hdr.current, ax=ax2) n = c.get_number_of_cycles() print(f"number of cycles: {n}") @@ -257,8 +257,8 @@ def _check_loader_from_outside(): ax6.plot(t, steps, label="steps") fig_3, (ax7, ax8) = plt.subplots(2, sharex=True) - raw.plot(x=hdr.test_time_txt, y=hdr.voltage_txt, ax=ax7) - raw.plot(x=hdr.test_time_txt, y=hdr.step_index_txt, ax=ax8) + raw.plot(x=hdr.test_time, y=hdr.potential, ax=ax7) + raw.plot(x=hdr.test_time, y=hdr.step_num, ax=ax8) plt.legend() plt.show() @@ -295,16 +295,16 @@ def _check_loader_from_outside_with_get(): steps.to_csv(r"C:\scripting\trash\steps.csv", sep=";") summary.to_csv(r"C:\scripting\trash\summary.csv", sep=";") - hdr = c.headers_normal + hdr = c.schema.raw fig_1, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(6, 10)) - raw.plot(x=hdr.test_time_txt, y=hdr.voltage_txt, ax=ax1, title="voltage") + raw.plot(x=hdr.test_time, y=hdr.potential, ax=ax1, title="voltage") raw.plot( - x=hdr.test_time_txt, - y=[hdr.charge_capacity_txt, hdr.discharge_capacity_txt], + x=hdr.test_time, + y=[hdr.cumulative_charge_capacity, hdr.cumulative_discharge_capacity], ax=ax3, title="caps", ) - raw.plot(x=hdr.test_time_txt, y=hdr.current_txt, ax=ax2, title="current") + raw.plot(x=hdr.test_time, y=hdr.current, ax=ax2, title="current") n = c.get_number_of_cycles() print(f"number of cycles: {n}") @@ -331,8 +331,8 @@ def _check_loader_from_outside_with_get(): ax6.plot(t, steps, label="steps") fig_3, (ax7, ax8) = plt.subplots(2, sharex=True) - raw.plot(x=hdr.test_time_txt, y=hdr.voltage_txt, ax=ax7, title="voltage") - raw.plot(x=hdr.test_time_txt, y=hdr.step_index_txt, ax=ax8, title="step index") + raw.plot(x=hdr.test_time, y=hdr.potential, ax=ax7, title="voltage") + raw.plot(x=hdr.test_time, y=hdr.step_num, ax=ax8, title="step index") plt.legend() plt.show() @@ -402,7 +402,7 @@ def _check_loader_from_outside_with_get2(): ) print(f"loaded the file - now lets see what we got") raw = c.data.raw - hdr = c.headers_normal + hdr = c.schema.raw print(raw.head()) c.make_step_table() @@ -420,15 +420,15 @@ def _check_loader_from_outside_with_get2(): constrained_layout=True, sharex=True, ) - raw.plot(x=hdr.test_time_txt, y=hdr.voltage_txt, ax=ax1, xlabel="") - raw.plot(x=hdr.test_time_txt, y=hdr.current_txt, ax=ax2, xlabel="") + raw.plot(x=hdr.test_time, y=hdr.potential, ax=ax1, xlabel="") + raw.plot(x=hdr.test_time, y=hdr.current, ax=ax2, xlabel="") raw.plot( - x=hdr.test_time_txt, - y=[hdr.charge_capacity_txt, hdr.discharge_capacity_txt], + x=hdr.test_time, + y=[hdr.cumulative_charge_capacity, hdr.cumulative_discharge_capacity], ax=ax3, xlabel="", ) - raw.plot(x=hdr.test_time_txt, y=hdr.cycle_index_txt, ax=ax4) + raw.plot(x=hdr.test_time, y=hdr.cycle_num, ax=ax4) fig_1.suptitle(f"{name.name}", fontsize=16) n = c.get_number_of_cycles() @@ -473,7 +473,7 @@ def _fix_bugs_now(): ) print(f"loaded the file - now lets see what we got") raw = c.data.raw - hdr = c.headers_normal + hdr = c.schema.raw print(raw.head()) c.make_step_table() steps = c.data.steps @@ -490,15 +490,15 @@ def _fix_bugs_now(): constrained_layout=True, sharex=True, ) - raw.plot(x=hdr.test_time_txt, y=hdr.voltage_txt, ax=ax1, xlabel="") - raw.plot(x=hdr.test_time_txt, y=hdr.current_txt, ax=ax2, xlabel="") + raw.plot(x=hdr.test_time, y=hdr.potential, ax=ax1, xlabel="") + raw.plot(x=hdr.test_time, y=hdr.current, ax=ax2, xlabel="") raw.plot( - x=hdr.test_time_txt, - y=[hdr.charge_capacity_txt, hdr.discharge_capacity_txt], + x=hdr.test_time, + y=[hdr.cumulative_charge_capacity, hdr.cumulative_discharge_capacity], ax=ax3, xlabel="", ) - raw.plot(x=hdr.test_time_txt, y=hdr.cycle_index_txt, ax=ax4) + raw.plot(x=hdr.test_time, y=hdr.cycle_num, ax=ax4) fig_1.suptitle(f"{name.name}", fontsize=16) n = c.get_number_of_cycles() diff --git a/tests/test_batmo.py b/tests/test_batmo.py index 8e816e42..41b33108 100644 --- a/tests/test_batmo.py +++ b/tests/test_batmo.py @@ -12,27 +12,27 @@ def test_load_batmo_bdf(): cycle_mode="anode", ) - hn = c.headers_normal + hn = c.schema.raw # Assert that data is loaded assert not c.data.raw.empty # Check that test time is in seconds (max time should be > 1000s) - max_time = c.data.raw[hn.test_time_txt].max() + max_time = c.data.raw[hn.test_time].max() assert max_time > 1000.0, "Time was not correctly converted to seconds" # Check that step_index is strictly increasing - step_indices = c.data.raw[hn.step_index_txt].unique() + step_indices = c.data.raw[hn.step_num].unique() assert len(step_indices) > 100, "Step indices were not cumulated properly" # Check step index monotonic property assert c.data.raw[ - hn.step_index_txt + hn.step_num ].is_monotonic_increasing, "Step index is not strictly increasing" # Test for missing columns - assert hn.current_txt in c.data.raw.columns - assert hn.voltage_txt in c.data.raw.columns - assert hn.cycle_index_txt in c.data.raw.columns + assert hn.current in c.data.raw.columns + assert hn.potential in c.data.raw.columns + assert hn.cycle_num in c.data.raw.columns assert c._validate_step_table() @@ -54,9 +54,9 @@ def test_batmo_bdf_step_index_is_preprocessed_to_continuous_segments(): cycle_mode="anode", ) - hn = c.headers_normal - hst = c.headers_step_table - si = hn.step_index_txt + hn = c.schema.raw + hst = c.schema.steps + si = hn.step_num loaded = c.data.raw.reset_index(drop=True) pd.testing.assert_series_equal( loaded[si], @@ -66,6 +66,6 @@ def test_batmo_bdf_step_index_is_preprocessed_to_continuous_segments(): assert loaded[si].is_monotonic_increasing assert loaded[si].nunique() == len(c.data.steps) - assert (loaded.groupby(si)[hn.step_time_txt].min() == 0.0).all() - assert {"charge", "discharge"}.issubset(set(c.data.steps[hst.type])) + assert (loaded.groupby(si)[hn.step_time].min() == 0.0).all() + assert {"charge", "discharge"}.issubset(set(c.data.steps[hst.step_type])) assert not c.get_cap(cycle=1, method="forth-and-forth", mode="absolute").empty diff --git a/tests/test_cell_readers.py b/tests/test_cell_readers.py index 7ddfafab..05e16e1d 100644 --- a/tests/test_cell_readers.py +++ b/tests/test_cell_readers.py @@ -53,8 +53,8 @@ def test_raw_bad_data_cycle_and_step(cellpy_data_instance, parameters): cycle = 5 step = 10 step_left = 11 - step_header = cellpy_data_instance.headers_normal.step_index_txt - cycle_header = cellpy_data_instance.headers_normal.cycle_index_txt + step_header = cellpy_data_instance.schema.raw.step_num + cycle_header = cellpy_data_instance.schema.raw.cycle_num cellpy_data_instance.from_raw(parameters.res_file_path, bad_steps=((cycle, step),)) @@ -65,7 +65,7 @@ def test_raw_bad_data_cycle_and_step(cellpy_data_instance, parameters): def test_raw_data_from_data_point(cellpy_data_instance, parameters): - data_point_header = cellpy_data_instance.headers_normal.data_point_txt + data_point_header = cellpy_data_instance.schema.raw.datapoint_num cellpy_data_instance.from_raw(parameters.res_file_path, data_points=(10_000, None)) p1 = cellpy_data_instance.data.raw[data_point_header].iloc[0] @@ -73,7 +73,7 @@ def test_raw_data_from_data_point(cellpy_data_instance, parameters): def test_raw_data_data_point(cellpy_data_instance, parameters): - data_point_header = cellpy_data_instance.headers_normal.data_point_txt + data_point_header = cellpy_data_instance.schema.raw.datapoint_num cellpy_data_instance.from_raw( parameters.res_file_path, data_points=(10_000, 10_200) ) @@ -157,7 +157,7 @@ def test_merge_auto_from_list(parameters): cdi2.from_raw(f2) cdi3.from_raw(files) - dp = cdi1.headers_normal.data_point_txt + dp = cdi1.schema.raw.datapoint_num table_first = cdi1.data.raw.describe() count_first = table_first.loc["count", dp] @@ -181,7 +181,7 @@ def test_print_step_table(dataset): def test_c_rate_calc(dataset): table = dataset.data.steps - unique = table[dataset.headers_step_table.rate_avr].unique() + unique = table[dataset.schema.steps.c_rate].unique() assert len(unique) == 6 @@ -517,14 +517,14 @@ def test_load_step_specs_short( file_name = parameters.short_step_table_file_path assert os.path.isfile(file_name) cellpy_data_instance.load_step_specifications(file_name, short=True) - hst = cellpy_data_instance.headers_step_table + hst = cellpy_data_instance.schema.steps step_table = cellpy_data_instance.data.steps t = step_table.loc[ - (step_table[hst.cycle] == cycle) & (step_table[hst.step] == step), hst.type + (step_table[hst.cycle_num] == cycle) & (step_table[hst.step_num] == step), hst.step_type ].values[0] assert t == expected_type i = step_table.loc[ - (step_table[hst.cycle] == cycle) & (step_table[hst.step] == step), hst.info + (step_table[hst.cycle_num] == cycle) & (step_table[hst.step_num] == step), "info" ].values[0] assert str(i) == expected_info @@ -613,16 +613,16 @@ def test_from_raw_local(cellpy_data_instance, parameters): data_point = 1457 step_time = 1500.05 sum_discharge_time = 362198.12 - hn = cellpy_data_instance.headers_normal - hs = cellpy_data_instance.headers_summary + hn = cellpy_data_instance.schema.raw + hs = cellpy_data_instance.schema.summary my_test = cellpy_data_instance.data summary = my_test.summary print(summary.head().T) # Polars Phase A (#457): keys live in columns, not indexes. - first_cycle = summary.loc[summary[hs.cycle_index] == 1] - assert int(first_cycle[hs.data_point].iloc[0]) == data_point + first_cycle = summary.loc[summary[hs.cycle_num] == 1] + assert int(first_cycle[hs.datapoint_num_last].iloc[0]) == data_point raw_row = my_test.raw.loc[ - my_test.raw[hn.data_point_txt] == 5, hn.step_time_txt + my_test.raw[hn.datapoint_num] == 5, hn.step_time ] assert step_time == pytest.approx(raw_row.iloc[0], 0.1) @@ -768,8 +768,8 @@ def test_make_summary_exclude_step_types(rate_dataset): import pandas as pd c = rate_dataset - hst = c.headers_step_table - hs = c.headers_summary + hst = c.schema.steps + hs = c.schema.summary # compute the step table first (normal workflow): exclude_step_types works on # the step table make_summary sees, so summary and the manual delta below must @@ -779,21 +779,21 @@ def test_make_summary_exclude_step_types(rate_dataset): excl = c.make_summary(exclude_step_types=["cv_"], create_copy=True).data.summary steps = c.data.steps - cv = steps[steps[hst.type].str.startswith("cv_", na=False)] + cv = steps[steps[hst.step_type].str.startswith("cv_", na=False)] assert not cv.empty, "fixture must contain cv_ steps for this test" d_chg = ( - (cv[f"{hst.charge}_last"] - cv[f"{hst.charge}_first"]) - .groupby(cv[hst.cycle]) + (cv[hst.charge_capacity_last] - cv[hst.charge_capacity_first]) + .groupby(cv[hst.cycle_num]) .sum() ) d_dchg = ( - (cv[f"{hst.discharge}_last"] - cv[f"{hst.discharge}_first"]) - .groupby(cv[hst.cycle]) + (cv[hst.discharge_capacity_last] - cv[hst.discharge_capacity_first]) + .groupby(cv[hst.cycle_num]) .sum() ) - b = base.set_index(hs.cycle_index) - e = excl.set_index(hs.cycle_index) + b = base.set_index(hs.cycle_num) + e = excl.set_index(hs.cycle_num) assert list(b.index) == list(e.index) for cyc in b.index: assert e.loc[cyc, hs.charge_capacity] == pytest.approx( @@ -854,20 +854,20 @@ def test_load_cellpyfile(cellpy_data_instance, parameters): data_point = 1457 step_time = 1500.05 sum_test_time = 9301719.457 - hn = cellpy_data_instance.headers_normal - hs = cellpy_data_instance.headers_summary - hst = cellpy_data_instance.headers_step_table + hn = cellpy_data_instance.schema.raw + hs = cellpy_data_instance.schema.summary + hst = cellpy_data_instance.schema.steps my_test = cellpy_data_instance.data unique_cycles = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18] - unique_cycles_read = my_test.steps.loc[:, hst.cycle].unique() + unique_cycles_read = my_test.steps.loc[:, hst.cycle_num].unique() assert any(map(lambda v: v in unique_cycles_read, unique_cycles)) # Polars Phase A (#457): keys live in columns, not indexes. - first_cycle = my_test.summary.loc[my_test.summary[hs.cycle_index] == cycle_number] - assert int(first_cycle[hs.data_point].iloc[0]) == data_point - raw_row = my_test.raw.loc[my_test.raw[hn.data_point_txt] == 5, hn.step_time_txt] + first_cycle = my_test.summary.loc[my_test.summary[hs.cycle_num] == cycle_number] + assert int(first_cycle[hs.datapoint_num_last].iloc[0]) == data_point + raw_row = my_test.raw.loc[my_test.raw[hn.datapoint_num] == 5, hn.step_time] assert step_time == pytest.approx(raw_row.iloc[0], 0.1) assert sum_test_time == pytest.approx( - my_test.summary.loc[:, hs.test_time].sum(), 0.1 + my_test.summary.loc[:, hs.last_test_time].sum(), 0.1 ) diff --git a/tests/test_cell_schema.py b/tests/test_cell_schema.py index 6fcc5bef..e05d35d5 100644 --- a/tests/test_cell_schema.py +++ b/tests/test_cell_schema.py @@ -104,46 +104,23 @@ def test_schema_does_not_warn(native_cell): @pytest.mark.essential @pytest.mark.parametrize( - "legacy_attr, schema_frame, native_attr", - [ - ("headers_normal", "raw", "potential"), - ("headers_step_table", "steps", "cycle_num"), - ("headers_summary", "summary", "charge_capacity"), - ], + "legacy_attr", ["headers_normal", "headers_step_table", "headers_summary"] ) -def test_legacy_shim_and_schema_agree( - native_cell, legacy_attr, schema_frame, native_attr -): - via_schema = getattr(getattr(native_cell.schema, schema_frame), native_attr) - via_shim = getattr(getattr(native_cell, legacy_attr), native_attr) - assert via_schema == via_shim - - -@pytest.mark.essential -def test_legacy_attribute_warns_and_names_the_schema_replacement(native_cell): - _deprecation._WARNED_SITES.clear() - with warnings.catch_warnings(record=True) as caught: - warnings.simplefilter("always") - value = native_cell.headers_normal.voltage_txt - - assert value == native_cell.schema.raw.potential - messages = [ - str(w.message) for w in caught if issubclass(w.category, DeprecationWarning) - ] - assert len(messages) == 1 - # The warning must name the exact attribute to type, not the concept. - assert "c.schema.raw.potential" in messages[0] +def test_legacy_header_attributes_are_removed(native_cell, legacy_attr): + """The 2.0 ``headers_*`` shim was removed in 2.1 (E3, #715); ``c.schema`` + is the replacement, so the attributes no longer exist on the cell.""" + assert not hasattr(native_cell, legacy_attr) + with pytest.raises(AttributeError): + getattr(native_cell, legacy_attr) @pytest.mark.essential def test_core_pipeline_does_not_trip_its_own_deprecation(dataset): - """cellpy's own code must be migrated off ``headers_*`` (#558). + """cellpy's own code is off ``headers_*`` (#558, shim removed in #715). - Guards the failure mode found while migrating: dropping a local - ``hdr = self.headers_step_table`` binding silently re-resolved a later use - to the module-level *legacy* singleton, which indexes a native frame with - legacy names. A warning from inside cellpy means a site is still on the - shim — or has fallen through to the legacy singleton. + Regression guard: the core pipeline must emit no ``headers_*`` deprecation + warning. The shim is gone, so any such warning would mean a reintroduced + legacy-header code path. """ _deprecation._WARNED_SITES.clear() with warnings.catch_warnings(record=True) as caught: diff --git a/tests/test_cellpy_file_roundtrip.py b/tests/test_cellpy_file_roundtrip.py index 3ea1a93d..97b2115f 100644 --- a/tests/test_cellpy_file_roundtrip.py +++ b/tests/test_cellpy_file_roundtrip.py @@ -116,11 +116,11 @@ def test_v8_load_selector_max_cycle_truncates_consistently(): assert selected.limit_data_points == 3119 assert len(selected.data.summary) == max_cycle # Polars Phase A (#457): summary keys live in columns, not the index. - hs = selected.headers_summary - assert selected.data.summary[hs.cycle_index].max() == max_cycle + hs = selected.schema.summary + assert selected.data.summary[hs.cycle_num].max() == max_cycle - hn = selected.headers_normal - cycle_col = hn.cycle_index_txt + hn = selected.schema.raw + cycle_col = hn.cycle_num assert selected.data.raw[cycle_col].max() <= max_cycle assert len(selected.data.raw) < len(full.data.raw) assert len(selected.data.steps) < len(full.data.steps) @@ -134,17 +134,17 @@ def test_legacy_v4_v7_load_shapes_and_columns(label, filename, version): pytest.skip(f"missing legacy fixture: {path}") cell = load_cellpy_file(path, accept_old=True) - hn = cell.headers_normal - hs = cell.headers_summary + hn = cell.schema.raw + hs = cell.schema.summary assert cell.data.raw.shape[0] > 0 assert cell.data.summary.shape[0] > 0 - assert hn.data_point_txt in cell.data.raw.columns - assert hn.cycle_index_txt in cell.data.raw.columns + assert hn.datapoint_num in cell.data.raw.columns + assert hn.cycle_num in cell.data.raw.columns # Polars Phase A (#457): summary keys live in columns for every version. - assert hs.cycle_index in cell.data.summary.columns + assert hs.cycle_num in cell.data.summary.columns assert cell.data.summary.index.name is None - assert hs.data_point in cell.data.summary.columns + assert hs.datapoint_num_last in cell.data.summary.columns assert hs.discharge_capacity in cell.data.summary.columns assert cell.data.meta_common.cellpy_file_version == version diff --git a/tests/test_cellpy_splitting.py b/tests/test_cellpy_splitting.py index 4a3d4bc1..96e22cd3 100644 --- a/tests/test_cellpy_splitting.py +++ b/tests/test_cellpy_splitting.py @@ -82,4 +82,4 @@ def test_with_cycles(cell): picked = [2, 4, 6] c1 = cell.with_cycles(picked) assert list(c1.get_cycle_numbers()) == picked - assert set(c1.data.steps[cell.headers_step_table.cycle].unique()) == set(picked) + assert set(c1.data.steps[cell.schema.steps.cycle_num].unique()) == set(picked) diff --git a/tests/test_cellpycell_add_to_summary.py b/tests/test_cellpycell_add_to_summary.py index 9f5fca14..ed1b83ef 100644 --- a/tests/test_cellpycell_add_to_summary.py +++ b/tests/test_cellpycell_add_to_summary.py @@ -11,7 +11,7 @@ def _summary_cycles(cell) -> pd.Series: """Cycle-index values for each summary row, regardless of whether cycle_index is stored as a column or as the summary's index.""" summary = cell.data.summary - hdrs_cycle = cell.headers_summary.cycle_index + hdrs_cycle = cell.schema.summary.cycle_num if hdrs_cycle in summary.columns: return summary[hdrs_cycle] return pd.Series(summary.index, index=summary.index, name=hdrs_cycle) @@ -20,7 +20,7 @@ def _summary_cycles(cell) -> pd.Series: def test_add_to_summary_last_default(cell): """Default method='last' maps a per-cycle constant onto summary.""" raw = cell.data.raw - hdr_cycle_raw = cell.headers_normal.cycle_index_txt + hdr_cycle_raw = cell.schema.raw.cycle_num raw["fake_temp"] = raw[hdr_cycle_raw] * 10.0 @@ -39,7 +39,7 @@ def test_add_to_summary_first_vs_last(cell): """For cycles with more than one raw row, first < last for a monotonic counter.""" raw = cell.data.raw - hdr_cycle_raw = cell.headers_normal.cycle_index_txt + hdr_cycle_raw = cell.schema.raw.cycle_num raw["counter"] = np.arange(len(raw), dtype=float) @@ -61,7 +61,7 @@ def test_add_to_summary_first_vs_last(cell): def test_add_to_summary_mean_matches_pandas(cell): """method='mean' matches a manual groupby+mean+map.""" raw = cell.data.raw - hdr_cycle_raw = cell.headers_normal.cycle_index_txt + hdr_cycle_raw = cell.schema.raw.cycle_num raw["fake_temp"] = raw[hdr_cycle_raw].astype(float) + np.arange( len(raw), dtype=float @@ -80,7 +80,7 @@ def test_add_to_summary_mean_matches_pandas(cell): def test_add_to_summary_new_name(cell): """new_name renames the resulting summary column.""" raw = cell.data.raw - hdr_cycle_raw = cell.headers_normal.cycle_index_txt + hdr_cycle_raw = cell.schema.raw.cycle_num raw["fake_temp"] = raw[hdr_cycle_raw] * 1.0 cell.add_to_summary("fake_temp", new_name="cell_temperature") @@ -93,7 +93,7 @@ def test_add_to_summary_new_name(cell): def test_add_to_summary_chainable(cell): """The method returns the same CellpyCell instance.""" raw = cell.data.raw - hdr_cycle_raw = cell.headers_normal.cycle_index_txt + hdr_cycle_raw = cell.schema.raw.cycle_num raw["fake_temp"] = raw[hdr_cycle_raw] * 1.0 result = cell.add_to_summary("fake_temp") @@ -107,7 +107,7 @@ def test_add_to_summary_unknown_column_raises(cell): def test_add_to_summary_unknown_method_raises(cell): raw = cell.data.raw - hdr_cycle_raw = cell.headers_normal.cycle_index_txt + hdr_cycle_raw = cell.schema.raw.cycle_num raw["fake_temp"] = raw[hdr_cycle_raw] * 1.0 with pytest.raises(ValueError, match="method must be one of"): diff --git a/tests/test_cv_partition.py b/tests/test_cv_partition.py index c87d0ad3..3ea7de31 100644 --- a/tests/test_cv_partition.py +++ b/tests/test_cv_partition.py @@ -60,7 +60,7 @@ def test_partition_summary_cv_steps_cc_only_has_zero_cv(figure_cell): """Golden CC-only figure cell: with-CV ≈ 0, without-CV ≈ all.""" cycle = _summary_cycle_column(figure_cell.data.summary) # Prefer gravimetric if present (summary_plot family), else raw. - hs = figure_cell.headers_summary + hs = figure_cell.schema.summary col = hs.charge_capacity + "_gravimetric" if col not in figure_cell.data.summary.columns: col = "charge_capacity" diff --git a/tests/test_exporters_bdf.py b/tests/test_exporters_bdf.py index 7eb10a0c..9e2aca56 100644 --- a/tests/test_exporters_bdf.py +++ b/tests/test_exporters_bdf.py @@ -11,7 +11,7 @@ from cellpy import log from cellpy.exporters import to_bdf from cellpy.exporters import bdf as bdf_module -from cellpy.parameters.internal_settings import CellpyUnits, get_headers_normal +from cellpy.parameters.internal_settings import CellpyUnits log.setup_logging(default_level=logging.DEBUG, testing=True) @@ -24,26 +24,25 @@ def _make_synthetic_cell(*, with_capacity: bool = True, with_datetime: bool = Tr """ from cellpy import cellreader - # Name the fabricated raw via the cell's own headers so the frame matches - # the runtime schema (native after the flip; the shim resolves the legacy - # *_txt attrs to the native column names). + # Name the fabricated raw via the cell's native schema so the frame matches + # the runtime column names the bdf exporter expects. cell = cellreader.CellpyCell(initialize=True) - headers = cell.headers_normal + headers = cell.schema.raw n = 6 raw = pd.DataFrame( { - headers.test_time_txt: [0.0, 1.0, 2.0, 3.0, 4.0, 5.0], - headers.voltage_txt: [3.0, 3.1, 3.2, 3.3, 3.4, 3.5], - headers.current_txt: [0.1, 0.1, -0.1, -0.1, 0.0, 0.0], - headers.cycle_index_txt: [1, 1, 2, 2, 3, 3], - headers.step_index_txt: [1, 2, 1, 2, 1, 2], + headers.test_time: [0.0, 1.0, 2.0, 3.0, 4.0, 5.0], + headers.potential: [3.0, 3.1, 3.2, 3.3, 3.4, 3.5], + headers.current: [0.1, 0.1, -0.1, -0.1, 0.0, 0.0], + headers.cycle_num: [1, 1, 2, 2, 3, 3], + headers.step_num: [1, 2, 1, 2, 1, 2], } ) if with_capacity: - raw[headers.charge_capacity_txt] = [0.0, 100.0, 100.0, 0.0, 0.0, 100.0] - raw[headers.discharge_capacity_txt] = [0.0, 0.0, 0.0, 100.0, 0.0, 0.0] + raw[headers.cumulative_charge_capacity] = [0.0, 100.0, 100.0, 0.0, 0.0, 100.0] + raw[headers.cumulative_discharge_capacity] = [0.0, 0.0, 0.0, 100.0, 0.0, 0.0] if with_datetime: - raw[headers.datetime_txt] = pd.to_datetime( + raw["date_time"] = pd.to_datetime( [ "2024-01-01 00:00:00", "2024-01-01 00:00:01", @@ -155,8 +154,8 @@ def test_last_cycle_filter(tmp_path: Path) -> None: def test_missing_required_raises(tmp_path: Path) -> None: cell = _make_synthetic_cell() - headers = cell.headers_normal - cell.data.raw = cell.data.raw.drop(columns=[headers.voltage_txt]) + headers = cell.schema.raw + cell.data.raw = cell.data.raw.drop(columns=[headers.potential]) with pytest.raises(ValueError, match="Voltage / V"): cell.to_bdf(tmp_path / "out.bdf.csv") @@ -291,16 +290,16 @@ def test_extras_unknown_column_warns_and_skips(tmp_path: Path, caplog) -> None: def test_extras_skips_columns_already_in_bdf_map(tmp_path: Path) -> None: """Naming a mapped raw column under ``extras`` must not duplicate it.""" cell = _make_synthetic_cell() - headers = cell.headers_normal + headers = cell.schema.raw out = cell.to_bdf( tmp_path / "out.bdf.csv", header_style="machine", - extras=[headers.voltage_txt], + extras=[headers.potential], ) df = pd.read_csv(out) - assert headers.voltage_txt not in df.columns + assert headers.potential not in df.columns assert "voltage_volt" in df.columns assert (df.columns == "voltage_volt").sum() == 1 diff --git a/tests/test_goldens.py b/tests/test_goldens.py index 5592b07b..2c4223d2 100644 --- a/tests/test_goldens.py +++ b/tests/test_goldens.py @@ -40,8 +40,8 @@ def _run_pipeline_smoke() -> tuple[pd.DataFrame, pd.DataFrame, str]: cell.make_summary() summary = cell.data.summary.reset_index(drop=True) steps = cell.data.steps.reset_index(drop=True) - # summary per-cycle datapoint column (native name via the header shim) - return summary, steps, cell.headers_summary.data_point + # summary per-cycle datapoint column (native name) + return summary, steps, cell.schema.summary.datapoint_num_last @pytest.mark.essential diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 66554ed1..b4acb4e7 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -139,7 +139,7 @@ def test_select_summary_based_on_rate(cell): def test_remove_outliers_on_index(cell): # Polars Phase A (#457): cycle numbers live in the cycle-number column. - hdr_cycle = cell.headers_summary.cycle_index + hdr_cycle = cell.schema.summary.cycle_num last = cell.get_cycle_numbers()[-1] s1 = helpers.remove_outliers_from_summary_on_index(cell.data.summary, indexes=[15]) s2 = helpers.remove_outliers_from_summary_on_index( diff --git a/tests/test_legacy_header_shim.py b/tests/test_legacy_header_shim.py deleted file mode 100644 index b666f0d0..00000000 --- a/tests/test_legacy_header_shim.py +++ /dev/null @@ -1,131 +0,0 @@ -"""Tests for the D6 legacy-header attribute shim (native-headers flip, Stage 2). - -The shim is not wired into ``CellpyCell`` yet (Stage 5), so these test it -directly against a native ``config.default_schema()``. -""" - -from __future__ import annotations - -import warnings - -import pytest -from cellpycore.config import default_schema - -from cellpy import _deprecation -from cellpy.parameters.legacy_header_shim import ( - LegacyHeaderShim, - build_legacy_shims, -) - - -@pytest.fixture -def shims(): - return build_legacy_shims(default_schema()) - - -def _count_dep_warnings(fn): - # warn_once dedups per call site globally; clear it so the count reflects - # this call only (the wired runtime warms sites in other tests). - _deprecation._WARNED_SITES.clear() - with warnings.catch_warnings(record=True) as caught: - warnings.simplefilter("always") - value = fn() - n = sum(1 for w in caught if issubclass(w.category, DeprecationWarning)) - return value, n - - -def test_build_legacy_shims_frames(shims): - assert set(shims) == {"headers_normal", "headers_step_table", "headers_summary"} - assert all(isinstance(s, LegacyHeaderShim) for s in shims.values()) - - -def test_raw_renamed_attributes_resolve_and_warn(shims): - hn = shims["headers_normal"] - assert _count_dep_warnings(lambda: hn.voltage_txt) == ("potential", 1) - assert _count_dep_warnings(lambda: hn.cycle_index_txt) == ("cycle_num", 1) - assert _count_dep_warnings(lambda: hn.data_point_txt) == ("datapoint_num", 1) - - -def test_step_and_summary_renamed_attributes_resolve(shims): - hst = shims["headers_step_table"] - hs = shims["headers_summary"] - assert _count_dep_warnings(lambda: hst.cycle) == ("cycle_num", 1) - assert _count_dep_warnings(lambda: hst.voltage) == ("potential", 1) - assert _count_dep_warnings(lambda: hs.cycle_index) == ("cycle_num", 1) - - -def test_unchanged_names_pass_through_without_warning(shims): - # native names (and legacy names that equal the native name) do not warn. - hn = shims["headers_normal"] - hs = shims["headers_summary"] - assert _count_dep_warnings(lambda: hn.potential) == ("potential", 0) - assert _count_dep_warnings(lambda: hn.current) == ("current", 0) - assert _count_dep_warnings(lambda: hs.charge_capacity) == ("charge_capacity", 0) - - -def test_summary_specific_column_by_key_composes(shims): - hs = shims["headers_summary"] - value, _ = _count_dep_warnings(lambda: hs["charge_capacity_gravimetric"]) - assert value == "charge_capacity_gravimetric" - value2, _ = _count_dep_warnings(lambda: hs["discharge_capacity_areal"]) - assert value2 == "discharge_capacity_areal" - - -def test_duplicate_value_pair_resolves_and_warns(shims): - hs = shims["headers_summary"] - # charge_capacity_raw shares the "charge_capacity" column value. - assert _count_dep_warnings(lambda: hs.discharge_capacity_raw) == ( - "discharge_capacity", - 1, - ) - assert _count_dep_warnings(lambda: hs.charge_capacity_raw) == ( - "charge_capacity", - 1, - ) - - -def test_getitem_matches_getattr(shims): - hn = shims["headers_normal"] - assert hn["voltage_txt"] == "potential" - assert hn["voltage_txt"] == hn.voltage_txt - - -def test_legacy_only_attribute_resolves_to_unchanged_name(shims): - # Legacy-only columns are not renamed by the flip (to_native passes them - # through), so the shim returns their unchanged legacy name (no warning). - hn = shims["headers_normal"] - hst = shims["headers_step_table"] - hs = shims["headers_summary"] - assert _count_dep_warnings(lambda: hn.power_txt) == ("power", 0) - assert _count_dep_warnings(lambda: hn.datetime_txt) == ("date_time", 0) - assert _count_dep_warnings(lambda: hn.test_id_txt) == ("test_id", 0) - assert _count_dep_warnings(lambda: hst.info) == ("info", 0) - assert _count_dep_warnings(lambda: hst.ustep) == ("ustep", 0) - assert _count_dep_warnings(lambda: hs.shifted_charge_capacity) == ( - "shifted_charge_capacity", - 0, - ) - - -def test_unknown_attribute_raises(shims): - with pytest.raises(AttributeError): - _ = shims["headers_normal"].not_a_real_attr - - -def test_warns_once_per_attribute(shims): - _deprecation._WARNED_SITES.clear() - hn = shims["headers_normal"] - with warnings.catch_warnings(record=True) as caught: - warnings.simplefilter("always") - hn.voltage_txt - hn.voltage_txt - hn.voltage_txt - voltage_warnings = [ - w for w in caught if "headers_normal.voltage_txt" in str(w.message) - ] - assert len(voltage_warnings) == 1 - - -def test_bad_frame_rejected(): - with pytest.raises(ValueError, match="unknown frame"): - LegacyHeaderShim("nonsense", default_schema().raw) diff --git a/tests/test_ocv_relax.py b/tests/test_ocv_relax.py index 1445b111..43609a48 100644 --- a/tests/test_ocv_relax.py +++ b/tests/test_ocv_relax.py @@ -12,16 +12,12 @@ def test_get_ocv_rlx_for_fitting(dataset): import matplotlib.pyplot as plt raw = dataset.data.raw - for h, v in dataset.headers_normal.items(): - print(f"{h}: {v}", end=" -> ") - if v in raw.columns: - print("Exists") - else: - print("MISSING") + for v in raw.columns: + print(f"{v}: Exists") steps = dataset.data.steps - hdr_s = dataset.headers_step_table - n = steps.loc[steps[hdr_s.cycle].isin([1]), :] - n = n.loc[n[hdr_s.type].str.startswith("ocvrlx_up"), :] + hdr_s = dataset.schema.steps + n = steps.loc[steps[hdr_s.cycle_num].isin([1]), :] + n = n.loc[n[hdr_s.step_type].str.startswith("ocvrlx_up"), :] print(n) rlx = dataset.get_ocv(direction="up", cycles=1) print(rlx) diff --git a/tests/test_plotutils_summary_plot.py b/tests/test_plotutils_summary_plot.py index 2833703c..622660cb 100644 --- a/tests/test_plotutils_summary_plot.py +++ b/tests/test_plotutils_summary_plot.py @@ -365,7 +365,7 @@ class TestSummaryPlotFiltersAndRate: introduced in issue #363.""" def _rate_cols(self, cell): - h = cell.headers_summary + h = cell.schema.summary return h.charge_c_rate, h.discharge_c_rate def test_filters_rate_range_drops_rows(self, cell): @@ -490,8 +490,8 @@ class TestSummaryPlotHoverColumns: def test_hover_columns_added(self, cell): """hover_columns survives the melt and reaches the plotly hover.""" - hdr = cell.headers_summary - extras = [hdr.test_time, hdr.data_point] + hdr = cell.schema.summary + extras = [hdr.last_test_time, hdr.datapoint_num_last] fig, data = summary_plot( cell, @@ -515,19 +515,19 @@ def test_hover_columns_added(self, cell): def test_hover_columns_unknown_warns(self, cell, caplog): """Unknown hover columns are dropped with a warning, not raised.""" - hdr = cell.headers_summary + hdr = cell.schema.summary with caplog.at_level(logging.WARNING): fig, data = summary_plot( cell, y="capacities_gravimetric", - hover_columns=[hdr.test_time, "definitely_not_a_real_column"], + hover_columns=[hdr.last_test_time, "definitely_not_a_real_column"], return_data=True, backend="plotly", show_formation=False, ) assert fig is not None - assert hdr.test_time in data.columns + assert hdr.last_test_time in data.columns assert "definitely_not_a_real_column" not in data.columns assert any( "definitely_not_a_real_column" in rec.getMessage() @@ -536,12 +536,12 @@ def test_hover_columns_unknown_warns(self, cell, caplog): def test_hover_columns_ignored_for_fullcell(self, cell, caplog): """fullcell_standard_* is out of scope: warn and continue.""" - hdr = cell.headers_summary + hdr = cell.schema.summary with caplog.at_level(logging.WARNING): fig = summary_plot( cell, y="fullcell_standard_gravimetric", - hover_columns=[hdr.test_time], + hover_columns=[hdr.last_test_time], backend="plotly", show_formation=False, ) diff --git a/tests/test_slim.py b/tests/test_slim.py index 54345eb2..947f5587 100644 --- a/tests/test_slim.py +++ b/tests/test_slim.py @@ -31,8 +31,8 @@ def test_core_seam_wired(cpi): """CellpyCell exposes a cellpy-core OldCellpyCellCore as ``self.core``.""" assert isinstance(cpi.core, OldCellpyCellCore) # legacy headers/units are restored by the bridge - assert cpi.core.raw_cols.charge_capacity_txt == cpi.headers_normal.charge_capacity_txt - assert cpi.core.cycle_cols.charge_capacity == cpi.headers_summary.charge_capacity + assert cpi.core.raw_cols.charge_capacity_txt == cpi.schema.raw.cumulative_charge_capacity + assert cpi.core.cycle_cols.charge_capacity == cpi.schema.summary.charge_capacity def test_data_ownership_in_core(cpi): @@ -53,7 +53,7 @@ def test_make_summary_through_core(cpi, parameters): cpi.make_summary(find_ir=True, find_end_voltage=True) summary = cpi.data.summary - h = cpi.headers_summary + h = cpi.schema.summary assert summary is not None assert not summary.empty @@ -67,8 +67,8 @@ def test_make_summary_through_core(cpi, parameters): assert col in summary.columns # golden value (matches the legacy/pre-seam summary; see test_from_raw_local) # Polars Phase A (#457): look up by the cycle_index column, not the index. - first_cycle = summary.loc[summary[h.cycle_index] == 1] - assert int(first_cycle[h.data_point].iloc[0]) == 1457 + first_cycle = summary.loc[summary[h.cycle_num] == 1] + assert int(first_cycle[h.datapoint_num_last].iloc[0]) == 1457 def test_make_summary_save_roundtrip(cpi, parameters, tmp_path): @@ -93,7 +93,7 @@ def test_direct_core_make_core_summary(cpi, parameters): data = cpi.data data = cpi.core.make_core_summary(data, find_ir=True, find_end_voltage=True) - h = cpi.headers_summary + h = cpi.schema.summary assert data.summary is not None assert not data.summary.empty assert h.charge_capacity in data.summary.columns