Source code for pybop._problem

import numpy as np

import pybop


[docs] class BaseProblem: """ Base class for defining a problem within the PyBOP framework, compatible with PINTS. Parameters ---------- parameters : list List of parameters for the problem. model : object, optional The model to be used for the problem (default: None). check_model : bool, optional Flag to indicate if the model should be checked (default: True). signal: List[str] The signal to observe. additional_variables : List[str], optional Additional variables to observe and store in the solution (default: []). init_soc : float, optional Initial state of charge (default: None). x0 : np.ndarray, optional Initial parameter values (default: None). """ def __init__( self, parameters, model=None, check_model=True, signal=["Voltage [V]"], additional_variables=[], init_soc=None, x0=None, ): self.parameters = parameters self._model = model self.check_model = check_model if isinstance(signal, str): signal = [signal] elif not all(isinstance(item, str) for item in signal): raise ValueError("Signal should be either a string or list of strings.") self.signal = signal self.init_soc = init_soc self.x0 = x0 self.n_parameters = len(self.parameters) self.n_outputs = len(self.signal) self._time_data = None self._target = None if isinstance(model, pybop.BaseModel): self.additional_variables = additional_variables else: self.additional_variables = [] # Set bounds (for all or no parameters) all_unbounded = True # assumption self.bounds = {"lower": [], "upper": []} for param in self.parameters: if param.bounds is not None: self.bounds["lower"].append(param.bounds[0]) self.bounds["upper"].append(param.bounds[1]) all_unbounded = False else: self.bounds["lower"].append(-np.inf) self.bounds["upper"].append(np.inf) if all_unbounded: self.bounds = None # Set initial standard deviation (for all or no parameters) all_have_sigma = True # assumption self.sigma0 = [] for param in self.parameters: if hasattr(param.prior, "sigma"): self.sigma0.append(param.prior.sigma) else: all_have_sigma = False if not all_have_sigma: self.sigma0 = None # Sample from prior for x0 if x0 is None: self.x0 = np.zeros(self.n_parameters) for i, param in enumerate(self.parameters): self.x0[i] = param.rvs(1) elif len(x0) != self.n_parameters: raise ValueError("x0 dimensions do not match number of parameters") # Add the initial values to the parameter definitions for i, param in enumerate(self.parameters): param.update(initial_value=self.x0[i])
[docs] def evaluate(self, x): """ Evaluate the model with the given parameters and return the signal. Parameters ---------- x : np.ndarray Parameter values to evaluate the model at. Raises ------ NotImplementedError This method must be implemented by subclasses. """ raise NotImplementedError
[docs] def evaluateS1(self, x): """ Evaluate the model with the given parameters and return the signal and its derivatives. Parameters ---------- x : np.ndarray Parameter values to evaluate the model at. Raises ------ NotImplementedError This method must be implemented by subclasses. """ raise NotImplementedError
[docs] def time_data(self): """ Returns the time data. Returns ------- np.ndarray The time array. """ return self._time_data
[docs] def target(self): """ Return the target dataset. Returns ------- np.ndarray The target dataset array. """ return self._target
@property
[docs] def model(self): return self._model
[docs] class FittingProblem(BaseProblem): """ Problem class for fitting (parameter estimation) problems. Extends `BaseProblem` with specifics for fitting a model to a dataset. Parameters ---------- model : object The model to fit. parameters : list List of parameters for the problem. dataset : Dataset Dataset object containing the data to fit the model to. signal : str, optional The variable used for fitting (default: "Voltage [V]"). additional_variables : List[str], optional Additional variables to observe and store in the solution (default additions are: ["Time [s]"]). init_soc : float, optional Initial state of charge (default: None). x0 : np.ndarray, optional Initial parameter values (default: None). """ def __init__( self, model, parameters, dataset, check_model=True, signal=["Voltage [V]"], additional_variables=[], init_soc=None, x0=None, ): # Add time and remove duplicates additional_variables.extend(["Time [s]"]) additional_variables = list(set(additional_variables)) super().__init__( parameters, model, check_model, signal, additional_variables, init_soc, x0 ) self._dataset = dataset.data self.x = self.x0 # Check that the dataset contains time and current dataset.check(self.signal + ["Current function [A]"]) # Unpack time and target data self._time_data = self._dataset["Time [s]"] self.n_time_data = len(self._time_data) self._target = {signal: self._dataset[signal] for signal in self.signal} # Add useful parameters to model if model is not None: self._model.signal = self.signal self._model.additional_variables = self.additional_variables self._model.n_parameters = self.n_parameters self._model.n_outputs = self.n_outputs self._model.n_time_data = self.n_time_data # Build the model from scratch if self._model._built_model is not None: self._model._model_with_set_params = None self._model._built_model = None self._model._built_initial_soc = None self._model._mesh = None self._model._disc = None self._model.build( dataset=self._dataset, parameters=self.parameters, check_model=self.check_model, init_soc=self.init_soc, )
[docs] def evaluate(self, x): """ Evaluate the model with the given parameters and return the signal. Parameters ---------- x : np.ndarray Parameter values to evaluate the model at. Returns ------- y : np.ndarray The model output y(t) simulated with inputs x. """ if np.any(x != self.x) and self._model.matched_parameters: for i, param in enumerate(self.parameters): param.update(value=x[i]) self._model.rebuild(parameters=self.parameters) self.x = x y = self._model.simulate(inputs=x, t_eval=self._time_data) return y
[docs] def evaluateS1(self, x): """ Evaluate the model with the given parameters and return the signal and its derivatives. Parameters ---------- x : np.ndarray Parameter values to evaluate the model at. Returns ------- tuple A tuple containing the simulation result y(t) and the sensitivities dy/dx(t) evaluated with given inputs x. """ if self._model.matched_parameters: raise RuntimeError( "Gradient not available when using geometric parameters." ) y, dy = self._model.simulateS1( inputs=x, t_eval=self._time_data, ) return (y, np.asarray(dy))
[docs] class DesignProblem(BaseProblem): """ Problem class for design optimization problems. Extends `BaseProblem` with specifics for applying a model to an experimental design. Parameters ---------- model : object The model to apply the design to. parameters : list List of parameters for the problem. experiment : object The experimental setup to apply the model to. check_model : bool, optional Flag to indicate if the model parameters should be checked for feasibility each iteration (default: True). signal : str, optional The signal to fit (default: "Voltage [V]"). additional_variables : List[str], optional Additional variables to observe and store in the solution (default additions are: ["Time [s]", "Current [A]"]). init_soc : float, optional Initial state of charge (default: None). x0 : np.ndarray, optional Initial parameter values (default: None). """ def __init__( self, model, parameters, experiment, check_model=True, signal=["Voltage [V]"], additional_variables=[], init_soc=None, x0=None, ): # Add time and current and remove duplicates additional_variables.extend(["Time [s]", "Current [A]"]) additional_variables = list(set(additional_variables)) super().__init__( parameters, model, check_model, signal, additional_variables, init_soc, x0 ) self.experiment = experiment # Build the model if required if experiment is not None: # Leave the build until later to apply the experiment self._model.parameters = self.parameters if self.parameters is not None: self._model.fit_keys = [param.name for param in self.parameters] elif self._model._built_model is None: self._model.build( experiment=self.experiment, parameters=self.parameters, check_model=self.check_model, init_soc=self.init_soc, ) # Add an example dataset for plotting comparison sol = self.evaluate(self.x0) self._time_data = sol["Time [s]"] self._target = {key: sol[key] for key in self.signal} self._dataset = None
[docs] def evaluate(self, x): """ Evaluate the model with the given parameters and return the signal. Parameters ---------- x : np.ndarray Parameter values to evaluate the model at. Returns ------- y : np.ndarray The model output y(t) simulated with inputs x. """ sol = self._model.predict( inputs=x, experiment=self.experiment, init_soc=self.init_soc, ) if sol == [np.inf]: return sol else: predictions = {} for signal in self.signal + self.additional_variables: predictions[signal] = sol[signal].data return predictions