Metrics

The metrics module provides classes for computing specific metrics from PyBaMM solutions at particular operating conditions.

Base Class

class ionworkspipeline.data_fits.models.metrics.BaseMetric(variable: str | None = None, step: int | list[int] | None = None)

Base class for metrics, the values read from a solution to fit or optimise.

Use Action classes (Maximize, Minimize, GreaterThan, LessThan) to define how metrics contribute to optimization objectives.

Parameters

variablestr or None

The variable name on which to compute the metric. May be None for wrapper metrics (ComposedMetric, IterativeMetric) that delegate to inner metrics.

stepint or list of int, optional

If provided, filters the solution to the specified step (0-indexed) using solution.sub_solutions[step] before computing the metric. A list of consecutive steps, such as [2, 3, 4], reads those steps joined into one, so Minimum(var, step=[2, 3, 4]) is one value over all three.

Examples

Arithmetic (+, -, *, /) composes metrics:

>>> # Pulse resistance = (V_after - V_before) / I_mean
>>> v_after = Time("Voltage [V]", value=1.0, step=2)
>>> v_before = Time("Voltage [V]", value=0.0, step=2)
>>> i_mean = Mean("Current [A]", step=2)
>>> resistance = (v_after - v_before) / i_mean

Fluent methods iterate over steps or cycles:

>>> Mean("Voltage [V]").by_step()  # evaluate for each step
<...IterativeMetric object at ...>
>>> Mean("Voltage [V]").by_cycle(step=1)  # the second step of each cycle
<...IterativeMetric object at ...>

Extends: ABC

any_fallback() → bool

Return whether this metric, or any metric nested in it, fell back last run.

fallback_used covers a metric’s own crossing only, so that a caller can name the crossing that was not found. A caller asking whether the value is a closest-match stand-in at all, such as an optimiser escalating its penalty, asks the whole tree here.

Returns

bool

True if any crossing in the tree used its closest-match fallback.

by_cycle(step: int | dict[int, int] | None = None, cycles: list[int] | None = None) → IterativeMetric

Evaluate this metric for each cycle.

Parameters

stepint or dict of int to int, optional

The step index (0-indexed) within each cycle to evaluate, or a mapping from cycle to step for a quantity measured at a different position in different cycles; a mapping selects exactly its own cycles. If None, evaluates the metric across the entire cycle (all steps).

cycleslist of int, optional

Specific cycle indices (0-indexed). If None, evaluates all cycles.

Returns

IterativeMetric

A metric that returns an array with one value per cycle.

Example

>>> Mean("Voltage [V]").by_cycle()  # all steps of all cycles
<...IterativeMetric object at ...>
>>> Mean("Voltage [V]").by_cycle(step=1)  # step 1 of all cycles
<...IterativeMetric object at ...>
>>> Maximum("Capacity [A.h]").by_cycle(step=1, cycles=[0, 5, 10])
<...IterativeMetric object at ...>
>>> Maximum("Capacity [A.h]").by_cycle(step={0: 5, 6: 0})  # step 5, then 0
<...IterativeMetric object at ...>
by_step(steps: list[int] | None = None) → IterativeMetric

Evaluate this metric for each step, returning an array of results.

Parameters

stepslist of int, optional

Specific step indices (0-indexed). If None, evaluates all steps.

Returns

IterativeMetric

A metric that returns an array with one value per step.

Example

>>> Mean("Voltage [V]").by_step()  # all steps
<...IterativeMetric object at ...>
>>> Mean("Voltage [V]").by_step(steps=[0, 2, 4])  # specific steps
<...IterativeMetric object at ...>
crossing_target() → tuple[str, float] | None

The variable and value this metric searches for a crossing of.

None for metrics that do not search for one. Lets a caller report which crossing was not found without knowing that SOC names its search column soc_variable while Voltage fixes it to the voltage.

cycles_read() → set[int] | None

Return the 0-based cycle indices this metric restricts the solve to.

Gathers the restrictions of sub_metrics(), returning None if any part needs every cycle. A leaf is not cycle-sliced and so imposes no restriction of its own; IterativeMetric adds its slicer’s.

Returns

set of int or None

The 0-based cycle indices required, an empty set when the metric adds no restriction, or None when every cycle is needed.

fallback_used: bool

Whether this metric’s own crossing fell back on its last run; a fallback nested inside it is not included, see any_fallback().

property name: str

The name of this metric, used for identification.

reads_step_endpoints_only() → bool

Return whether this metric reads only the first/last sample of a step.

Such metrics are unaffected by store_first_last (which keeps only each step’s endpoints). The default is conservatively False; First, Last, and IterativeMetric over a cycle slicer override it.

Returns

bool

True only when every value the metric extracts is a step endpoint.

remap_cycles(mapping: dict[int, int]) → None

Rewrite the cycle selections in this metric’s tree through mapping.

Propagates into sub_metrics(), each slicer keeping its own selection; IterativeMetric also remaps its own slicer.

Parameters

mappingdict of int to int

The position each selected cycle number sits at.

remap_steps(step_rows: list[list[list[int]]] | None) → None

Make step name a steps-table row rather than an experiment step.

Applies to a metric run on the whole solution, where step counts across the solve. A metric nested in an IterativeMetric is handed sub-solutions its slicer has already grouped by row, so the mapping stops at the slicer. See SolutionSlicer.remap_steps().

Parameters

step_rowslist of list of list of int or None

Per cycle, per row, the positions within the cycle of the experiment steps generated from that row. None leaves positions as they are.

run(solution: Solution, model: BaseModel = None) → float | ndarray

Compute the metric from the solution.

If a step filter is specified, the solution is filtered to that step’s sub-solution before computing the metric.

Parameters

solutionpybamm.Solution

The solved PyBaMM solution.

modelpybamm.BaseModel, optional

The PyBaMM model instance.

Returns

float or np.ndarray

The computed metric value(s).

Raises

StepNotSolvedError

If the step it reads was skipped by the solver.

set_cycles(cycles: list[int]) → None

Set the cycle selection on the slicers in this metric’s tree, where unset.

Propagates into sub_metrics(); IterativeMetric also sets its own slicer’s.

Parameters

cycleslist of int

The 0-based cycle indices to apply to any cycle slicer with no selection of its own.

sub_metrics() → list[BaseMetric]

Return the metrics nested inside this one.

Empty for leaf metrics. Composite metrics override this so callers can walk a metric tree without knowing which attributes hold its children — a new composite type declares its children here and every traversal picks them up.

to_config() → dict

Convert the metric to a configuration dictionary.

Returns

dict

Parsed configuration dictionary for this metric.

property variable: str

The variable name for this metric.

Raises NotImplementedError for wrapper metrics that must override this.

Metrics that reduce a series of per-cycle or per-step values to one number derive from AggregationMetric.

class ionworkspipeline.data_fits.models.metrics.metrics.AggregationMetric(variable: str, step: int | list[int] | None = None)

Abstract base class for metrics that perform aggregation operations on solution data.

This class provides a common implementation for metrics that extract variable data from a solution and apply a numpy aggregation function (sum, mean, min, max, etc.).

Parameters

variablestr

The variable name on which to compute the metric.

stepint or list of int, optional

If provided, filters the solution to the specified step (0-indexed).

Extends: BaseMetric

Time-Based Metrics

class ionworkspipeline.data_fits.models.metrics.Time(variable: str, value: float | list[float] | ndarray, step: int | list[int] | None = None)

A metric computed at one or more specific times.

Parameters

variablestr

The variable name on which to compute the metric.

valuefloat or array-like of float

The time value(s) (in seconds) to evaluate. A scalar returns a scalar; a 1-D array of times returns an array of the same length (read in a single call, the vectorised form needed for experiment-free continuous solves that have no per-cycle sub-solutions to slice). -1 selects the final solution time.

stepint or list of int, optional

If provided, filters the solution to the specified step (0-indexed).

Extends: BaseMetric

to_config() → dict

Convert the metric to a configuration dictionary.

Condition-Based Metrics

class ionworkspipeline.data_fits.models.metrics.Crossing(variable: str, crossing_variable: str, value: float | BaseMetric, step: int | list[int] | None = None)

Metric read where a variable crosses a threshold.

Reads variable at each time crossing_variable crosses value; if it never does, the closest match stands in and the result is penalised. A metric threshold is evaluated on the whole solution, before step narrows it, so it can depend on an earlier step of the same run. SOC and Voltage are crossings of a state of charge and of the voltage.

Parameters

variablestr

The variable name on which to compute the metric.

crossing_variablestr

The variable searched for the crossing.

valuefloat or BaseMetric

The value of crossing_variable to find, or a metric giving it, evaluated on the whole solution. A metric must evaluate to one value.

stepint or list of int, optional

If provided, filters the solution to the specified step (0-indexed), or to a list of consecutive steps joined into one.

Example

Time for the throughput of step 2 to reach 80% of the capacity discharged in step 0:

>>> capacity = Last("Discharge capacity [A.h]", step=0)
>>> Crossing("Time [s]", "Throughput capacity [A.h]", 0.8 * capacity, step=2)
<...Crossing object at ...>

Extends: BaseMetric

crossing_target() → tuple[str, float | None]

The variable searched and the threshold searched for.

A metric threshold is None until the metric has run.

cycles_read() → set[int] | None

Add no restriction, whatever cycles a metric threshold selects.

The threshold reads the solution this crossing is handed, so its cycles count cycles of that solution, not of the solve. Reporting them would let CycleAgeing save only the threshold’s cycles for a crossing that searches the whole solve, and treat it as per-cycle. remap_steps() does reach the threshold, on purpose: its mapping describes that solution.

Returns

set of int

Always empty.

remap_cycles(mapping: dict[int, int]) → None

Leave a metric threshold’s cycles as written; see cycles_read().

Parameters

mappingdict of int to int

Unused.

run(solution: Solution, model: BaseModel = None) → float | ndarray

Resolve the threshold on the whole solution, then find the crossing.

A threshold that falls back is searched for as its closest match, unpenalised, and the result is penalised once instead. Its fallback stays on the threshold’s own flag, so any_fallback() reports it and the crossing that was not found can be named.

Parameters

solutionpybamm.Solution

The solved PyBaMM solution.

modelpybamm.BaseModel, optional

The PyBaMM model instance.

Returns

float or np.ndarray

The value of variable at each crossing of the threshold.

set_cycles(cycles: list[int]) → None

Leave a metric threshold’s cycles as written; see cycles_read().

Parameters

cycleslist of int

Unused.

sub_metrics() → list[BaseMetric]

The threshold, when it is a metric.

to_config() → dict

Convert to a configuration dictionary, a metric threshold included.

class ionworkspipeline.data_fits.models.metrics.SOC(variable: str, soc_variable: str, value: float | BaseMetric, step: int | list[int] | None = None)

Metric computed at a specific state-of-charge using root finding for SOC crossings.

A Crossing of soc_variable at a state of charge between 0 and 1. For any other variable crossing a threshold, use Crossing.

Parameters

variablestr

The variable name on which to compute the metric.

soc_variablestr

The variable name for the SOC variable.

valuefloat or BaseMetric

The state-of-charge value (between 0 and 1), or a metric giving it, evaluated on the whole solution. A metric threshold is not range-checked, since its value is only known once the solution is.

stepint or list of int, optional

If provided, filters the solution to the specified step (0-indexed), or to a list of consecutive steps joined into one.

Extends: Crossing

property soc_variable: str

The state-of-charge variable searched for the crossing.

class ionworkspipeline.data_fits.models.metrics.Voltage(variable: str, value: float | BaseMetric, step: int | list[int] | None = None)

Metric computed at a specific voltage using root finding for voltage crossings.

A Crossing of "Voltage [V]".

Parameters

variablestr

The variable name on which to compute the metric.

valuefloat or BaseMetric

The voltage value in Volts, or a metric giving it, evaluated on the whole solution.

stepint or list of int, optional

If provided, filters the solution to the specified step (0-indexed), or to a list of consecutive steps joined into one.

Extends: Crossing

Aggregation Metrics

class ionworkspipeline.data_fits.models.metrics.Maximum(variable: str, step: int | list[int] | None = None)

A maximum metric computed across multiple solution points.

Parameters

variablestr

The variable name on which to compute the metric.

Extends: AggregationMetric

class ionworkspipeline.data_fits.models.metrics.Minimum(variable: str, step: int | list[int] | None = None)

A minimum metric computed across multiple solution points.

Parameters

variablestr

The variable name on which to compute the metric.

Extends: AggregationMetric

class ionworkspipeline.data_fits.models.metrics.Mean(variable: str, step: int | list[int] | None = None)

A mean metric computed across multiple solution points.

Parameters

variablestr

The variable name on which to compute the metric.

Extends: AggregationMetric

class ionworkspipeline.data_fits.models.metrics.Sum(variable: str, step: int | list[int] | None = None)

A summation metric computed across multiple solution points.

Parameters

variablestr

The variable name on which to compute the metric.

Extends: AggregationMetric

Point-Based Metrics

class ionworkspipeline.data_fits.models.metrics.PointBased(variable: str, step: int | list[int] | None = None)

A metric that extracts a single constant value from a solution variable.

This metric is designed for variables that have a constant value throughout the solution (e.g., design parameters like electrode thickness, cell cost). It extracts the value at the final time point.

Useful for optimization objectives that involve parameters defined via pybamm.Parameter expressions which don’t vary over time.

Parameters

variablestr

The variable name on which to compute the metric.

stepint or list of int, optional

If provided, filters the solution to the specified step (0-indexed).

Example

Define a cost variable in the model and use PointBased to extract it:

model.variables["Cell cost"] = (
    pybamm.Parameter("Total cell area [m]")
    * pybamm.Parameter("Areal cost density [$/m^2]")
)
cost_metric = PointBased("Cell cost")

Extends: BaseMetric

Action Classes

Actions wrap metrics and define how they contribute to the optimization cost landscape.

class ionworkspipeline.data_fits.models.metrics.BaseAction(metric: BaseMetric, weight: float = 1.0)

Base class for optimization actions that wrap metrics.

Actions define how a metric contributes to the optimization cost landscape. Each action wraps a metric and transforms its output appropriately for the optimizer (which minimizes).

Parameters

metricBaseMetric

The metric to wrap with this action.

weightfloat, optional

Weight to apply to this metric in the objective function. Default is 1.0.

Examples

>>> from ionworkspipeline.data_fits.models.metrics import Maximum, Minimum
>>> from ionworkspipeline.data_fits.models.metrics.actions import (
...     Maximize, Minimize, LessThan, GreaterThan,
... )

Define objective actions: >>> actions = { … “Cell capacity”: Maximize(Maximum(“Discharge capacity [A.h]”), weight=1.5), … }

Define constraint actions: >>> constraints = { … “Max temperature”: LessThan(Maximum(“Temperature [K]”), 323.15), … “Min voltage”: GreaterThan(Minimum(“Voltage [V]”), 2.5), … }

Extends: ABC

abstractmethod apply(metric_value: float | ndarray) → float | ndarray

Apply the action transformation to a metric value.

This method applies the action’s transformation (e.g., negation for Maximize) without running the underlying metric. Use this when you already have the raw metric value and want to avoid running the metric twice.

Parameters

metric_valuefloat or np.ndarray

The raw metric value to transform.

Returns

float or np.ndarray

The transformed metric value for the optimizer.

property name: str

Return a descriptive name for this action.

run(solution: Solution, model: BaseModel | None = None) → float | ndarray

Execute the action on a solution.

This is a convenience method that runs the metric and applies the action transformation in one step.

Parameters

solutionpybamm.Solution

The solved PyBaMM solution.

modelpybamm.BaseModel, optional

The PyBaMM model instance.

Returns

float or np.ndarray

The transformed metric value for the optimizer.

signed_violation(metric_value: float | ndarray) → float

Signed constraint violation for surrogate optimizers.

Convention: <= 0 means feasible, > 0 means violated. The base implementation falls back to the penalty from apply() (0 = feasible, > 0 = violated); threshold constraints override this with the signed distance to their threshold.

Parameters

metric_valuefloat or np.ndarray

The raw metric value(s) to evaluate against the constraint.

Returns

float

Signed violation.

Raises

NumericalError

If the metric value is empty or NaN, as apply() does.

property variable: str

Return the underlying metric’s variable name.

class ionworkspipeline.data_fits.models.metrics.Maximize(metric: BaseMetric, weight: float = 1.0)

Maximize the metric value during optimization.

Since optimizers typically minimize, this action negates the metric value so that minimizing the negative is equivalent to maximizing the original.

Parameters

metricBaseMetric

The metric to maximize.

Example

Maximize discharge capacity:

Maximize(Maximum("Discharge capacity [A.h]"))

Extends: BaseAction

apply(metric_value: float | ndarray) → float | ndarray

Return negated metric value (optimizer minimizes, so we negate to maximize).

to_config() → dict

Convert to configuration dict with explicit action type.

class ionworkspipeline.data_fits.models.metrics.Minimize(metric: BaseMetric, weight: float = 1.0)

Minimize the metric value during optimization.

This action passes through the metric value unchanged since optimizers already minimize by default.

Parameters

metricBaseMetric

The metric to minimize.

Example

Minimize cell cost:

Minimize(PointBased("Cell cost"))

Extends: BaseAction

apply(metric_value: float | ndarray) → float | ndarray

Return metric value as-is (optimizer already minimizes).

to_config() → dict

Convert to configuration dict with explicit action type.

class ionworkspipeline.data_fits.models.metrics.GreaterThan(metric: BaseMetric, value: float, penalty: float = 1000000.0)

Constraint that the metric value should be greater than or equal to a threshold.

Returns 0 when the constraint is satisfied (value >= threshold), and a large penalty value when violated (value < threshold).

Parameters

metricBaseMetric

The metric to constrain.

valuefloat

The lower bound threshold. Values below this trigger penalties.

penaltyfloat, optional

The penalty value to return when the constraint is violated. This contribution is penalty * (1 + violation) to include both a steep violation and a proportion term to directionally inform the optimiser. Default is 1e6.

Example

Constrain minimum anode potential to be >= 0V (prevent lithium plating):

GreaterThan(Minimum("Negative electrode potential [V]"), 0.0)

Extends: BaseAction

apply(metric_value: float | ndarray) → float

Apply the constraint transformation to a metric value.

Returns

float

0.0 if constraint is satisfied (value >= threshold), penalty * (1 + violation) if violated (value < threshold).

Raises

NumericalError

If the metric value is empty or contains NaN.

property name: str

Return a descriptive name for this constraint.

signed_violation(metric_value: float | ndarray) → float

Signed violation threshold - min(metric) (<= 0 means feasible).

to_config() → dict

Convert to configuration dict with explicit action type.

class ionworkspipeline.data_fits.models.metrics.LessThan(metric: BaseMetric, value: float, penalty: float = 1000000.0)

Constraint that the metric value should be less than or equal to a threshold.

Returns 0 when the constraint is satisfied (value <= threshold), and a large penalty value when violated (value > threshold).

Parameters

metricBaseMetric

The metric to constrain.

valuefloat

The upper bound threshold. Values above this trigger penalties.

penaltyfloat, optional

The penalty value to scale the metric by when the constraint is violated. This contribution is penalty * (1 + violation) to include both a steep violation and a proportion term to directionally inform the optimiser. Default is 1e6.

Example

Constrain maximum temperature to <= 50°C:

LessThan(Maximum("Temperature [K]"), 323.15)

Extends: BaseAction

apply(metric_value: float | ndarray) → float

Apply the constraint transformation to a metric value.

Returns

float

0.0 if constraint is satisfied (value <= threshold), penalty * (1 + violation) if violated (value > threshold).

Raises

NumericalError

If the metric value is empty or contains NaN.

property name: str

Return a descriptive name for this constraint.

signed_violation(metric_value: float | ndarray) → float

Signed violation max(metric) - threshold (<= 0 means feasible).

to_config() → dict

Convert to configuration dict with explicit action type.