diff --git a/benchmarks/benchmark_model.py b/benchmarks/benchmark_model.py index 843b03bcc..5d496215b 100644 --- a/benchmarks/benchmark_model.py +++ b/benchmarks/benchmark_model.py @@ -1,8 +1,7 @@ import numpy as np import pybop - -from .benchmark_utils import set_random_seed +from benchmarks.benchmark_utils import set_random_seed class BenchmarkModel: diff --git a/benchmarks/benchmark_optim_construction.py b/benchmarks/benchmark_optim_construction.py index fee5f0789..75bb28b3c 100644 --- a/benchmarks/benchmark_optim_construction.py +++ b/benchmarks/benchmark_optim_construction.py @@ -1,8 +1,7 @@ import numpy as np import pybop - -from .benchmark_utils import set_random_seed +from benchmarks.benchmark_utils import set_random_seed class BenchmarkOptimisationConstruction: diff --git a/benchmarks/benchmark_parameterisation.py b/benchmarks/benchmark_parameterisation.py index a64116a48..681502387 100644 --- a/benchmarks/benchmark_parameterisation.py +++ b/benchmarks/benchmark_parameterisation.py @@ -1,8 +1,7 @@ import numpy as np import pybop - -from .benchmark_utils import set_random_seed +from benchmarks.benchmark_utils import set_random_seed class BenchmarkParameterisation: diff --git a/benchmarks/benchmark_track_parameterisation.py b/benchmarks/benchmark_track_parameterisation.py index 9180ffecb..a420dd3b9 100644 --- a/benchmarks/benchmark_track_parameterisation.py +++ b/benchmarks/benchmark_track_parameterisation.py @@ -1,8 +1,7 @@ import numpy as np import pybop - -from .benchmark_utils import set_random_seed +from benchmarks.benchmark_utils import set_random_seed class BenchmarkTrackParameterisation: diff --git a/docs/_extension/gallery_directive.py b/docs/_extension/gallery_directive.py index 3579ffcd8..4ab88d996 100644 --- a/docs/_extension/gallery_directive.py +++ b/docs/_extension/gallery_directive.py @@ -12,7 +12,7 @@ """ from pathlib import Path -from typing import Any, Dict, List +from typing import Any from docutils import nodes from docutils.parsers.rst import directives @@ -68,7 +68,7 @@ class GalleryGridDirective(SphinxDirective): "class-card": directives.unchanged, } - def run(self) -> List[nodes.Node]: + def run(self) -> list[nodes.Node]: """Create the gallery grid.""" if self.arguments: # If an argument is given, assume it's a path to a YAML file @@ -129,7 +129,7 @@ def run(self) -> List[nodes.Node]: return [container.children[0]] -def setup(app: Sphinx) -> Dict[str, Any]: +def setup(app: Sphinx) -> dict[str, Any]: """Add custom configuration to sphinx app. Args: diff --git a/docs/conf.py b/docs/conf.py index df93a8fa4..d7c54b116 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -7,11 +7,11 @@ from pathlib import Path sys.path.append(str(Path(".").resolve())) -from pybop._version import __version__ # noqa: E402 +from pybop._version import __version__ # -- Project information ----------------------------------------------------- project = "PyBOP" -copyright = "2023, The PyBOP Team" +copyright = "2023, The PyBOP Team" # noqa A001 author = "The PyBOP Team" release = f"v{__version__}" diff --git a/examples/standalone/problem.py b/examples/standalone/problem.py index d76f9dca5..18bf1f7d4 100644 --- a/examples/standalone/problem.py +++ b/examples/standalone/problem.py @@ -24,7 +24,7 @@ def __init__( self._dataset = dataset.data # Check that the dataset contains time and current - for name in ["Time [s]"] + self.signal: + for name in ["Time [s]", *self.signal]: if name not in self._dataset: raise ValueError(f"expected {name} in list of dataset") diff --git a/pybop/_dataset.py b/pybop/_dataset.py index 0da8be4be..66bcb1f10 100644 --- a/pybop/_dataset.py +++ b/pybop/_dataset.py @@ -1,6 +1,5 @@ import numpy as np -from pybamm import Interpolant, solvers -from pybamm import t as pybamm_t +from pybamm import solvers class Dataset: @@ -77,26 +76,7 @@ def __getitem__(self, key): return self.data[key] - def Interpolant(self): - """ - Create an interpolation function of the dataset based on the independent variable. - - Currently, only time-based interpolation is supported. This method modifies - the instance's Interpolant attribute to be an interpolation function that - can be evaluated at different points in time. - - Raises - ------ - NotImplementedError - If the independent variable for interpolation is not supported. - """ - - if self.variable == "time": - self.Interpolant = Interpolant(self.x, self.y, pybamm_t) - else: - NotImplementedError("Only time interpolation is supported") - - def check(self, signal=["Voltage [V]"]): + def check(self, signal=None): """ Check the consistency of a PyBOP Dataset against the expected format. @@ -110,11 +90,13 @@ def check(self, signal=["Voltage [V]"]): ValueError If the time series and the data series are not consistent. """ + if signal is None: + signal = ["Voltage [V]"] if isinstance(signal, str): signal = [signal] # Check that the dataset contains time and chosen signal - for name in ["Time [s]"] + signal: + for name in ["Time [s]", *signal]: if name not in self.names: raise ValueError(f"expected {name} in list of dataset") diff --git a/pybop/costs/_likelihoods.py b/pybop/costs/_likelihoods.py index c0f580a2a..c6e5916de 100644 --- a/pybop/costs/_likelihoods.py +++ b/pybop/costs/_likelihoods.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Union +from typing import Union import numpy as np @@ -14,7 +14,7 @@ class BaseLikelihood(BaseCost): """ def __init__(self, problem: BaseProblem): - super(BaseLikelihood, self).__init__(problem) + super().__init__(problem) self.n_time_data = problem.n_time_data @@ -32,8 +32,8 @@ class GaussianLogLikelihoodKnownSigma(BaseLikelihood): per dimension. """ - def __init__(self, problem: BaseProblem, sigma0: Union[List[float], float]): - super(GaussianLogLikelihoodKnownSigma, self).__init__(problem) + def __init__(self, problem: BaseProblem, sigma0: Union[list[float], float]): + super().__init__(problem) sigma0 = self.check_sigma0(sigma0) self.sigma2 = sigma0**2.0 self._offset = -0.5 * self.n_time_data * np.log(2 * np.pi * self.sigma2) @@ -62,7 +62,7 @@ def _evaluate(self, inputs: Inputs, grad: Union[None, np.ndarray] = None) -> flo return e if self.n_outputs != 1 else e.item() - def _evaluateS1(self, inputs: Inputs) -> Tuple[float, np.ndarray]: + def _evaluateS1(self, inputs: Inputs) -> tuple[float, np.ndarray]: """ Calls the problem.evaluateS1 method and calculates the log-likelihood and gradient. """ @@ -90,7 +90,7 @@ def check_sigma0(self, sigma0: Union[np.ndarray, float]): if np.shape(sigma0) not in [(), (1,), (self.n_outputs,)]: raise ValueError( "sigma0 must be either a scalar value (one standard deviation for " - + "all coordinates) or an array with one entry per dimension." + "all coordinates) or an array with one entry per dimension." ) return sigma0 @@ -115,10 +115,10 @@ class GaussianLogLikelihood(BaseLikelihood): def __init__( self, problem: BaseProblem, - sigma0: Union[float, List[float], List[Parameter]] = 0.002, + sigma0: Union[float, list[float], list[Parameter]] = 0.002, dsigma_scale: float = 1.0, ): - super(GaussianLogLikelihood, self).__init__(problem) + super().__init__(problem) self._dsigma_scale = dsigma_scale self._logpi = -0.5 * self.n_time_data * np.log(2 * np.pi) @@ -128,7 +128,7 @@ def __init__( self._dl = np.ones(self.n_parameters) def _add_sigma_parameters(self, sigma0): - sigma0 = [sigma0] if not isinstance(sigma0, List) else sigma0 + sigma0 = [sigma0] if not isinstance(sigma0, list) else sigma0 sigma0 = self._pad_sigma0(sigma0) for i, value in enumerate(sigma0): @@ -214,7 +214,7 @@ def _evaluate(self, inputs: Inputs, grad: Union[None, np.ndarray] = None) -> flo return e if self.n_outputs != 1 else e.item() - def _evaluateS1(self, inputs: Inputs) -> Tuple[float, np.ndarray]: + def _evaluateS1(self, inputs: Inputs) -> tuple[float, np.ndarray]: """ Calls the problem.evaluateS1 method and calculates the log-likelihood. @@ -265,7 +265,7 @@ class MAP(BaseLikelihood): """ def __init__(self, problem, likelihood, sigma0=None, gradient_step=1e-3): - super(MAP, self).__init__(problem) + super().__init__(problem) self.sigma0 = sigma0 self.gradient_step = gradient_step if self.sigma0 is None: @@ -278,7 +278,7 @@ def __init__(self, problem, likelihood, sigma0=None, gradient_step=1e-3): except Exception as e: raise ValueError( f"An error occurred when constructing the Likelihood class: {e}" - ) + ) from e if hasattr(self, "likelihood") and not isinstance( self.likelihood, BaseLikelihood @@ -310,7 +310,7 @@ def _evaluate(self, inputs: Inputs, grad=None) -> float: posterior = log_likelihood + log_prior return posterior - def _evaluateS1(self, inputs: Inputs) -> Tuple[float, np.ndarray]: + def _evaluateS1(self, inputs: Inputs) -> tuple[float, np.ndarray]: """ Compute the maximum a posteriori with respect to the parameters. The method passes the likelihood gradient to the optimiser without modification. diff --git a/pybop/costs/base_cost.py b/pybop/costs/base_cost.py index b5ad603c3..c7f7a7bbf 100644 --- a/pybop/costs/base_cost.py +++ b/pybop/costs/base_cost.py @@ -72,7 +72,7 @@ def evaluate(self, x, grad=None): raise e except Exception as e: - raise ValueError(f"Error in cost calculation: {e}") + raise ValueError(f"Error in cost calculation: {e}") from e def _evaluate(self, inputs: Inputs, grad=None): """ @@ -129,7 +129,7 @@ def evaluateS1(self, x): raise e except Exception as e: - raise ValueError(f"Error in cost calculation: {e}") + raise ValueError(f"Error in cost calculation: {e}") from e def _evaluateS1(self, inputs: Inputs): """ diff --git a/pybop/costs/design_costs.py b/pybop/costs/design_costs.py index 85f3dee40..ac8ecacac 100644 --- a/pybop/costs/design_costs.py +++ b/pybop/costs/design_costs.py @@ -31,7 +31,7 @@ def __init__(self, problem, update_capacity=False): problem : object The problem instance containing the model and data. """ - super(DesignCost, self).__init__(problem) + super().__init__(problem) self.problem = problem if update_capacity is True: nominal_capacity_warning = ( @@ -41,7 +41,7 @@ def __init__(self, problem, update_capacity=False): nominal_capacity_warning = ( "The nominal capacity is fixed at the initial model value." ) - warnings.warn(nominal_capacity_warning, UserWarning) + warnings.warn(nominal_capacity_warning, UserWarning, stacklevel=2) self.update_capacity = update_capacity self.parameter_set = problem.model.parameter_set self.update_simulation_data(self.parameters.as_dict("initial")) @@ -97,7 +97,7 @@ class GravimetricEnergyDensity(DesignCost): """ def __init__(self, problem, update_capacity=False): - super(GravimetricEnergyDensity, self).__init__(problem, update_capacity) + super().__init__(problem, update_capacity) def _evaluate(self, inputs: Inputs, grad=None): """ @@ -153,7 +153,7 @@ class VolumetricEnergyDensity(DesignCost): """ def __init__(self, problem, update_capacity=False): - super(VolumetricEnergyDensity, self).__init__(problem, update_capacity) + super().__init__(problem, update_capacity) def _evaluate(self, inputs: Inputs, grad=None): """ diff --git a/pybop/costs/fitting_costs.py b/pybop/costs/fitting_costs.py index 6315c4b6f..a0cba766a 100644 --- a/pybop/costs/fitting_costs.py +++ b/pybop/costs/fitting_costs.py @@ -18,7 +18,7 @@ class RootMeanSquaredError(BaseCost): """ def __init__(self, problem): - super(RootMeanSquaredError, self).__init__(problem) + super().__init__(problem) # Default fail gradient self._de = 1.0 @@ -131,7 +131,7 @@ class SumSquaredError(BaseCost): """ def __init__(self, problem): - super(SumSquaredError, self).__init__(problem) + super().__init__(problem) # Default fail gradient self._de = 1.0 @@ -161,7 +161,7 @@ def _evaluate(self, inputs: Inputs, grad=None): e = np.asarray( [ - np.sum(((prediction[signal] - self._target[signal]) ** 2)) + np.sum((prediction[signal] - self._target[signal]) ** 2) for signal in self.signal ] ) diff --git a/pybop/models/base_model.py b/pybop/models/base_model.py index e9ba3d6c4..461989086 100644 --- a/pybop/models/base_model.py +++ b/pybop/models/base_model.py @@ -1,6 +1,6 @@ import copy from dataclasses import dataclass -from typing import Any, Dict, Optional, Union +from typing import Any, Optional, Union import casadi import numpy as np @@ -11,7 +11,7 @@ @dataclass -class TimeSeriesState(object): +class TimeSeriesState: """ The current state of a time series model that is a pybamm model. """ @@ -76,9 +76,9 @@ def __init__(self, name="Base Model", parameter_set=None): def build( self, dataset: Dataset = None, - parameters: Union[Parameters, Dict] = None, + parameters: Union[Parameters, dict] = None, check_model: bool = True, - init_soc: float = None, + init_soc: Optional[float] = None, ) -> None: """ Construct the PyBaMM model if not already built, and set parameters. @@ -191,10 +191,10 @@ def set_params(self, rebuild=False): def rebuild( self, dataset: Dataset = None, - parameters: Union[Parameters, Dict] = None, + parameters: Union[Parameters, dict] = None, parameter_set: ParameterSet = None, check_model: bool = True, - init_soc: float = None, + init_soc: Optional[float] = None, ) -> None: """ Rebuild the PyBaMM model for a given parameter set. @@ -329,7 +329,7 @@ def step(self, state: TimeSeriesState, time: np.ndarray) -> TimeSeriesState: def simulate( self, inputs: Inputs, t_eval: np.array - ) -> Dict[str, np.ndarray[np.float64]]: + ) -> dict[str, np.ndarray[np.float64]]: """ Execute the forward model simulation and return the result. @@ -457,8 +457,8 @@ def predict( t_eval: np.array = None, parameter_set: ParameterSet = None, experiment: Experiment = None, - init_soc: float = None, - ) -> Dict[str, np.ndarray[np.float64]]: + init_soc: Optional[float] = None, + ) -> dict[str, np.ndarray[np.float64]]: """ Solve the model using PyBaMM's simulation framework and return the solution. @@ -676,7 +676,7 @@ def submesh_types(self): return self._submesh_types @submesh_types.setter - def submesh_types(self, submesh_types: Optional[Dict[str, Any]]): + def submesh_types(self, submesh_types: Optional[dict[str, Any]]): self._submesh_types = ( submesh_types.copy() if submesh_types is not None else None ) @@ -690,7 +690,7 @@ def var_pts(self): return self._var_pts @var_pts.setter - def var_pts(self, var_pts: Optional[Dict[str, int]]): + def var_pts(self, var_pts: Optional[dict[str, int]]): self._var_pts = var_pts.copy() if var_pts is not None else None @property @@ -698,7 +698,7 @@ def spatial_methods(self): return self._spatial_methods @spatial_methods.setter - def spatial_methods(self, spatial_methods: Optional[Dict[str, Any]]): + def spatial_methods(self, spatial_methods: Optional[dict[str, Any]]): self._spatial_methods = ( spatial_methods.copy() if spatial_methods is not None else None ) diff --git a/pybop/models/lithium_ion/base_echem.py b/pybop/models/lithium_ion/base_echem.py index fd4aa2682..f60f500d3 100644 --- a/pybop/models/lithium_ion/base_echem.py +++ b/pybop/models/lithium_ion/base_echem.py @@ -136,7 +136,7 @@ def _check_params( ): if self.param_check_counter <= len(electrode_params): infeasibility_warning = "Non-physical point encountered - [{material_vol_fraction} + {porosity}] > 1.0!" - warnings.warn(infeasibility_warning, UserWarning) + warnings.warn(infeasibility_warning, UserWarning, stacklevel=2) self.param_check_counter += 1 return allow_infeasible_solutions @@ -308,7 +308,7 @@ def approximate_capacity(self, inputs: Inputs): mean_sto_pos ) - negative_electrode_ocp(mean_sto_neg) except Exception as e: - raise ValueError(f"Error in average voltage calculation: {e}") + raise ValueError(f"Error in average voltage calculation: {e}") from e # Calculate and update nominal capacity theoretical_capacity = theoretical_energy / average_voltage diff --git a/pybop/models/lithium_ion/echem.py b/pybop/models/lithium_ion/echem.py index 8bd8ab636..9fdd308e3 100644 --- a/pybop/models/lithium_ion/echem.py +++ b/pybop/models/lithium_ion/echem.py @@ -1,8 +1,7 @@ from pybamm import lithium_ion as pybamm_lithium_ion from pybop.models.lithium_ion.base_echem import EChemBaseModel - -from .weppner_huggins import BaseWeppnerHuggins +from pybop.models.lithium_ion.weppner_huggins import BaseWeppnerHuggins class SPM(EChemBaseModel): diff --git a/pybop/models/lithium_ion/weppner_huggins.py b/pybop/models/lithium_ion/weppner_huggins.py index 5d8d626a4..74c42c70e 100644 --- a/pybop/models/lithium_ion/weppner_huggins.py +++ b/pybop/models/lithium_ion/weppner_huggins.py @@ -36,7 +36,7 @@ def __init__(self, name="Weppner & Huggins model", **model_kwargs): # Model kwargs (build, options) are not implemented, keeping here for consistent interface if model_kwargs is not dict(build=True): unused_kwargs_warning = "The input model_kwargs are not currently used by the Weppner & Huggins model." - warnings.warn(unused_kwargs_warning, UserWarning) + warnings.warn(unused_kwargs_warning, UserWarning, stacklevel=2) super().__init__({}, name) diff --git a/pybop/observers/observer.py b/pybop/observers/observer.py index 1c35c25df..f7d6f25f3 100644 --- a/pybop/observers/observer.py +++ b/pybop/observers/observer.py @@ -38,10 +38,14 @@ def __init__( parameters: Parameters, model: BaseModel, check_model=True, - signal=["Voltage [V]"], - additional_variables=[], + signal=None, + additional_variables=None, init_soc=None, ) -> None: + if additional_variables is None: + additional_variables = [] + if signal is None: + signal = ["Voltage [V]"] super().__init__( parameters, model, check_model, signal, additional_variables, init_soc ) diff --git a/pybop/observers/unscented_kalman.py b/pybop/observers/unscented_kalman.py index afbc2a010..60f4f0949 100644 --- a/pybop/observers/unscented_kalman.py +++ b/pybop/observers/unscented_kalman.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import List, Tuple, Union +from typing import Union import numpy as np import scipy.linalg as linalg @@ -41,17 +41,21 @@ class UnscentedKalmanFilterObserver(Observer): def __init__( self, - parameters: List[Parameter], + parameters: list[Parameter], model: BaseModel, sigma0: Union[Covariance, float], process: Union[Covariance, float], measure: Union[Covariance, float], dataset=None, check_model=True, - signal=["Voltage [V]"], - additional_variables=[], + signal=None, + additional_variables=None, init_soc=None, ) -> None: + if additional_variables is None: + additional_variables = [] + if signal is None: + signal = ["Voltage [V]"] super().__init__( parameters, model, check_model, signal, additional_variables, init_soc ) @@ -59,7 +63,7 @@ def __init__( self._dataset = dataset.data # Check that the dataset contains time and current - dataset.check(self.signal + ["Current function [A]"]) + dataset.check([*self.signal, "Current function [A]"]) self._time_data = self._dataset["Time [s]"] self.n_time_data = len(self._time_data) @@ -152,7 +156,7 @@ def get_current_covariance(self) -> Covariance: @dataclass -class SigmaPoint(object): +class SigmaPoint: """ A sigma point is a point in the state space that is used to estimate the mean and covariance of a random variable. """ @@ -162,7 +166,7 @@ class SigmaPoint(object): w_c: float -class SquareRootUKF(object): +class SquareRootUKF: """ van der Menve, R., & Wan, E. A. (2001). THE SQUARE-ROOT UNSCENTED KALMAN FILTER FOR STATE AND PARAMETER-ESTIMATION. https://doi.org/10.1109/ICASSP.2001.940586 @@ -235,7 +239,7 @@ def reset(self, x: np.ndarray, S: np.ndarray) -> None: @staticmethod def gen_sigma_points( x: np.ndarray, S: np.ndarray, alpha: float, beta: float, states: np.ndarray - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: """ Generates 2L+1 sigma points for the unscented transform, where L is the number of states. @@ -291,7 +295,7 @@ def unscented_transform( w_c: np.ndarray, sqrtR: np.ndarray, states: Union[np.ndarray, None] = None, - ) -> Tuple[np.ndarray, np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray]: """ Performs the unscented transform diff --git a/pybop/optimisers/_adamw.py b/pybop/optimisers/_adamw.py index 24e5ec982..817d8f290 100644 --- a/pybop/optimisers/_adamw.py +++ b/pybop/optimisers/_adamw.py @@ -157,7 +157,7 @@ def tell(self, reply): # Check ask-tell pattern if not self._ready_for_tell: - raise Exception("ask() not called before tell()") + raise RuntimeError("ask() not called before tell()") self._ready_for_tell = False # Unpack reply diff --git a/pybop/optimisers/base_optimiser.py b/pybop/optimisers/base_optimiser.py index 4693468f2..2b00b2345 100644 --- a/pybop/optimisers/base_optimiser.py +++ b/pybop/optimisers/base_optimiser.py @@ -1,4 +1,5 @@ import warnings +from typing import Optional import numpy as np @@ -75,8 +76,9 @@ def __init__( cost_test = cost(self.x0) warnings.warn( "The cost is not an instance of pybop.BaseCost, but let's continue " - + "assuming that it is a callable function to be minimised.", + "assuming that it is a callable function to be minimised.", UserWarning, + stacklevel=2, ) self.cost = cost for i, value in enumerate(self.x0): @@ -85,8 +87,10 @@ def __init__( ) self.minimising = True - except Exception: - raise Exception("The cost is not a recognised cost object or function.") + except Exception as e: + raise Exception( + "The cost is not a recognised cost object or function." + ) from e if not np.isscalar(cost_test) or not np.isreal(cost_test): raise TypeError( @@ -211,7 +215,7 @@ def check_optimal_parameters(self, x): else: warnings.warn( "Optimised parameters are not physically viable! \nConsider retrying the optimisation" - + " with a non-gradient-based optimiser and the option allow_infeasible_solutions=False", + " with a non-gradient-based optimiser and the option allow_infeasible_solutions=False", UserWarning, stacklevel=2, ) @@ -269,8 +273,8 @@ class Result: def __init__( self, x: np.ndarray = None, - final_cost: float = None, - n_iterations: int = None, + final_cost: Optional[float] = None, + n_iterations: Optional[int] = None, scipy_result=None, ): self.x = x diff --git a/pybop/optimisers/pints_optimisers.py b/pybop/optimisers/pints_optimisers.py index 64b77b674..83f13aa00 100644 --- a/pybop/optimisers/pints_optimisers.py +++ b/pybop/optimisers/pints_optimisers.py @@ -272,7 +272,7 @@ def __init__(self, cost, **optimiser_kwargs): if len(x0) == 1 or len(cost.parameters) == 1: raise ValueError( "CMAES requires optimisation of >= 2 parameters at once. " - + "Please choose another optimiser." + "Please choose another optimiser." ) super().__init__(cost, PintsCMAES, **optimiser_kwargs) diff --git a/pybop/optimisers/scipy_optimisers.py b/pybop/optimisers/scipy_optimisers.py index 544abfc88..30499cdb9 100644 --- a/pybop/optimisers/scipy_optimisers.py +++ b/pybop/optimisers/scipy_optimisers.py @@ -161,7 +161,7 @@ def callback(intermediate_result: OptimizeResult): # Compute the absolute initial cost and resample if required self._cost0 = np.abs(self.cost(self.x0)) if np.isinf(self._cost0): - for i in range(1, self.num_resamples): + for _i in range(1, self.num_resamples): self.x0 = self.parameters.rvs(1)[0] self._cost0 = np.abs(self.cost(self.x0)) if not np.isinf(self._cost0): diff --git a/pybop/parameters/parameter.py b/pybop/parameters/parameter.py index 27836fc80..093abaed9 100644 --- a/pybop/parameters/parameter.py +++ b/pybop/parameters/parameter.py @@ -1,12 +1,12 @@ import warnings from collections import OrderedDict -from typing import Dict, List, Union +from typing import Union import numpy as np from pybop._utils import is_numeric -Inputs = Dict[str, float] +Inputs = dict[str, float] class Parameter: @@ -219,7 +219,7 @@ def __getitem__(self, key: str) -> Parameter: def __len__(self) -> int: return len(self.param) - def keys(self) -> List: + def keys(self) -> list: """ A list of parameter names """ @@ -245,7 +245,7 @@ def add(self, parameter): if parameter.name in self.param.keys(): raise ValueError( f"There is already a parameter with the name {parameter.name} " - + "in the Parameters object. Please remove the duplicate entry." + "in the Parameters object. Please remove the duplicate entry." ) self.param[parameter.name] = parameter elif isinstance(parameter, dict): @@ -255,7 +255,7 @@ def add(self, parameter): if name in self.param.keys(): raise ValueError( f"There is already a parameter with the name {name} " - + "in the Parameters object. Please remove the duplicate entry." + "in the Parameters object. Please remove the duplicate entry." ) self.param[name] = Parameter(**parameter) else: @@ -287,7 +287,7 @@ def join(self, parameters=None): else: print(f"Discarding duplicate {param.name}.") - def get_bounds(self) -> Dict: + def get_bounds(self) -> dict: """ Get bounds, for either all or no parameters. """ @@ -317,12 +317,12 @@ def update(self, initial_values=None, values=None, bounds=None): if values is not None: param.update(value=values[i]) if bounds is not None: - if isinstance(bounds, Dict): + if isinstance(bounds, dict): param.set_bounds(bounds=[bounds["lower"][i], bounds["upper"][i]]) else: param.set_bounds(bounds=bounds[i]) - def rvs(self, n_samples: int) -> List: + def rvs(self, n_samples: int) -> list: """ Draw random samples from each parameter's prior distribution. @@ -355,7 +355,7 @@ def rvs(self, n_samples: int) -> List: return all_samples - def get_sigma0(self) -> List: + def get_sigma0(self) -> list: """ Get the standard deviation, for either all or no parameters. """ @@ -434,7 +434,7 @@ def get_bounds_for_plotly(self): return bounds - def as_dict(self, values=None) -> Dict: + def as_dict(self, values=None) -> dict: """ Parameters ---------- @@ -465,7 +465,7 @@ def verify(self, inputs: Union[Inputs, None] = None): ---------- inputs : Inputs or numeric """ - if inputs is None or isinstance(inputs, Dict): + if inputs is None or isinstance(inputs, dict): return inputs elif (isinstance(inputs, list) and all(is_numeric(x) for x in inputs)) or all( is_numeric(x) for x in list(inputs) diff --git a/pybop/parameters/parameter_set.py b/pybop/parameters/parameter_set.py index 43f3e999b..c1b9505b5 100644 --- a/pybop/parameters/parameter_set.py +++ b/pybop/parameters/parameter_set.py @@ -1,6 +1,5 @@ import json import types -from typing import List from pybamm import LithiumIonParameters, ParameterValues, parameter_sets @@ -35,7 +34,7 @@ def __setitem__(self, key, value): def __getitem__(self, key): return self.params[key] - def keys(self) -> List: + def keys(self) -> list: """ A list of parameter names """ @@ -67,7 +66,7 @@ def import_parameters(self, json_path=None): # Read JSON file if not self.params and self.json_path: - with open(self.json_path, "r") as file: + with open(self.json_path) as file: self.params = json.load(file) else: raise ValueError( @@ -139,7 +138,7 @@ def export_parameters(self, output_json_path, fit_params=None): # Update parameter set if fit_params is not None: - for i, param in enumerate(fit_params): + for _i, param in enumerate(fit_params): exportable_params.update({param.name: param.value}) # Replace non-serializable values diff --git a/pybop/plotting/plot2d.py b/pybop/plotting/plot2d.py index ee8d70573..781b697ba 100644 --- a/pybop/plotting/plot2d.py +++ b/pybop/plotting/plot2d.py @@ -71,8 +71,9 @@ def plot2d( if len(cost.parameters) > 2: warnings.warn( "This cost function requires more than 2 parameters. " - + "Plotting in 2d with fixed values for the additional parameters.", + "Plotting in 2d with fixed values for the additional parameters.", UserWarning, + stacklevel=2, ) for ( i, diff --git a/pybop/plotting/plot_dataset.py b/pybop/plotting/plot_dataset.py index 70573e476..ecc84aa6e 100644 --- a/pybop/plotting/plot_dataset.py +++ b/pybop/plotting/plot_dataset.py @@ -3,9 +3,7 @@ from pybop import StandardPlot, plot_trajectories -def plot_dataset( - dataset, signal=["Voltage [V]"], trace_names=None, show=True, **layout_kwargs -): +def plot_dataset(dataset, signal=None, trace_names=None, show=True, **layout_kwargs): """ Quickly plot a PyBOP Dataset using Plotly. @@ -31,6 +29,8 @@ def plot_dataset( """ # Get data dictionary + if signal is None: + signal = ["Voltage [V]"] dataset.check(signal) # Compile ydata and labels or legend diff --git a/pybop/plotting/plotly_manager.py b/pybop/plotting/plotly_manager.py index 7b4b079a4..5554a2af5 100644 --- a/pybop/plotting/plotly_manager.py +++ b/pybop/plotting/plotly_manager.py @@ -119,7 +119,7 @@ def check_browser_availability(self): if self.pio and self.pio.renderers.default == "browser": try: webbrowser.get() - except webbrowser.Error: + except webbrowser.Error as e: raise Exception( "\n **Browser Not Found** \nFor Windows users, in order to view figures in the browser using Plotly, " "you need to set the environment variable BROWSER equal to the " @@ -129,4 +129,4 @@ def check_browser_availability(self): "\n\nThen reactivate your virtual environment. Alternatively, you can use a " "different Plotly renderer. For more information see: " "https://plotly.com/python/renderers/#setting-the-default-renderer" - ) + ) from e diff --git a/pybop/plotting/quick_plot.py b/pybop/plotting/quick_plot.py index 5be353a62..1ef4e3ffe 100644 --- a/pybop/plotting/quick_plot.py +++ b/pybop/plotting/quick_plot.py @@ -57,16 +57,16 @@ def __init__( x, y, layout=None, - layout_options=DEFAULT_LAYOUT_OPTIONS.copy(), - trace_options=DEFAULT_TRACE_OPTIONS.copy(), + layout_options=DEFAULT_LAYOUT_OPTIONS, + trace_options=DEFAULT_TRACE_OPTIONS, trace_names=None, trace_name_width=40, ): self.x = x self.y = y self.layout = layout - self.layout_options = layout_options - self.trace_options = DEFAULT_TRACE_OPTIONS.copy() + self.layout_options = layout_options.copy() + self.trace_options = trace_options.copy() if trace_options is not None: for arg, value in trace_options.items(): self.trace_options[arg] = value @@ -246,9 +246,9 @@ def __init__( num_cols=None, axis_titles=None, layout=None, - layout_options=DEFAULT_LAYOUT_OPTIONS.copy(), - subplot_options=DEFAULT_SUBPLOT_OPTIONS.copy(), - trace_options=DEFAULT_SUBPLOT_TRACE_OPTIONS.copy(), + layout_options=DEFAULT_LAYOUT_OPTIONS, + subplot_options=DEFAULT_SUBPLOT_OPTIONS, + trace_options=DEFAULT_SUBPLOT_TRACE_OPTIONS, trace_names=None, trace_name_width=40, ): @@ -267,7 +267,7 @@ def __init__( elif self.num_cols is None: self.num_cols = int(math.ceil(self.num_traces / self.num_rows)) self.axis_titles = axis_titles - self.subplot_options = DEFAULT_SUBPLOT_OPTIONS.copy() + self.subplot_options = subplot_options.copy() if subplot_options is not None: for arg, value in subplot_options.items(): self.subplot_options[arg] = value diff --git a/pybop/problems/base_problem.py b/pybop/problems/base_problem.py index 4d9d85194..ee36a6bb4 100644 --- a/pybop/problems/base_problem.py +++ b/pybop/problems/base_problem.py @@ -27,11 +27,15 @@ def __init__( parameters, model=None, check_model=True, - signal=["Voltage [V]"], - additional_variables=[], + signal=None, + additional_variables=None, init_soc=None, ): # Check if parameters is a list of pybop.Parameter objects + if additional_variables is None: + additional_variables = [] + if signal is None: + signal = ["Voltage [V]"] if isinstance(parameters, list): if all(isinstance(param, Parameter) for param in parameters): parameters = Parameters(*parameters) diff --git a/pybop/problems/design_problem.py b/pybop/problems/design_problem.py index a1efa22fd..30e2d9c3a 100644 --- a/pybop/problems/design_problem.py +++ b/pybop/problems/design_problem.py @@ -34,11 +34,15 @@ def __init__( parameters, experiment, check_model=True, - signal=["Voltage [V]"], - additional_variables=[], + signal=None, + additional_variables=None, init_soc=None, ): # Add time and current and remove duplicates + if additional_variables is None: + additional_variables = [] + if signal is None: + signal = ["Voltage [V]"] additional_variables.extend(["Time [s]", "Current [A]"]) additional_variables = list(set(additional_variables)) diff --git a/pybop/problems/fitting_problem.py b/pybop/problems/fitting_problem.py index b27955479..58d59e9ee 100644 --- a/pybop/problems/fitting_problem.py +++ b/pybop/problems/fitting_problem.py @@ -32,11 +32,15 @@ def __init__( parameters, dataset, check_model=True, - signal=["Voltage [V]"], - additional_variables=[], + signal=None, + additional_variables=None, init_soc=None, ): # Add time and remove duplicates + if additional_variables is None: + additional_variables = [] + if signal is None: + signal = ["Voltage [V]"] additional_variables.extend(["Time [s]"]) additional_variables = list(set(additional_variables)) @@ -47,7 +51,7 @@ def __init__( self.parameters.initial_value() # Check that the dataset contains time and current - dataset.check(self.signal + ["Current function [A]"]) + dataset.check([*self.signal, "Current function [A]"]) # Unpack time and target data self._time_data = self._dataset["Time [s]"] diff --git a/pyproject.toml b/pyproject.toml index b101d343e..0ec781e58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,10 +81,23 @@ addopts = "--showlocals -v -n auto" [tool.ruff] extend-include = ["*.ipynb"] extend-exclude = ["__init__.py"] +fix = true [tool.ruff.lint] -extend-select = ["I"] +select = [ + "A", # flake8-builtins: Check for Python builtins being used as variables or parameters + "B", # flake8-bugbear: Find likely bugs and design problems + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes: Detect various errors by parsing the source file + "I", # isort: Check and enforce import ordering + "ISC", # flake8-implicit-str-concat: Check for implicit string concatenation + "TID", # flake8-tidy-imports: Validate import hygiene + "UP", # pyupgrade: Automatically upgrade syntax for newer versions of Python +] + ignore = ["E501","E741"] +per-file-ignores = {"**.ipynb" = ["E402", "E703"]} -[tool.ruff.lint.per-file-ignores] -"**.ipynb" = ["E402", "E703"] +[tool.ruff.lint.flake8-tidy-imports] +ban-relative-imports = "all" diff --git a/tests/examples/test_examples.py b/tests/examples/test_examples.py index 1c45be840..2bebc6fc6 100644 --- a/tests/examples/test_examples.py +++ b/tests/examples/test_examples.py @@ -12,14 +12,14 @@ class TestExamples: """ def list_of_examples(): - list = [] + examples_list = [] path_to_example_scripts = os.path.join( pybop.script_path, "..", "examples", "scripts" ) for example in os.listdir(path_to_example_scripts): if example.endswith(".py"): - list.append(os.path.join(path_to_example_scripts, example)) - return list + examples_list.append(os.path.join(path_to_example_scripts, example)) + return examples_list @pytest.mark.parametrize("example", list_of_examples()) @pytest.mark.examples diff --git a/tests/unit/test_dataset.py b/tests/unit/test_dataset.py index 9c4eac13d..618b8ad53 100644 --- a/tests/unit/test_dataset.py +++ b/tests/unit/test_dataset.py @@ -20,7 +20,7 @@ def test_dataset(self): data_dictionary = { "Time [s]": solution["Time [s]"].data, "Current [A]": solution["Current [A]"].data, - "Terminal voltage [V]": solution["Terminal voltage [V]"].data, + "Voltage [V]": solution["Voltage [V]"].data, } dataset = pybop.Dataset(data_dictionary) @@ -55,4 +55,4 @@ def test_dataset(self): dataset["Time"] # Test conversion of single signal to list - assert dataset.check(signal="Terminal voltage [V]") + assert dataset.check() diff --git a/tests/unit/test_observer_unscented_kalman.py b/tests/unit/test_observer_unscented_kalman.py index ce60abbc0..0b5d3067b 100644 --- a/tests/unit/test_observer_unscented_kalman.py +++ b/tests/unit/test_observer_unscented_kalman.py @@ -156,3 +156,22 @@ def test_wrong_input_shapes(self, model, parameters): pybop.UnscentedKalmanFilterObserver( parameters, model, sigma0, process, measure, signal=signal ) + + @pytest.mark.unit + def test_without_signal(self): + model = pybop.lithium_ion.SPM() + parameters = pybop.Parameters( + pybop.Parameter( + "Negative electrode active material volume fraction", + prior=pybop.Gaussian(0.5, 0.05), + ) + ) + model.build(parameters=parameters) + n = model.n_states + sigma0 = np.diag([1e-4] * n) + process = np.diag([1e-4] * n) + measure = np.diag([1e-4]) + observer = pybop.UnscentedKalmanFilterObserver( + parameters, model, sigma0, process, measure + ) + assert observer.signal == ["Voltage [V]"] diff --git a/tests/unit/test_optimisation.py b/tests/unit/test_optimisation.py index 8444159b9..c61d2fae2 100644 --- a/tests/unit/test_optimisation.py +++ b/tests/unit/test_optimisation.py @@ -236,7 +236,7 @@ def test_optimiser_kwargs(self, cost, optimiser): assert optim.pints_optimiser._lambda == 0.1 # Incorrect values - for i, match in (("Value", -1),): + for i, _match in (("Value", -1),): with pytest.raises( Exception, match="must be a numeric value between 0 and 1." ): @@ -253,7 +253,7 @@ def test_optimiser_kwargs(self, cost, optimiser): # Check defaults assert optim.pints_optimiser.n_hyper_parameters() == 5 assert optim.pints_optimiser.x_guessed() == optim.pints_optimiser._x0 - with pytest.raises(Exception): + with pytest.raises(RuntimeError): optim.pints_optimiser.tell([0.1]) else: @@ -326,12 +326,8 @@ def test_default_optimiser(self, cost): optim = pybop.Optimisation(cost=cost) assert optim.name() == "Exponential Natural Evolution Strategy (xNES)" - # Test incorrect setting attribute - with pytest.raises( - AttributeError, - match="'Optimisation' object has no attribute 'not_a_valid_attribute'", - ): - optim.not_a_valid_attribute + # Test getting incorrect attribute + assert not hasattr(optim, "not_a_valid_attribute") @pytest.mark.unit def test_incorrect_optimiser_class(self, cost): diff --git a/tests/unit/test_parameters.py b/tests/unit/test_parameters.py index ebfccea12..90c43622c 100644 --- a/tests/unit/test_parameters.py +++ b/tests/unit/test_parameters.py @@ -130,8 +130,8 @@ def test_parameters_construction(self, parameter): with pytest.raises( ValueError, match="There is already a parameter with the name " - + "Negative electrode active material volume fraction" - + " in the Parameters object. Please remove the duplicate entry.", + "Negative electrode active material volume fraction" + " in the Parameters object. Please remove the duplicate entry.", ): params.add(parameter) @@ -158,8 +158,8 @@ def test_parameters_construction(self, parameter): with pytest.raises( ValueError, match="There is already a parameter with the name " - + "Negative electrode active material volume fraction" - + " in the Parameters object. Please remove the duplicate entry.", + "Negative electrode active material volume fraction" + " in the Parameters object. Please remove the duplicate entry.", ): params.add( dict( diff --git a/tests/unit/test_plots.py b/tests/unit/test_plots.py index 4c7e14d42..79015c006 100644 --- a/tests/unit/test_plots.py +++ b/tests/unit/test_plots.py @@ -66,7 +66,7 @@ def test_dataset_plots(self, dataset): dataset["Voltage [V]"], trace_names=["Time [s]", "Voltage [V]"], ) - pybop.plot_dataset(dataset, signal=["Voltage [V]"]) + pybop.plot_dataset(dataset) @pytest.fixture def fitting_problem(self, model, parameters, dataset):