Source code for pybop._result

from typing import TYPE_CHECKING

import numpy as np
from pybamm import ParameterValues

if TYPE_CHECKING:
    from pybop import BaseOptimiser
from pybop import Logger, plot


[docs] class OptimisationResult: """ Stores the result of the optimisation. Attributes ---------- optim : pybop.BaseOptimiser The optimisation object used to generate the results. logger : pybop.Logger The log of the optimisation process. time : float The time taken. optim_name : str The name of the optimiser. message : str The reason for stopping given by the optimiser. scipy_result : scipy.optimize.OptimizeResult, optional The result obtained from a SciPy optimiser. """ def __init__( self, optim: "BaseOptimiser", logger: Logger, time: float, optim_name: str | None = None, message: str | None = None, scipy_result=None, ):
[docs] self._optim = optim
[docs] self._minimising = optim.problem.minimising
[docs] self.optim_name = optim_name
[docs] self.n_runs = 0
[docs] self._best_run = None
[docs] self._parameter_values = None
[docs] self._x = [logger.x_model_best]
[docs] self._x_model = [logger.x_model]
[docs] self._x0 = [logger.x0]
[docs] self._best_cost = [logger.cost_best]
[docs] self._cost = [logger.cost_convergence]
[docs] self._initial_cost = [logger.cost[0]]
[docs] self._n_iterations = [logger.iteration]
[docs] self._iteration_number = [logger.iteration_number]
[docs] self._n_evaluations = [logger.evaluations]
[docs] self._message = [message]
[docs] self._scipy_result = [scipy_result]
[docs] self._time = [time]
self._validate() @staticmethod
[docs] def combine(results: list["OptimisationResult"]) -> "OptimisationResult": """ Combine multiple OptimisationResult objects into a single one. Parameters ---------- results : list[OptimisationResult] List of OptimisationResult objects to combine. Returns ------- OptimisationResult Combined OptimisationResult object. """ if len(results) == 0: raise ValueError("No results to combine.") ret = results[0] ret._x = [x for result in results for x in result._x] # noqa: SLF001 ret._x_model = [x for result in results for x in result._x_model] # noqa: SLF001 ret._x0 = [x for result in results for x in result._x0] # noqa: SLF001 ret._best_cost = [ # noqa: SLF001 x for result in results for x in result._best_cost # noqa: SLF001 ] ret._cost = [x for result in results for x in result._cost] # noqa: SLF001 ret._initial_cost = [ # noqa: SLF001 x for result in results for x in result._initial_cost # noqa: SLF001 ] ret._n_iterations = [ # noqa: SLF001 x for result in results for x in result._n_iterations # noqa: SLF001 ] ret._iteration_number = [ # noqa: SLF001 x for result in results for x in result._iteration_number # noqa: SLF001 ] ret._n_evaluations = [ # noqa: SLF001 x for result in results for x in result._n_evaluations # noqa: SLF001 ] ret._message = [ # noqa: SLF001 x for result in results for x in result._message # noqa: SLF001 ] ret._scipy_result = [ # noqa: SLF001 x for result in results for x in result._scipy_result # noqa: SLF001 ] ret._time = [x for result in results for x in result._time] # noqa: SLF001 ret._best_run = None # noqa: SLF001 ret.n_runs = len(results) ret._validate() # noqa: SLF001 return ret
[docs] def _validate(self): """Check that there is a finite cost and update best run.""" self._check_for_finite_cost() if self._minimising: self._best_run = self._best_cost.index(min(self._best_cost)) else: self._best_run = self._best_cost.index(max(self._best_cost))
[docs] def _check_for_finite_cost(self) -> None: """ Validate the optimised parameters and ensure they produce a finite cost value. Raises: ValueError: If the optimised parameters do not produce a finite cost value. """ if not any(np.isfinite(self._best_cost)): raise ValueError( f"Optimised parameters {self._optim.problem.parameters.to_dict(self._x[-1])} do not produce a finite cost value." )
[docs] def __str__(self) -> str: """ A string representation of the OptimisationResult object. Returns: str: A formatted string containing optimisation result information. """ return ( f"OptimisationResult:\n" f" Best result from {self.n_runs} run(s).\n" f" Initial parameters: {self.x0}\n" f" Optimised parameters: {self.x}\n" f" Best cost: {self.best_cost}\n" f" Optimisation time: {self.time} seconds\n" f" Number of iterations: {self.total_iterations()}\n" f" Number of evaluations: {self.total_evaluations()}\n" f" Reason for stopping: {self.message}" )
[docs] def total_iterations(self) -> np.floating | None: """Calculates the total number of iterations across all runs.""" return np.sum(self._n_iterations) if len(self._n_iterations) > 0 else None
[docs] def total_evaluations(self) -> np.floating | None: """Calculates the total number of evaluations across all runs.""" return np.sum(self._n_evaluations) if len(self._n_evaluations) > 0 else None
[docs] def total_runtime(self) -> np.floating | None: """Calculates the total runtime across all runs.""" return np.sum(self._time) if len(self._time) > 0 else None
[docs] def _get_single_or_all(self, attr): value = getattr(self, attr) if len(value) > 1: return value[self._best_run] return value[0]
@property
[docs] def x(self) -> np.ndarray: """The solution of the optimisation (in model space).""" return self._get_single_or_all("_x")
@property
[docs] def x_model(self) -> np.ndarray: """The log of the evaluated parameters (in model space).""" return self._get_single_or_all("_x_model")
@property
[docs] def x0(self) -> np.ndarray: """The initial parameter values.""" return self._get_single_or_all("_x0")
@property
[docs] def best_cost(self) -> float: """The best cost value(s).""" return self._get_single_or_all("_best_cost")
@property
[docs] def cost(self) -> np.ndarray: """The log of the cost values.""" return self._get_single_or_all("_cost")
@property
[docs] def initial_cost(self) -> float: """The initial cost value(s).""" return self._get_single_or_all("_initial_cost")
@property
[docs] def n_iterations(self) -> int: """The number of iterations.""" return self._get_single_or_all("_n_iterations")
@property
[docs] def iteration_number(self) -> np.ndarray | None: """The number of iterations.""" return self._get_single_or_all("_iteration_number")
@property
[docs] def n_evaluations(self) -> int: """The number of evaluations.""" return self._get_single_or_all("_n_evaluations")
@property
[docs] def optim(self) -> "BaseOptimiser": """The optimisation problem.""" return self._optim
@property
[docs] def minimising(self) -> bool: """Whether the cost was minimised (or maximised).""" return self._minimising
@property
[docs] def parameter_values(self) -> ParameterValues | dict: """The best parameter values from the optimisation.""" return self._parameter_values
@property
[docs] def message(self) -> str | None: """The optimisation termination message(s).""" return self._get_single_or_all("_message")
@property
[docs] def scipy_result(self): """The SciPy result.""" return self._get_single_or_all("_scipy_result")
@property
[docs] def time(self) -> float | None: """The optimisation time(s).""" return self.total_runtime()
[docs] def plot_convergence(self, **kwargs): """ Plot the evolution of the best cost during the optimisation. Parameters ---------- show : bool, optional If True, the figure is shown upon creation (default: True). **layout_kwargs : optional Valid Plotly layout keys and their values. """ return plot.convergence(result=self, **kwargs)
[docs] def plot_parameters(self, **kwargs): """ Plot the evolution of parameter values during the optimisation. Parameters ---------- show : bool, optional If True, the figure is shown upon creation (default: True). **layout_kwargs : optional Valid Plotly layout keys and their values. """ return plot.parameters(result=self, **kwargs)
[docs] def plot_surface(self, **kwargs): """ Plot a 2D representation of the Voronoi diagram with color-coded regions. Parameters ---------- bounds : numpy.ndarray, optional A 2x2 array specifying the [min, max] bounds for each parameter. normalise : bool, optional If True, the voronoi regions are computed using the Euclidean distance between points normalised with respect to the bounds (default: True). resolution : int, optional Resolution of the plot (default: 500). show : bool, optional If True, the figure is shown upon creation (default: True). **layout_kwargs : optional Valid Plotly layout keys and their values. """ return plot.surface(result=self, **kwargs)
[docs] def plot_contour(self, **kwargs): """ Generate and plot a 2D visualisation of the cost landscape with the optimisation trace. Parameters ---------- gradient : bool, optional If True, gradient plots are also generated (default: False). bounds : numpy.ndarray | list[list[float]], optional A 2x2 array specifying the [min, max] bounds for each parameter. transformed : bool, optional Uses the transformed parameter values, as seen by the optimiser (default: False). steps : int, optional The number of grid points to divide the parameter space into along each dimension (default: 10). show : bool, optional If True, the figure is shown upon creation (default: True). **layout_kwargs : optional Valid Plotly layout keys and their values. """ return plot.contour(call_object=self, **kwargs)