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, soMinimum(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_usedcovers 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.
Nonefor metrics that do not search for one. Lets a caller report which crossing was not found without knowing thatSOCnames its search columnsoc_variablewhileVoltagefixes 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(), returningNoneif any part needs every cycle. A leaf is not cycle-sliced and so imposes no restriction of its own;IterativeMetricadds 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
Nonewhen 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().
- 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 conservativelyFalse;First,Last, andIterativeMetricover a cycle slicer override it.Returns¶
- bool
Trueonly 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;IterativeMetricalso 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
stepname a steps-table row rather than an experiment step.Applies to a metric run on the whole solution, where
stepcounts across the solve. A metric nested in anIterativeMetricis handed sub-solutions its slicer has already grouped by row, so the mapping stops at the slicer. SeeSolutionSlicer.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();IterativeMetricalso 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.
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).
-1selects the final solution time.- stepint or list of int, optional
If provided, filters the solution to the specified step (0-indexed).
Extends:
BaseMetric
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
variableat each timecrossing_variablecrossesvalue; if it never does, the closest match stands in and the result is penalised. A metric threshold is evaluated on the whole solution, beforestepnarrows it, so it can depend on an earlier step of the same run.SOCandVoltageare 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_variableto 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
Noneuntil 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
cyclescount cycles of that solution, not of the solve. Reporting them would letCycleAgeingsave 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
variableat 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.
- 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
Crossingofsoc_variableat a state of charge between 0 and 1. For any other variable crossing a threshold, useCrossing.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
- 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
Crossingof"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
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.
- 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:
<= 0means feasible,> 0means violated. The base implementation falls back to the penalty fromapply()(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.
- 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
- 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
- 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.
- 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.