Skip to content

ICA and DVA

Incremental capacity analysis (dQ/dV) and differential voltage analysis (dV/dQ).

from cellpy import ica

frame = ica.dqdv(c)                      # cycle, direction, voltage, capacity, dqdv
frame = ica.dvdq(c, direction="charge")  # cycle, direction, capacity, voltage, dvdq

Both verbs accept the same three kinds of source — a CellpyCell, a curve frame from get_cap, or a bare (voltage, capacity) pair — and the same IcaOptions recipe.

ica

Incremental capacity analysis (dQ/dV) and differential voltage analysis (dV/dQ).

Two public verbs over one pure core:

from cellpy import ica

frame = ica.dqdv(c)                       # cycle, direction, voltage, capacity, dqdv
frame = ica.dvdq(c, direction="charge")   # cycle, direction, capacity, voltage, dvdq

The recipe is unchanged from cellpy 1.x — interpolate V(q), optionally smooth, invert to q(V), differentiate, optionally smooth again, optionally normalize — but it now lives in stateless functions ([transform_half_cycle][]) configured by a single frozen [IcaOptions][] object rather than ~20 keyword arguments tunnelled through four entry points.

What changed in 2.0 (see DEPRECATIONS.md and the migration guide):

  • The output frame is specced: always long format, always the same columns, with direction spelled "charge"/"discharge" instead of the old ±1 code whose meaning depended on cycle_mode.
  • The incremental-capacity column is named dqdv. The old name dq is kept as a duplicate column for one release.
  • dvdq() is new. cellpy could not compute differential voltage analysis at all before, even though the pipeline already built the smoothed V(q) representation it needs.
  • Half-cycles that fail are reported, not silently replaced by empty arrays.
  • Converter, dqdv_cycle, dqdv_cycles, dqdv_np, dqdv(split=True) and dqdv(tidy=False) are deprecated shims over the new core. They reproduce the 1.x numbers bit-for-bit (tests/data/goldens/ica_dqdv_*) and go away in 2.1.

scipy stays on this side of the cellpy/cellpycore boundary: cellpycore is scipy-free, and the ICA math is interpolation and filtering, not frame algebra.

Converter

Converter(capacity=None, voltage=None, points_pr_split=10, max_points=None, voltage_resolution=None, capacity_resolution=None, minimum_splits=3, interpolation_method='linear', increment_method='diff', pre_smoothing=False, smoothing=False, post_smoothing=True, normalize=True, normalizing_factor=None, normalizing_roof=None, savgol_filter_window_divisor_default=50, savgol_filter_window_order=3, voltage_fwhm=0.01, gaussian_order=0, gaussian_mode='reflect', gaussian_cval=0.0, gaussian_truncate=4.0)

Deprecated staged dQ/dV converter.

Use transform_half_cycle instead: it takes the same recipe as an IcaOptions and returns its derived quantities rather than writing them back onto itself.

The five stages (set → inspect → pre-process → increment → post-process) are kept because they are how the 1.x characterization tests drive the pipeline, but each now delegates to the pure functions above.

Hidden state

inspect_data overwrites normalizing_factor from the data, so a converter reused across half-cycles carries the previous one's normalization into the next. That behaviour is preserved here for compatibility; the new core returns the factor instead.

increment_data

increment_data()

Perform the dq-dv transform.

inspect_data

inspect_data(capacity=None, voltage=None, err_est=False, diff_est=False)

Check and inspect the data.

post_process_data

post_process_data(voltage=None, incremental_capacity=None, voltage_step=None)

Smooth, normalize and optionally re-grid the finished derivative.

pre_process_data

pre_process_data()

Interpolate V(q), optionally pre-smoothed.

set_data

set_data(capacity, voltage=None, capacity_label='q', voltage_label='v')

Set the data.

GaussianOptions dataclass

GaussianOptions(order: int = 0, mode: str = 'reflect', cval: float = 0.0, truncate: float = 4.0)

Parameters passed straight through to scipy.ndimage.gaussian_filter1d.

HalfCycleResult dataclass

HalfCycleResult(x: ndarray, y: ndarray, partner: ndarray, derivative: str, normalizing_factor: float, post_smoothing_applied: bool, notes: tuple[str, ...] = ())

Output of one half-cycle transform.

Attributes:

  • x (ndarray) –

    The abscissa — voltage for dqdv, capacity for dvdq.

  • y (ndarray) –

    The derivative itself.

  • partner (ndarray) –

    The other coordinate at the same points, so ICA and DVA curves can be cross-plotted. Capacity for dqdv, voltage for dvdq.

  • derivative (str) –

    "dqdv" or "dvdq".

  • normalizing_factor (float) –

    The factor actually used. In 1.x this was written back onto the Converter, so a reused converter silently carried one cycle's normalization into the next; it is returned here instead.

  • post_smoothing_applied (bool) –

    Whether gaussian post-smoothing survived — it is dropped when it raises (see notes).

  • notes (tuple[str, ...]) –

    Human-readable record of anything the transform had to work around.

IcaCols dataclass

IcaCols(cycle: str = 'cycle', direction: str = 'direction', voltage: str = 'voltage', capacity: str = 'capacity', dqdv: str = 'dqdv', dvdq: str = 'dvdq', legacy_dqdv: str = 'dq')

Column names of the specced ICA/DVA output frames.

This is a data contract: users index the returned frame by these names, so it is versioned with the package rather than left to each entry point.

ordered_names

ordered_names(derivative: str = 'dqdv') -> list[str]

Column order for the given derivative.

IcaOptions dataclass

IcaOptions(voltage_resolution: float | None = None, capacity_resolution: float | None = None, max_points: int | None = None, interpolation_method: str = 'linear', pre_smoothing: bool = False, diff_smoothing: bool = False, post_smoothing: bool = True, savgol_window_divisor: int = 50, savgol_order: int = 3, voltage_fwhm: float = 0.01, capacity_fwhm: float | None = None, gaussian: GaussianOptions = GaussianOptions(), normalize: Literal['area'] | bool = 'area', normalizing_factor: float | None = None, normalizing_roof: float | None = None, increment_method: Literal['diff'] = 'diff')

The complete recipe for one dQ/dV or dV/dQ transform.

One definition, one docstring. In 1.x these ~20 parameters could arrive as Converter constructor arguments, as **kwargs tunnelled through any of four entry points, or as dqdv_np's 17 explicit parameters — with the same option spelled smoothing in one place and diff_smoothing in another. Now there is one object.

Attributes:

  • voltage_resolution (float | None) –

    Voltage step for the q(V) interpolation, e.g. 0.005. None keeps the point count of the input.

  • capacity_resolution (float | None) –

    Capacity step for the V(q) interpolation. Ignored when max_points is set.

  • max_points (int | None) –

    Cap on the V(q) interpolation grid. Takes precedence over capacity_resolution.

  • interpolation_method (str) –

    Any kind accepted by scipy.interpolate.interp1d.

  • pre_smoothing (bool) –

    Savitzky-Golay smoothing of V(q) before inversion.

  • diff_smoothing (bool) –

    Savitzky-Golay smoothing of q(V) before differentiating. (1.x called this smoothing on Converter and diff_smoothing on dqdv_np.)

  • post_smoothing (bool) –

    Gaussian smoothing of the finished derivative.

  • savgol_window_divisor (int) –

    Window length divisor for both Savitzky-Golay passes; the window is clamped to at least 3 points and forced odd.

  • savgol_order (int) –

    Polynomial order for both Savitzky-Golay passes.

  • voltage_fwhm (float) –

    Full width at half maximum, in volts, of the gaussian post-smoothing — used when differentiating along voltage (dQ/dV).

  • capacity_fwhm (float | None) –

    The dV/dQ analogue, in capacity units. None derives it as 1% of the capacity span of the half-cycle, which is the same fraction the default voltage_fwhm of 0.01 V represents on a typical ~1 V window.

  • gaussian (GaussianOptions) –

    Remaining gaussian_filter1d parameters.

  • normalize (Literal['area'] | bool) –

    "area" scales the curve so its integral equals the normalizing factor; False leaves it in physical units. 1.x spelled "area" as True, which is still accepted.

  • normalizing_factor (float | None) –

    Target for normalize="area". None uses the half-cycle's own end capacity.

  • normalizing_roof (float | None) –

    Rescales the normalizing factor by end_capacity / normalizing_roof — the hook for normalizing a whole series to one nominal capacity.

  • increment_method (Literal['diff']) –

    Only "diff". The half-finished "hist" binning method from 1.x is reachable through the deprecated Converter only; see cellpy#566.

replace

replace(**overrides: Any) -> 'IcaOptions'

Return a copy with overrides applied (validated).

dqdv

dqdv(source, cycles=None, direction: str = BOTH, options: IcaOptions | None = None, *, strict: bool = False, cycle_mode: str | None = None, number_of_points: int | None = None, **overrides) -> pd.DataFrame

Incremental capacity analysis: dQ/dV against voltage.

Parameters:

  • source

    A CellpyCell, a curve frame from get_cap(categorical_column=True, label_cycle_number=True), or a (voltage, capacity) pair of arrays.

  • cycles

    Cycle number or list of cycle numbers. None processes all.

  • direction (str, default: BOTH ) –

    "charge", "discharge" or "both". Replaces 1.x's split=True, which returned two frames of a different shape.

  • options (IcaOptions | None, default: None ) –

    An IcaOptions. Defaults to IcaOptions().

  • strict (bool, default: False ) –

    Raise instead of warning when a half-cycle fails.

  • cycle_mode (str | None, default: None ) –

    Overrides the cell's own cycle_mode, which decides whether the first half-cycle of each cycle is a charge or a discharge.

  • number_of_points (int | None, default: None ) –

    Passed to the curve extraction.

  • **overrides

    Individual IcaOptions fields, for the common case of changing one thing.

Returns:

  • DataFrame

    A long frame with columns cycle, direction, voltage,

  • DataFrame

    capacity, dqdv - plus a deprecated duplicate dq column that

  • DataFrame

    goes away in 2.1. frame.attrs carries the options used, the

  • DataFrame

    resolved cycle mode, and any per-half-cycle failures.

Example

frame = dqdv(c, cycles=[1, 2], voltage_resolution=0.005) charge = frame[frame.direction == "charge"]

dqdv_cycle

dqdv_cycle(cycle_df, splitter=True, label_direction=False, **kwargs)

Deprecated. Use dqdv with a curve frame.

Returns a tuple of numpy arrays rather than a frame, and reports failure by substituting empty arrays.

Parameters:

  • cycle_df

    One cycle ('potential', 'capacity', 'direction' as ±1).

  • splitter

    Insert a NaN row between the two half-cycles.

  • label_direction

    Also return the ±1 direction array.

dqdv_cycles

dqdv_cycles(cycles_df, not_merged=False, label_direction=False, **kwargs)

Deprecated. Use dqdv with a curve frame.

Parameters:

  • cycles_df

    Curve frame with a cycle-number column.

  • not_merged

    Return (cycle_numbers, frames) instead of one frame.

  • label_direction

    Include the ±1 direction column.

dqdv_np

dqdv_np(voltage, capacity, voltage_resolution=None, capacity_resolution=None, voltage_fwhm=0.01, pre_smoothing=True, diff_smoothing=False, post_smoothing=True, post_normalization=True, interpolation_method=None, gaussian_order=None, gaussian_mode=None, gaussian_cval=None, gaussian_truncate=None, points_pr_split=None, savgol_filter_window_divisor_default=None, savgol_filter_window_order=None, max_points=None, **kwargs)

Deprecated. Use transform_half_cycle.

Returns:

  • (voltage, dqdv) as numpy arrays.

dvdq

dvdq(source, cycles=None, direction: str = BOTH, options: IcaOptions | None = None, *, strict: bool = False, cycle_mode: str | None = None, number_of_points: int | None = None, **overrides) -> pd.DataFrame

Differential voltage analysis (DVA): dV/dQ against capacity.

New in cellpy 2.0. DVA is the standard technique for electrode balancing and degradation-mode analysis, and cellpy could not compute it - some loaders ingested a dv_dq column when the cycler happened to export one, but there was no way to derive it from the curves.

It rides the same pipeline as dqdv and is in fact the simpler of the two: dQ/dV needs the V(q) curve inverted to q(V) before differentiating, while dV/dQ differentiates the smoothed V(q) curve the pipeline has already built.

Args and Returns as dqdv, except that the frame's columns are cycle, direction, capacity, voltage, dvdq, and that normalization defaults to False: DVA is read from the peak positions on the capacity axis, so rescaling the ordinate would only obscure the comparison between cycles.

Example

frame = dvdq(c, cycles=1, direction="charge") frame.plot(x="capacity", y="dvdq")

index_bounds

index_bounds(x) -> tuple[float, float]

Return (first, last) item of x.

to_wide

to_wide(frame: DataFrame) -> pd.DataFrame

Convert a specced long frame to the wide, cycle-per-column layout.

Wide format is an explicit conversion, not an entry-point mode: in 1.x whether you got long or wide depended on tidy= and split=, whose defaults disagreed between functions.

Parameters:

  • frame (DataFrame) –

    A frame from dqdv or dvdq.

Returns:

  • DataFrame

    A frame whose columns are a (cycle, value) MultiIndex. When the

  • DataFrame

    input holds both directions the top level is "<cycle> <direction>",

  • DataFrame

    so the two do not collide.

transform_half_cycle

transform_half_cycle(voltage, capacity, options: IcaOptions | None = None, *, derivative: str = 'dqdv') -> HalfCycleResult

Transform one half-cycle into dQ/dV or dV/dQ.

This is the pure core: same inputs, same outputs, no shared state. It is public because it is the honest replacement for 1.x's dqdv_np — the "I already have two arrays" case.

Parameters:

  • voltage

    Voltage samples.

  • capacity

    Capacity samples, monotonic and the same length as voltage.

  • options (IcaOptions | None, default: None ) –

    The recipe. Defaults to IcaOptions for dqdv and to DVA_DEFAULTS (no normalization) for dvdq.

  • derivative (str, default: 'dqdv' ) –

    "dqdv" or "dvdq".

Returns:

Raises:

  • NullData

    If either array is missing, or has one point or fewer.

  • ValueError

    If derivative is not a known mode.

Example

capacity, voltage = c.get_ccap(5, as_frame=False) result = transform_half_cycle(voltage, capacity) result.x, result.y # voltage, dQ/dV

value_bounds

value_bounds(x) -> tuple[float, float]

Return (min, max) of x.