Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions cellpy/collect/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
A collection is a product, not a side effect: :class:`Collection` = a tidy frame
plus provenance. Built on ``cellpy.batch.aggregate`` (Epic A), replacing the
``utils/collectors`` "elevated arguments" machinery and fixing the cross-cell
cycle-narrowing bug by design. ``cellpy.utils.collectors`` becomes a shim (B3).
cycle-narrowing bug by design. ``cellpy.utils.collectors`` is now a thin shim
whose legacy ``Batch*Collector`` family is removed in 2.1 (#708).

Arcs: options/collection/collect_summaries + per-cell curves (#705, this arc);
rate/group pipeline (#706); convenience class + shims (#707); plotting (#708).
Arcs: options/collection/collect_summaries + per-cell curves (#705); rate/group
pipeline (#706); convenience class + recipes (#707); ICA collection + plotting
handover (Collection.plot -> cellpy.plotting) + collectors shim (#708).
"""

from __future__ import annotations
Expand All @@ -16,11 +18,13 @@
from cellpy.collect.collector import (
BatchCollector,
cycles_collector,
ica_collector,
normalize_column,
standard_gravimetric,
summary_collector,
)
from cellpy.collect.curves import collect_cycles
from cellpy.collect.ica import collect_ica
from cellpy.collect.options import (
CurveOptions,
IcaOptions,
Expand All @@ -35,6 +39,7 @@
"load_collection",
"collect_summaries",
"collect_cycles",
"collect_ica",
"iter_cells",
"CellItem",
"SummaryOptions",
Expand All @@ -44,6 +49,7 @@
"BatchCollector",
"summary_collector",
"cycles_collector",
"ica_collector",
"standard_gravimetric",
"normalize_column",
]
19 changes: 19 additions & 0 deletions cellpy/collect/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,31 @@ class Collection:
name: str
meta: CollectionMeta

#: collection kind -> ``collected_plot`` family (#657).
_FAMILY = {"summary": "summary", "cycles": "cycles", "ica": "ica"}

def to_wide(
self, values: str, index: str = "cycle_num", columns: str = "cell"
) -> pl.DataFrame:
"""Explicit, tested pivot to wide layout (replaces the try/except pivots)."""
return self.data.pivot(values=values, index=index, on=columns)

def plot(self, *, family_kind: str | None = None, **kwargs):
"""Draw the collection via :func:`cellpy.plotting.collected_plot`.

The drawing lives in ``cellpy.plotting`` (#657); a collection just hands
it the tidy frame and the family. The only reconciliation needed is the
summary family's cycle column, which the summary plotter spells
``cycle`` (capacity/ICA curves keep ``cycle_num`` / ``cycle``).
"""
from cellpy.plotting import collected_plot

family = family_kind or self._FAMILY.get(self.kind, "cycles")
frame = self.data.to_pandas()
if family == "summary" and "cycle_num" in frame.columns:
frame = frame.rename(columns={"cycle_num": "cycle"})
return collected_plot(frame, family_kind=family, **kwargs)

def save(
self,
directory: Path | str | None = None,
Expand Down
10 changes: 9 additions & 1 deletion cellpy/collect/collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
import polars as pl

from cellpy.collect.curves import collect_cycles
from cellpy.collect.options import CurveOptions, SummaryOptions
from cellpy.collect.ica import collect_ica
from cellpy.collect.options import CurveOptions, IcaOptions, SummaryOptions
from cellpy.collect.summary import collect_summaries

#: A collect function takes ``(batch, options, **overrides)`` -> Collection.
Expand Down Expand Up @@ -109,6 +110,13 @@ def cycles_collector(
return BatchCollector(batch, collect_cycles, options, autorun=autorun, **overrides)


def ica_collector(
batch: Any, options: IcaOptions | None = None, *, autorun: bool = True, **overrides
) -> BatchCollector:
"""Convenience :class:`BatchCollector` bound to :func:`collect_ica`."""
return BatchCollector(batch, collect_ica, options, autorun=autorun, **overrides)


def normalize_column(
column: str, norm_factor: float, out: str | None = None
) -> Callable[[pl.DataFrame], pl.DataFrame]:
Expand Down
93 changes: 93 additions & 0 deletions cellpy/collect/ica.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""dQ/dV (ICA) collection (collectors redesign, #708).

Per-cell dQ/dV curves via :func:`cellpy.utils.ica.dqdv`, concatenated into one
tidy frame with ``cell`` / ``group`` / ``sub_group`` keys. Mirrors
:func:`cellpy.collect.curves.collect_cycles` -- including the per-cell cycle
isolation that fixes the legacy cross-cell narrowing bug (collectors.py:1691) --
but emits the specced ICA frame (#566): ``cycle, direction, voltage, capacity,
dqdv`` (+ the deprecated ``dq`` duplicate until 2.1).
"""

from __future__ import annotations

from typing import Any

import polars as pl

from cellpy.collect.cells import iter_cells
from cellpy.collect.collection import Collection, CollectionMeta
from cellpy.collect.options import IcaOptions


def _as_polars(frame: Any) -> pl.DataFrame | None:
if frame is None:
return None
if isinstance(frame, pl.DataFrame):
return frame
try:
return pl.from_pandas(frame)
except (TypeError, ValueError):
return None


def collect_ica(
batch: Any, options: IcaOptions | None = None, **overrides
) -> Collection:
"""Collect dQ/dV (incremental capacity) curves per cell into one Collection.

Cycle selection is derived per cell from the *originally requested* cycles
every iteration, so a cell missing a cycle never narrows the request for
the cells after it.
"""
from cellpy.utils import ica

opts = options or IcaOptions()
if overrides:
opts = opts.replace(**overrides)
requested = tuple(opts.cycles) if opts.cycles is not None else None

dqdv_kwargs: dict[str, Any] = {}
if opts.voltage_resolution is not None:
dqdv_kwargs["voltage_resolution"] = opts.voltage_resolution

frames: list[pl.DataFrame] = []
for item in iter_cells(batch):
cell = item.cell
if requested is None:
cycles = None
else:
available = set(cell.get_cycle_numbers())
cycles = [c for c in requested if c in available]
if not cycles:
continue

curve = _as_polars(ica.dqdv(cell, cycles=cycles, **dqdv_kwargs))
if curve is None or curve.height == 0:
continue
frames.append(
curve.with_columns(
pl.lit(item.label).alias("cell"),
pl.lit(item.group).alias("group"),
pl.lit(item.sub_group).alias("sub_group"),
)
)

data = pl.concat(frames, how="diagonal_relaxed") if frames else pl.DataFrame()
for transform in opts.transforms:
data = transform(data)

meta = CollectionMeta(
kind="ica",
batch_name=batch.journal.name,
options={
"cycles": list(requested) if requested else None,
"voltage_resolution": opts.voltage_resolution,
},
cells_included=list(batch.cells),
)
return Collection(
data=data,
kind="ica",
name=f"{batch.journal.name or 'batch'}_ica",
meta=meta,
)
Loading
Loading