From 5024850831c5fdbfb8e17347bf8ae878f2e15f77 Mon Sep 17 00:00:00 2001 From: RohitP2005 Date: Wed, 15 Jan 2025 01:21:10 +0530 Subject: [PATCH] Initial Implementation --- noxfile.py | 9 ++ src/pybamm/expression_tree/symbol.py | 21 ++-- src/pybamm/parameters/parameter_values.py | 13 ++- src/pybamm/simulation.py | 12 ++- src/pybamm/solvers/base_solver.py | 30 +++--- utils/__init__.py | 1 + utils/decorators.py | 102 +++++++++++++++++++ utils/exceptions.py | 115 ++++++++++++++++++++++ 8 files changed, 272 insertions(+), 31 deletions(-) create mode 100644 utils/__init__.py create mode 100644 utils/decorators.py create mode 100644 utils/exceptions.py diff --git a/noxfile.py b/noxfile.py index 7892ad86ac..9e59cee319 100644 --- a/noxfile.py +++ b/noxfile.py @@ -141,10 +141,19 @@ def run_tests(session): set_environment_variables(PYBAMM_ENV, session=session) session.install("setuptools", silent=False) session.install("-e", ".[all,dev,jax]", silent=False) + warning_flags = [ + "-W", + "error::DeprecationWarning", + "-W", + "error::PendingDeprecationWarning", + "-W", + "error::FutureWarning", + ] session.run( "python", "-m", "pytest", + *warning_flags, *(session.posargs if session.posargs else ["-m", "unit or integration"]), ) diff --git a/src/pybamm/expression_tree/symbol.py b/src/pybamm/expression_tree/symbol.py index 3f695b768e..d7697e4518 100644 --- a/src/pybamm/expression_tree/symbol.py +++ b/src/pybamm/expression_tree/symbol.py @@ -3,7 +3,6 @@ # from __future__ import annotations import numbers -import warnings import numpy as np import sympy @@ -15,6 +14,7 @@ import pybamm from pybamm.util import import_optional_dependency from pybamm.expression_tree.printing.print_name import prettify_print_name +from utils import deprecate_function if TYPE_CHECKING: # pragma: no cover import casadi @@ -356,6 +356,9 @@ def domain(self, domain): ) @property + @deprecate_function( + msg="symbol.auxiliary_domains has been deprecated, use symbol.domains instead" + ) def auxiliary_domains(self): """Returns auxiliary domains.""" raise NotImplementedError( @@ -994,18 +997,20 @@ def create_copy( children = self._children_for_copying(new_children) return self.__class__(self.name, children, domains=self.domains) + # Assuming `deprecate_function` is imported from deprecation_decorators + + @deprecate_function( + version="2.0.0", + msg="The 'new_copy' function for expression tree symbols is deprecated, use 'create_copy' instead.", + ) def new_copy( self, new_children: list[Symbol] | None = None, perform_simplifications: bool = True, ): - """ """ - warnings.warn( - "The 'new_copy' function for expression tree symbols is deprecated, use " - "'create_copy' instead.", - DeprecationWarning, - stacklevel=2, - ) + """ + This function is deprecated. Use 'create_copy' instead. + """ return self.create_copy(new_children, perform_simplifications) @cached_property diff --git a/src/pybamm/parameters/parameter_values.py b/src/pybamm/parameters/parameter_values.py index ba8c6c324e..cc830c4026 100644 --- a/src/pybamm/parameters/parameter_values.py +++ b/src/pybamm/parameters/parameter_values.py @@ -4,6 +4,7 @@ from pprint import pformat from warnings import warn from collections import defaultdict +from utils import deprecate_multiple_renamedParameters class ParameterValues: @@ -417,18 +418,24 @@ def set_initial_ocps( return parameter_values @staticmethod + @deprecate_multiple_renamedParameters( + { + "propotional term": "... proportional term [s-1]", + "1 + dlnf/dlnc": "Thermodynamic factor", + "electrode diffusivity": "particle diffusivity", + } + ) def check_parameter_values(values): for param in list(values.keys()): if "propotional term" in param: raise ValueError( f"The parameter '{param}' has been renamed to " "'... proportional term [s-1]', and its value should now be divided" - "by 3600 to get the same results as before." + " by 3600 to get the same results as before." ) - # specific check for renamed parameter "1 + dlnf/dlnc" if "1 + dlnf/dlnc" in param: raise ValueError( - f"parameter '{param}' has been renamed to 'Thermodynamic factor'" + f"The parameter '{param}' has been renamed to 'Thermodynamic factor'" ) if "electrode diffusivity" in param: new_param = param.replace("electrode", "particle") diff --git a/src/pybamm/simulation.py b/src/pybamm/simulation.py index 7bd1e5eea0..c2c6cf4d3b 100644 --- a/src/pybamm/simulation.py +++ b/src/pybamm/simulation.py @@ -8,6 +8,7 @@ from datetime import timedelta import pybamm.telemetry from pybamm.util import import_optional_dependency +from utils import deprecate_function from pybamm.expression_tree.operations.serialise import Serialise @@ -171,7 +172,11 @@ def _set_random_seed(self): % (2**32) ) + @deprecate_function( + msg="pybamm.simulation.set_up_and_parameterise_experiment is deprecated and not meant to be accessed by users." + ) def set_up_and_parameterise_experiment(self, solve_kwargs=None): + """Sets up and parameterizes the experiment.""" msg = "pybamm.simulation.set_up_and_parameterise_experiment is deprecated and not meant to be accessed by users." warnings.warn(msg, DeprecationWarning, stacklevel=2) self._set_up_and_parameterise_experiment(solve_kwargs=solve_kwargs) @@ -254,12 +259,17 @@ def _set_up_and_parameterise_experiment(self, solve_kwargs=None): parameterised_model ) + @deprecate_function( + version="2.0.0", + msg="pybamm.set_parameters is deprecated and not meant to be accessed by users.", + ) def set_parameters(self): + """Sets the parameters.""" msg = ( "pybamm.set_parameters is deprecated and not meant to be accessed by users." ) warnings.warn(msg, DeprecationWarning, stacklevel=2) - self._set_parameters() + self._set_parameters() # Call the internal method def _set_parameters(self): """ diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index 9b19f0a8a0..03f3a00dcb 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -13,6 +13,7 @@ import pybamm from pybamm.expression_tree.binary_operators import _Heaviside from pybamm import ParameterValues +from utils import deprecated_params class BaseSolver: @@ -1162,6 +1163,14 @@ def process_t_interp(self, t_interp): return t_interp + @deprecated_params( + { + "npts": ( + "t_eval", + "The 'npts' parameter is deprecated, use 't_eval' instead.", + ) + } + ) def step( self, old_solution, @@ -1199,12 +1208,6 @@ def step( Save solution with all previous timesteps. Defaults to True. calculate_sensitivities : list of str or bool, optional Whether the solver calculates sensitivities of all input parameters. Defaults to False. - If only a subset of sensitivities are required, can also pass a - list of input parameter names. **Limitations**: sensitivities are not calculated up to numerical tolerances - so are not guarenteed to be within the tolerances set by the solver, please raise an issue if you - require this functionality. Also, when using this feature with `pybamm.Experiment`, the sensitivities - do not take into account the movement of step-transitions wrt input parameters, so do not use this feature - if the timings of your experimental protocol change rapidly with respect to your input parameters. t_interp : None, list or ndarray, optional The times (in seconds) at which to interpolate the solution. Defaults to None. Only valid for solvers that support intra-solve interpolation (`IDAKLUSolver`). @@ -1223,31 +1226,20 @@ def step( or old_solution.termination == "final time" or "[experiment]" in old_solution.termination ): - # Return same solution as an event has already been triggered - # With hack to allow stepping past experiment current / voltage cut-off return old_solution - # Make sure model isn't empty self._check_empty_model(model) - # Make sure dt is greater than zero if dt <= 0: raise pybamm.SolverError("Step time must be >0") - # Raise deprecation warning for npts and convert it to t_eval - if npts is not None: - warnings.warn( - "The 'npts' parameter is deprecated, use 't_eval' instead.", - DeprecationWarning, - stacklevel=2, - ) - t_eval = np.linspace(0, dt, npts) - elif t_eval is None: + if t_eval is None: t_eval = np.array([0, dt]) elif t_eval[0] != 0 or t_eval[-1] != dt: raise pybamm.SolverError( "Elements inside array t_eval must lie in the closed interval 0 to dt" ) + else: pass diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000000..48bad8326a --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1 @@ +from .decorators import * diff --git a/utils/decorators.py b/utils/decorators.py new file mode 100644 index 0000000000..90f08493a1 --- /dev/null +++ b/utils/decorators.py @@ -0,0 +1,102 @@ +import functools +import warnings +from .exceptions import DeprecatedFunctionWarning + + +def deprecate_function(func=None, version=None, msg=None): + """ + A decorator to mark a function as deprecated. + + Parameters: + - func: The function to decorate. If no function is provided, the decorator will be used as a factory. + - version: The version in which the function was deprecated. + - msg: Custom message to display alongside the deprecation warning. + + If no message is provided, a default message is used. + """ + + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + # Construct the warning message + message = ( + msg + or f"Function '{func.__name__}' is deprecated and will be removed in future versions." + ) + if version: + message += f" Deprecated since version {version}." + + # Raise the deprecation warning + warnings.warn(message, category=DeprecatedFunctionWarning, stacklevel=2) + + return func(*args, **kwargs) + + return wrapper + + # If no function is passed (when used as a decorator with parameters) + if func is None: + return decorator + else: + return decorator(func) + + +def deprecate_multiple_renamedParameters(param_dict: dict): + """ + Decorator to handle deprecated parameter names dynamically, issuing warnings when old + parameter names are found and replacing them with the new ones provided in the param_dict. + param_dict is a dictionary mapping old parameter names to new parameter names. + """ + + def decorator(func): + @functools.wraps(func) + def wrapper(self, values, *args, **kwargs): + # Iterate over the old-to-new parameter mapping + for old_param, new_param in param_dict.items(): + if old_param in values: + # Issue a deprecation warning and update the parameter + warnings.warn( + f"The parameter '{old_param}' has been renamed to '{new_param}'", + DeprecationWarning, + stacklevel=2, + ) + values[new_param] = values.pop(old_param) + # Call the original function with the updated values + return func(self, values, *args, **kwargs) + + return wrapper + + return decorator + + +def deprecated_params(param_map): + """ + A decorator to handle deprecated parameters in a function. + + Parameters + ---------- + param_map : dict + A dictionary mapping deprecated parameter names to a tuple containing the + new parameter name (or None if it's removed) and a message explaining the deprecation. + + Example + ------- + @handle_deprecated_params({ + 'npts': ('t_eval', "The 'npts' parameter is deprecated, use 't_eval' instead.") + }) + def step(...): + ... + """ + + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + for deprecated_param, (new_param, message) in param_map.items(): + if deprecated_param in kwargs: + warnings.warn(message, DeprecationWarning, stacklevel=2) + if new_param: + kwargs[new_param] = kwargs.pop(deprecated_param) + return func(*args, **kwargs) + + return wrapper + + return decorator diff --git a/utils/exceptions.py b/utils/exceptions.py new file mode 100644 index 0000000000..c8466dda49 --- /dev/null +++ b/utils/exceptions.py @@ -0,0 +1,115 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +""" +This module contains errors/exceptions and warnings for deprecation handling. +""" + +__all__ = [ + "DeprecatedArgumentWarning", + "DeprecatedFunctionWarning", + "DeprecatedModuleWarning", + "DeprecationWarning", + "NoValue", + "RenamedArgumentWarning", +] + + +class DeprecationWarning(Warning): + """ + The base warning class from which all deprecation-related warnings should inherit. + + Any warning inheriting from this class can be handled by a logger or displayed + accordingly. + """ + + +class DeprecatedFunctionWarning(DeprecationWarning): + """ + A warning class to indicate a deprecated function. + """ + + def __init__( + self, + func_name, + message="This function is deprecated and will be removed in future versions.", + ): + self.func_name = func_name + full_message = f"Function '{func_name}': {message}" + super().__init__(full_message) + + +class DeprecatedModuleWarning(DeprecationWarning): + """ + A warning class to indicate a deprecated module. + """ + + def __init__( + self, + module_name, + message="This module is deprecated and will be removed in future versions.", + ): + self.module_name = module_name + full_message = f"Module '{module_name}': {message}" + super().__init__(full_message) + + +class DeprecatedArgumentWarning(DeprecationWarning): + """ + A warning class to indicate a deprecated argument in a function. + """ + + def __init__( + self, + arg_name, + func_name, + message="This argument is deprecated and will be removed in future versions.", + ): + self.arg_name = arg_name + self.func_name = func_name + full_message = f"Argument '{arg_name}' in function '{func_name}': {message}" + super().__init__(full_message) + + +class RenamedArgumentWarning(DeprecationWarning): + """ + A warning class to indicate a renamed argument in a function. + """ + + def __init__( + self, + old_arg_name, + new_arg_name, + func_name, + message="This argument has been renamed.", + ): + self.old_arg_name = old_arg_name + self.new_arg_name = new_arg_name + self.func_name = func_name + full_message = f"Argument '{old_arg_name}' in function '{func_name}' has been renamed to '{new_arg_name}': {message}" + super().__init__(full_message) + + +class _NoValue: + """Special keyword value for deprecated arguments to check if they have been assigned a user-defined value.""" + + def __repr__(self): + return "NoValue" + + +NoValue = _NoValue() + + +def __getattr__(name: str): + if name in ("DeprecationWarning", "RenamedArgumentWarning"): + import warnings + + warnings.warn( + f"Importing {name} from this module is deprecated and will be removed in a future version. " + "Please use the respective classes directly.", + category=DeprecationWarning, + stacklevel=1, + ) + + # Handle further imports if necessary + return globals().get(name) + + raise AttributeError(f"Module {__name__!r} has no attribute {name!r}.")