from __future__ import annotations
import json
import re
from collections import defaultdict
from collections.abc import Iterator
from pathlib import Path
from pprint import pformat
from typing import TYPE_CHECKING, Any
from warnings import warn
import numpy as np
import pybamm
from pybamm.expression_tree.operations.serialise import (
Serialise,
convert_function_to_symbolic_expression,
convert_symbol_from_json,
convert_symbol_to_json,
)
from pybamm.models.full_battery_models.lithium_ion.msmr import (
is_deprecated_msmr_name,
replace_deprecated_msmr_name,
)
from .parameter_store import (
ParameterCategory,
ParameterDiff,
ParameterInfo,
ParameterStore,
)
from .parameter_substitutor import ParameterSubstitutor
if TYPE_CHECKING:
from collections.abc import Mapping
[docs]
class ParameterValues:
"""
The parameter values for a simulation.
Note that this class does not inherit directly from the python dictionary class as
this causes issues with saving and loading simulations.
Parameters
----------
values : dict or string or ParameterValues
Explicit set of parameters, or reference to an inbuilt parameter set.
If string and matches one of the inbuilt parameter sets, returns that
parameter set.
Examples
--------
>>> values = {"some parameter": 1, "another parameter": 2}
>>> param = pybamm.ParameterValues(values)
>>> param["some parameter"]
1
>>> param = pybamm.ParameterValues("Marquis2019")
>>> param["Reference temperature [K]"]
298.15
>>> info = param.get_info("Reference temperature [K]")
>>> info.units
'K'
>>> electrode_params = param.list_by_category("negative electrode")
>>> len(electrode_params) > 0
True
"""
# Physical constants are deprecated in ParameterValues
_DEPRECATED_CONSTANTS = {
"Ideal gas constant [J.K-1.mol-1]": "pybamm.constants.R",
"Faraday constant [C.mol-1]": "pybamm.constants.F",
"Boltzmann constant [J.K-1]": "pybamm.constants.k_b",
"Electron charge [C]": "pybamm.constants.q_e",
}
def __init__(self, values: dict[str, Any] | str | ParameterValues) -> None:
# Initialize the store
self._store = ParameterStore({})
# Initialize the processor (uses this instance's store)
self._processor = ParameterSubstitutor(self._store)
if isinstance(values, dict | ParameterValues):
# Copy to avoid mutating input
if isinstance(values, ParameterValues):
values_dict = dict(values._store._data)
else:
values_dict = dict(values)
# Remove the "chemistry" key if it exists
chemistry = values_dict.pop("chemistry", None)
self.update(values_dict)
else:
# Check if values is a named parameter set
if isinstance(values, str) and values in pybamm.parameter_sets.keys():
values_dict = dict(pybamm.parameter_sets[values])
chemistry = values_dict.pop("chemistry", None)
self.update(values_dict)
else:
valid_sets = "\n".join(pybamm.parameter_sets.keys())
raise ValueError(
f"'{values}' is not a valid parameter set. "
f"Parameter set must be one of:\n{valid_sets}"
)
if chemistry == "ecm":
self._set_initial_state = pybamm.equivalent_circuit.set_initial_state
else:
self._set_initial_state = pybamm.lithium_ion.set_initial_state
# Save citations
if "citations" in self._store:
for citation in self._store["citations"]:
pybamm.citations.register(citation)
@property
def store(self) -> ParameterStore:
return self._store
# Factory methods
[docs]
@classmethod
def create_from_bpx(
cls, filename: str | Path, target_soc: float = 1.0
) -> ParameterValues:
"""
Create ParameterValues from a BPX file.
Parameters
----------
filename : str or Path
The filename of the `BPX <https://bpxstandard.com/>`_ file.
target_soc : float, optional
Target state of charge. Must be between 0 and 1. Default is 1.
Returns
-------
ParameterValues
A parameter values object with the parameters from the BPX file.
Examples
--------
>>> param = pybamm.ParameterValues.create_from_bpx("battery_params.json") # doctest: +SKIP
>>> param = pybamm.ParameterValues.create_from_bpx("battery_params.json", target_soc=0.5) # doctest: +SKIP
"""
from bpx import parse_bpx_file
bpx = parse_bpx_file(str(filename))
return cls._create_from_bpx(bpx, target_soc)
[docs]
@classmethod
def create_from_bpx_obj(
cls, bpx_obj: dict, target_soc: float = 1.0
) -> ParameterValues:
"""
Create ParameterValues from a BPX dictionary object.
Parameters
----------
bpx_obj : dict
A dictionary containing the parameters in the
`BPX <https://bpxstandard.com/>`_ format.
target_soc : float, optional
Target state of charge. Must be between 0 and 1. Default is 1.
Returns
-------
ParameterValues
A parameter values object with the parameters in the BPX file.
Examples
--------
>>> bpx_dict = {"Header": {...}, "Cell": {...}, "Parameterisation": {...}} # doctest: +SKIP
>>> param = pybamm.ParameterValues.create_from_bpx_obj(bpx_dict) # doctest: +SKIP
>>> param = pybamm.ParameterValues.create_from_bpx_obj(bpx_dict, target_soc=0.8) # doctest: +SKIP
"""
from bpx import parse_bpx_obj
bpx = parse_bpx_obj(bpx_obj)
return cls._create_from_bpx(bpx, target_soc)
@classmethod
def _create_from_bpx(cls, bpx, target_soc: float) -> ParameterValues:
"""Internal method to create ParameterValues from a parsed BPX object."""
from bpx import get_electrode_concentrations
from bpx.schema import ElectrodeBlended, ElectrodeBlendedSPM
from .bpx import bpx_to_param_dict
if target_soc < 0 or target_soc > 1:
raise ValueError("Target SOC should be between 0 and 1")
pybamm_dict = bpx_to_param_dict(bpx)
if "Open-circuit voltage at 0% SOC [V]" not in pybamm_dict:
pybamm_dict["Open-circuit voltage at 0% SOC [V]"] = pybamm_dict[
"Lower voltage cut-off [V]"
]
warn(
"'Open-circuit voltage at 0% SOC [V]' not found in BPX file. Using "
"'Lower voltage cut-off [V]'.",
stacklevel=2,
)
if "Open-circuit voltage at 100% SOC [V]" not in pybamm_dict:
pybamm_dict["Open-circuit voltage at 100% SOC [V]"] = pybamm_dict[
"Upper voltage cut-off [V]"
]
warn(
"'Open-circuit voltage at 100% SOC [V]' not found in BPX file. Using "
"'Upper voltage cut-off [V]'.",
stacklevel=2,
)
# Get initial concentrations based on SOC
bpx_neg = bpx.parameterisation.negative_electrode
bpx_pos = bpx.parameterisation.positive_electrode
if isinstance(bpx_neg, ElectrodeBlended | ElectrodeBlendedSPM) or isinstance(
bpx_pos, ElectrodeBlended | ElectrodeBlendedSPM
):
pybamm.logger.warning(
"Initial concentrations cannot be set using stoichiometry limits for "
"blend electrodes. Please set the initial concentrations manually."
)
else:
c_n_init, c_p_init = get_electrode_concentrations(target_soc, bpx)
pybamm_dict["Initial concentration in negative electrode [mol.m-3]"] = (
c_n_init
)
pybamm_dict["Initial concentration in positive electrode [mol.m-3]"] = (
c_p_init
)
return cls(pybamm_dict)
[docs]
@staticmethod
def from_json(filename_or_dict: str | Path | dict) -> ParameterValues:
"""
Load a ParameterValues object from a JSON file or a dictionary.
Parameters
----------
filename_or_dict : str, Path, or dict
The filename to load the JSON file from, or a dictionary.
Returns
-------
ParameterValues
The ParameterValues object.
Examples
--------
>>> param = pybamm.ParameterValues.from_json("parameters.json") # doctest: +SKIP
>>> param_dict = {"Temperature [K]": 298.15}
>>> param = pybamm.ParameterValues.from_json(param_dict)
"""
if isinstance(filename_or_dict, str | Path):
with open(filename_or_dict) as f:
parameter_values_dict = json.load(f)
elif isinstance(filename_or_dict, dict):
parameter_values_dict = filename_or_dict.copy()
else:
raise TypeError("Input must be a filename (str or pathlib.Path) or a dict")
for key, value in parameter_values_dict.items():
if isinstance(value, dict):
parameter_values_dict[key] = convert_symbol_from_json(value)
return ParameterValues(parameter_values_dict)
from_config = from_json
[docs]
def to_json(self, filename: str | None = None) -> dict:
"""
Convert the parameter values to a JSON-serializable dictionary.
Optionally saves to a file.
Parameters
----------
filename : str, optional
The filename to save the JSON file to. If not provided, the
dictionary is not saved.
Returns
-------
dict
The JSON-serializable dictionary.
Examples
--------
>>> param = pybamm.ParameterValues({"Temperature [K]": 298.15})
>>> param_dict = param.to_json() # Get dictionary
>>> isinstance(param_dict, dict)
True
>>> param.to_json("parameters.json")
{'Temperature [K]': 298.15}
"""
return convert_parameter_values_to_json(self, filename)
to_config = to_json
# Dictionary-like interface
def __getitem__(self, key: str) -> Any:
"""Get a parameter value by key."""
try:
return self._store[key]
except KeyError as err:
# Provide helpful error for deprecated physical constants
if key in self._DEPRECATED_CONSTANTS:
raise KeyError(
f"Accessing '{key}' from ParameterValues is deprecated. "
f"Use {self._DEPRECATED_CONSTANTS[key]} instead."
) from err
# Provide helpful error for renamed parameter
if (
"Exchange-current density for lithium metal electrode [A.m-2]"
in str(err)
and "Exchange-current density for plating [A.m-2]" in self._store
):
raise KeyError(
"'Exchange-current density for plating [A.m-2]' has been renamed "
"to 'Exchange-current density for lithium metal electrode [A.m-2]' "
"when referring to the reaction at the surface of a lithium metal "
"electrode. This is to avoid confusion with the exchange-current "
"density for the lithium plating reaction in a porous negative "
"electrode. To avoid this error, change your parameter file to use "
"the new name."
) from err
raise
[docs]
def get(self, key: str, default: Any = None) -> Any:
"""
Return item corresponding to key if it exists, otherwise return default.
Parameters
----------
key : str
The parameter name to retrieve.
default : Any, optional
The default value to return if key is not found. Default is None.
Returns
-------
Any
The parameter value if found, otherwise the default value.
Examples
--------
>>> param = pybamm.ParameterValues("Chen2020")
>>> param.get("Current function [A]")
5.0
>>> param.get("NonExistent Parameter", 42)
42
"""
if key in self._DEPRECATED_CONSTANTS:
warn(
f"Accessing '{key}' from ParameterValues is deprecated. "
f"Use {self._DEPRECATED_CONSTANTS[key]} instead.",
DeprecationWarning,
stacklevel=2,
)
return self._store.get(key, default)
def __setitem__(self, key: str, value: Any) -> None:
"""Set a parameter value (allows new parameters)."""
# Process special string values like "[input]"
if isinstance(value, str):
if value == "[input]":
value = pybamm.InputParameter(key)
elif (
value.startswith("[function]")
or value.startswith("[current data]")
or value.startswith("[data]")
or value.startswith("[2D data]")
):
raise ValueError(
"Specifying parameters via [function], [current data], [data] "
"or [2D data] is no longer supported. For functions, pass in a "
"python function object. For data, pass in a python function "
"that returns a pybamm Interpolant object. "
"See the Ai2020 parameter set for an example with both."
)
else:
# Try to convert to float
try:
value = float(value)
except ValueError:
pass # Keep as string if not convertible
self._store[key] = value
self._processor.clear_cache()
def __delitem__(self, key: str) -> None:
"""Delete a parameter."""
del self._store[key]
self._processor.clear_cache()
def __contains__(self, key: str) -> bool:
"""Check if a parameter exists."""
return key in self._store
def __iter__(self) -> Iterator[str]:
"""Iterate over parameter keys."""
return iter(self._store)
def __len__(self) -> int:
"""Return the number of parameters."""
return len(self._store)
def __repr__(self) -> str:
"""Return a string representation."""
return pformat(dict(self._store._data), width=1)
def __eq__(self, other: object) -> bool:
"""Check equality with another ParameterValues."""
if not isinstance(other, ParameterValues):
return NotImplemented
return dict(self._store._data) == dict(other._store._data)
[docs]
def keys(self):
"""
Return parameter keys.
Returns
-------
dict_keys
The parameter keys.
Examples
--------
>>> param = pybamm.ParameterValues("Chen2020")
>>> keys = list(param.keys())
>>> "Current function [A]" in keys
True
"""
return self._store.keys()
[docs]
def values(self):
"""
Return parameter values.
Returns
-------
dict_values
The parameter values.
Examples
--------
>>> param = pybamm.ParameterValues({"Temperature [K]": 298.15})
>>> vals = list(param.values())
>>> 298.15 in vals
True
"""
return self._store.values()
[docs]
def items(self):
"""
Return parameter items.
Returns
-------
dict_items
The parameter items as (key, value) pairs.
Examples
--------
>>> param = pybamm.ParameterValues({"Temperature [K]": 298.15})
>>> items = list(param.items())
>>> ("Temperature [K]", 298.15) in items
True
"""
return self._store.items()
[docs]
def pop(self, *args, **kwargs) -> Any:
"""
Remove and return a parameter value.
Example
-------
>>> params = pybamm.ParameterValues("Chen2020")
>>> val = params.pop("Current function [A]")
"""
result = self._store.pop(*args, **kwargs)
self._processor.clear_cache()
return result
[docs]
def copy(self) -> ParameterValues:
"""
Return a copy of the parameter values.
Example
-------
>>> params = pybamm.ParameterValues("Chen2020")
>>> params_copy = params.copy()
>>> params_copy["Current function [A]"] = 10.0 # Original unchanged
"""
new_copy = ParameterValues(dict(self._store._data))
new_copy._set_initial_state = self._set_initial_state
return new_copy
[docs]
def search(self, key: str, print_values: bool = True) -> None:
"""
Search dictionary for keys containing 'key'.
Example
-------
>>> params = pybamm.ParameterValues("Chen2020")
>>> params.search("electrolyte", print_values=False)
Results for 'electrolyte': ...
"""
return self._store.search(key, print_values)
# Update methods
[docs]
def update(
self,
values: Mapping[str, Any],
*,
check_conflict: bool = False,
check_already_exists: bool = False,
path: str = "",
) -> None:
"""
Update parameter values.
Parameters
----------
values : dict
Dictionary of parameter values to update.
check_conflict : bool, optional
Deprecated.
check_already_exists : bool, optional
Deprecated.
path : str, optional
Path from which to load functions (legacy parameter).
Example
-------
>>> params = pybamm.ParameterValues("Chen2020")
>>> params.update({"Current function [A]": 2.0}) # Update existing
>>> params.update({'a': 1.0}) # Create new
Notes
-----
The `check_already_exists` parameter is deprecated.
"""
# Handle deprecated arguments
if check_already_exists is not False:
warn(
"check_already_exists is deprecated. "
"This check is no longer supported in pybamm.",
DeprecationWarning,
stacklevel=2,
)
if check_conflict is not False:
warn(
"check_conflict is deprecated. "
"This check is no longer supported in pybamm.",
DeprecationWarning,
stacklevel=2,
)
# Convert ParameterValues to dict
if isinstance(values, ParameterValues):
values = dict(values._store._data)
# Check and transform parameter values
values = self.check_parameter_values(dict(values))
for name, value in values.items():
# Process value
if isinstance(value, str):
if (
value.startswith("[function]")
or value.startswith("[current data]")
or value.startswith("[data]")
or value.startswith("[2D data]")
):
raise ValueError(
"Specifying parameters via [function], [current data], [data] "
"or [2D data] is no longer supported. For functions, pass in a "
"python function object. For data, pass in a python function "
"that returns a pybamm Interpolant object. "
"See the Ai2020 parameter set for an example with both."
)
elif value == "[input]":
self._store[name] = pybamm.InputParameter(name)
else:
# Convert to float
self._store[name] = float(value)
elif isinstance(value, tuple) and isinstance(value[1], np.ndarray):
# If data is provided as a 2-column array (1D data),
# convert to two arrays for compatibility with 2D data
func_name, data = value
data = ([data[:, 0]], data[:, 1])
self._store[name] = (func_name, data)
else:
self._store[name] = value
# Clear processor cache
self._processor.clear_cache()
[docs]
@staticmethod
def check_parameter_values(values: dict) -> dict:
"""
Check and transform parameter values.
Parameters
----------
values : dict
Dictionary of parameter values to check.
Returns
-------
dict
Checked and transformed parameter values.
Raises
------
ValueError
If parameter names contain deprecated or invalid formats.
Examples
--------
>>> values = {"Electrode height [m]": 0.065}
>>> checked = pybamm.ParameterValues.check_parameter_values(values)
"""
values = scalarize_dict(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."
)
# 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'"
)
if "electrode diffusivity" in param:
new_param = param.replace("electrode", "particle")
warn(
f"The parameter '{param}' has been renamed to '{new_param}'",
DeprecationWarning,
stacklevel=2,
)
values[new_param] = values.get(param)
if is_deprecated_msmr_name(param):
new_param = replace_deprecated_msmr_name(param)
warn(
f"The parameter '{param}' has been renamed to '{new_param}'",
DeprecationWarning,
stacklevel=2,
)
values[new_param] = values.get(param)
return values
# Initial state methods
[docs]
def set_initial_state(
self,
initial_value,
direction=None,
param=None,
inplace: bool = True,
options=None,
inputs=None,
):
"""
Set the initial state of the battery.
Delegates to chemistry-specific implementation.
Parameters
----------
initial_value : float
The initial state value (e.g., SOC or voltage).
direction : str, optional
Direction for setting state. Default is None.
param : pybamm.ParameterValues, optional
Parameter values to use. Default is None (uses self).
inplace : bool, optional
If True, modify parameters in place. Default is True.
options : dict, optional
Model options. Default is None.
inputs : dict, optional
Input parameters. Default is None.
Returns
-------
ParameterValues or None
Updated parameter values if inplace=False, otherwise None.
Examples
--------
>>> param = pybamm.ParameterValues("Chen2020")
>>> result = param.set_initial_state(0.5) # Sets initial SOC to 50%
>>> isinstance(result, pybamm.ParameterValues)
True
"""
return self._set_initial_state(
initial_value,
self,
direction=direction,
param=param,
inplace=inplace,
options=options,
inputs=inputs,
)
[docs]
def set_initial_stoichiometry_half_cell(
self,
initial_value,
direction=None,
param=None,
known_value="cyclable lithium capacity",
inplace=True,
options=None,
inputs=None,
):
"""Deprecated: Use set_initial_state instead."""
msg = "pybamm.parameter_values.set_initial_stoichiometry_half_cell is deprecated, please use set_initial_state."
warn(msg, DeprecationWarning, stacklevel=2)
return self._set_initial_state(
initial_value,
self,
direction=direction,
param=param,
known_value=known_value,
inplace=inplace,
options=options,
inputs=inputs,
)
[docs]
def set_initial_stoichiometries(
self,
initial_value,
direction=None,
param=None,
known_value="cyclable lithium capacity",
inplace=True,
options=None,
inputs=None,
tol=1e-6,
):
"""Deprecated: Use set_initial_state instead."""
msg = "pybamm.parameter_values.set_initial_stoichiometries is deprecated, please use set_initial_state."
warn(msg, DeprecationWarning, stacklevel=2)
return self._set_initial_state(
initial_value,
self,
direction=direction,
param=param,
known_value=known_value,
inplace=inplace,
options=options,
inputs=inputs,
tol=tol,
)
[docs]
def set_initial_ocps(
self,
initial_value,
direction=None,
param=None,
known_value="cyclable lithium capacity",
inplace=True,
options=None,
inputs=None,
):
"""Deprecated: Use set_initial_state instead."""
msg = "pybamm.parameter_values.set_initial_ocps is deprecated, please use set_initial_state."
warn(msg, DeprecationWarning, stacklevel=2)
return self._set_initial_state(
initial_value,
self,
direction=direction,
param=param,
known_value=known_value,
inplace=inplace,
options=options,
inputs=inputs,
)
# Processing methods
# These are delegated to the ParameterSubstitutor
[docs]
def process_model(
self,
unprocessed_model: pybamm.BaseModel,
inplace: bool = True,
delayed_variable_processing: bool | None = None,
) -> pybamm.BaseModel:
"""
Assign parameter values to a model.
Parameters
----------
unprocessed_model : pybamm.BaseModel
Model to assign parameter values for.
inplace : bool, optional
If True (default), replace parameters in place.
If False, return a new model with parameter values set.
delayed_variable_processing : bool, optional
If True, make variable processing a post-processing step.
Default is False.
Returns
-------
pybamm.BaseModel
The parameterized model.
Raises
------
pybamm.ModelError
If an empty model is passed.
Example
-------
>>> model = pybamm.lithium_ion.SPM()
>>> params = pybamm.ParameterValues("Chen2020")
>>> processed_model = params.process_model(model)
"""
model = self._processor.process_model(
unprocessed_model,
inplace=inplace,
delayed_variable_processing=delayed_variable_processing,
)
# Attach parameter_values to the model's symbol_processor
pybamm.logger.debug(
"Attaching the `parameter_values` to the `symbol_processor`"
)
model.symbol_processor.parameter_values = self
return model
[docs]
def process_geometry(self, geometry: dict) -> None:
"""
Assign parameter values to a geometry (inplace).
Parameters
----------
geometry : dict
Geometry specs to assign parameter values to.
Examples
--------
>>> geometry = pybamm.battery_geometry()
>>> param = pybamm.ParameterValues("Chen2020")
>>> param.process_geometry(geometry)
"""
self._processor.process_geometry(geometry)
[docs]
def process_symbol(self, symbol: pybamm.Symbol) -> pybamm.Symbol:
"""
Walk through the symbol and replace any Parameter with a Value.
If a symbol has already been processed, the cached value is returned.
Parameters
----------
symbol : pybamm.Symbol
Symbol or Expression tree to set parameters for.
Returns
-------
pybamm.Symbol
Symbol with Parameter instances replaced by values.
Example
-------
>>> params = pybamm.ParameterValues("Chen2020")
>>> param = pybamm.Parameter("Current function [A]")
>>> processed = params.process_symbol(param)
>>> result = processed.evaluate() # Returns evaluated value
"""
return self._processor.process_symbol(symbol)
[docs]
def process_boundary_conditions(self, model: pybamm.BaseModel) -> dict:
"""
Process boundary conditions for a model.
Boundary conditions are dictionaries {"left": left bc, "right": right bc}
in general, but may be imposed on the tabs for some variables.
Parameters
----------
model : pybamm.BaseModel
Model whose boundary conditions to process.
Returns
-------
dict
Processed boundary conditions.
Examples
--------
>>> model = pybamm.lithium_ion.SPM()
>>> param = pybamm.ParameterValues("Chen2020")
>>> bcs = param.process_boundary_conditions(model)
"""
return self._processor.process_boundary_conditions(model)
[docs]
def evaluate(self, symbol: pybamm.Symbol, inputs: dict | None = None) -> Any:
"""
Process and evaluate a symbol.
Parameters
----------
symbol : pybamm.Symbol
Symbol or Expression tree to evaluate.
inputs : dict, optional
Input parameter values for evaluation.
Returns
-------
number or array
The evaluated symbol.
Example
-------
>>> params = pybamm.ParameterValues("Chen2020")
>>> param = pybamm.Parameter("Current function [A]")
>>> result = params.evaluate(param) # Returns evaluated value
"""
return self._processor.evaluate(symbol, inputs)
# Metatdata methods
[docs]
def get_info(self, key: str) -> ParameterInfo:
"""
Get metadata about a parameter.
Parameters
----------
key : str
The parameter name.
Returns
-------
ParameterInfo
Metadata including value, units, category, and type information.
Examples
--------
>>> param = pybamm.ParameterValues("Chen2020")
>>> info = param.get_info("Maximum concentration in negative electrode [mol.m-3]")
>>> info.units
'mol.m-3'
>>> info.category
'negative electrode'
"""
return self._store.get_info(key)
[docs]
def list_by_category(self, category: ParameterCategory | str) -> list[str]:
"""
Return all parameter names in a given category.
Parameters
----------
category : ParameterCategory or str
The category to filter by. Can be a ParameterCategory enum value
or a string like "negative electrode".
Returns
-------
list[str]
List of parameter names in the category.
Examples
--------
>>> param = pybamm.ParameterValues("Chen2020")
>>> electrode_params = param.list_by_category("negative electrode")
>>> len(electrode_params) > 0
True
"""
return self._store.list_by_category(category)
[docs]
def categories(self) -> dict[str, list[str]]:
"""
Return all parameters grouped by category.
Returns
-------
dict[str, list[str]]
Dictionary mapping category names to lists of parameter names.
Examples
--------
>>> param = pybamm.ParameterValues("Chen2020")
>>> cats = param.categories()
>>> "negative electrode" in cats
True
"""
return self._store.categories()
[docs]
def diff(self, other: ParameterValues, *, rtol: float = 0.0) -> ParameterDiff:
"""
Compare this ParameterValues with another and return differences.
Parameters
----------
other : ParameterValues
The other parameter values to compare against.
rtol : float, optional
Relative tolerance for numerical comparisons. Differences smaller
than ``rtol * max(|a|, |b|)`` are ignored. Default is 0.0 (exact
comparison). Set to e.g. 1e-6 to ignore tiny floating-point
differences.
Returns
-------
ParameterDiff
Object containing added, removed, and changed parameters.
Examples
--------
>>> chen = pybamm.ParameterValues("Chen2020")
>>> marquis = pybamm.ParameterValues("Marquis2019")
>>> diff = chen.diff(marquis)
>>> isinstance(diff.changed, dict)
True
With tolerance:
>>> params1 = pybamm.ParameterValues({"x": 1.0})
>>> params2 = pybamm.ParameterValues({"x": 1.0 + 1e-10})
>>> params1.diff(params2, rtol=1e-9).changed # Empty
{}
"""
return self._store.diff(other._store, rtol=rtol)
# Utility methods
def _ipython_key_completions_(self) -> list[str]:
"""Provide key completions for IPython."""
return list(self._store.keys())
[docs]
def print_parameters(self, parameters, output_file: str | None = None) -> dict:
"""
Return dictionary of evaluated parameters.
Optionally print these evaluated parameters to an output file.
Parameters
----------
parameters : class or dict containing pybamm.Parameter objects
Class or dictionary containing all the parameters to be evaluated.
output_file : str, optional
The file to print parameters to. If None, the parameters are not
printed, and this function simply acts as a test that all the
parameters can be evaluated.
Returns
-------
dict
The evaluated parameters.
Examples
--------
>>> param = pybamm.ParameterValues("Chen2020")
>>> params_dict = {"param1": pybamm.Parameter("Current function [A]")}
>>> evaluated = param.print_parameters(params_dict)
>>> isinstance(evaluated, dict)
True
>>> param.print_parameters(params_dict, "output.txt")
defaultdict(<class 'list'>, {'param1': np.float64(5.0)})
"""
# Set list of attributes to ignore
ignore = [
"__name__",
"__doc__",
"__package__",
"__loader__",
"__spec__",
"__file__",
"__cached__",
"__builtins__",
"absolute_import",
"division",
"print_function",
"unicode_literals",
"pybamm",
"_options",
"constants",
"np",
"geo",
"elec",
"therm",
"half_cell",
"x",
"r",
]
# If 'parameters' is a class, extract the dict
if not isinstance(parameters, dict):
parameters_dict = {
k: v for k, v in parameters.__dict__.items() if k not in ignore
}
for domain in ["n", "s", "p"]:
domain_param = getattr(parameters, domain)
parameters_dict.update(
{
f"{domain}.{k}": v
for k, v in domain_param.__dict__.items()
if k not in ignore
}
)
parameters = parameters_dict
evaluated_parameters = defaultdict(list)
for name, symbol in parameters.items():
if isinstance(symbol, pybamm.Symbol):
try:
proc_symbol = self.process_symbol(symbol)
except KeyError:
# skip parameters that don't have a value in that parameter set
proc_symbol = None
if not (
callable(proc_symbol)
or proc_symbol is None
or proc_symbol.has_symbol_of_classes(
(pybamm.Concatenation, pybamm.Broadcast)
)
):
evaluated_parameters[name] = proc_symbol.evaluate(t=0)
# Print the evaluated_parameters dict to output_file
if output_file:
self.print_evaluated_parameters(evaluated_parameters, output_file)
return evaluated_parameters
[docs]
def print_evaluated_parameters(
self, evaluated_parameters: dict, output_file: str
) -> None:
"""
Print a dictionary of evaluated parameters to an output file.
Parameters
----------
evaluated_parameters : dict
The evaluated parameters.
output_file : str
The file to print parameters to.
Examples
--------
>>> param = pybamm.ParameterValues("Chen2020")
>>> evaluated_params = {"Temperature [K]": 298.15, "Voltage [V]": 3.7}
>>> param.print_evaluated_parameters(evaluated_params, "params.txt")
"""
# Get column width for pretty printing
column_width = max(len(name) for name in evaluated_parameters.keys())
s = f"{{:>{column_width}}}"
with open(output_file, "w") as file:
for name, value in sorted(evaluated_parameters.items()):
if 0.001 < abs(value) < 1000:
file.write((s + " : {:10.4g}\n").format(name, value))
else:
file.write((s + " : {:10.3E}\n").format(name, value))
class ParameterNameParser:
"""
Utility class for parsing and manipulating parameter names.
Parameter names follow a grammar:
- Base: "Parameter name"
- With units: "Parameter name [unit]"
- With index: "Parameter name (idx) [unit]"
Examples
--------
>>> parser = ParameterNameParser()
>>> parser.parse_units("Temperature [K]")
'K'
>>> parser.split("Temperature [K]")
('Temperature', 'K')
>>> parser.combine("Temperature", 0, "K")
'Temperature (0) [K]'
"""
# Regex for indexed parameter names: "name (idx) [unit]"
_INDEXED_RE = re.compile(
r"""^
(?P<base>[^\[\]]+?) # base name (non-greedy)
\s\((?P<idx>\d+)\) # (index)
(?:\s\[(?P<tag>[^\]]+)\])? # optional [unit]
$""",
re.VERBOSE,
)
# Regex for simple parameter names: "name [unit]"
_SIMPLE_RE = re.compile(
r"""^
(?P<base>[^\[\]]+?) # base name (non-greedy)
(?:\s\[(?P<tag>[^\]]+)\])? # optional [unit]
$""",
re.VERBOSE,
)
@classmethod
def parse_units(cls, name: str) -> str | None:
"""
Extract units from a parameter name.
Example
-------
>>> ParameterNameParser.parse_units("Temperature [K]")
'K'
"""
match = cls._SIMPLE_RE.match(name)
return match["tag"] if match else None
@classmethod
def split(cls, name: str) -> tuple[str, str | None]:
"""
Split a parameter name into (base, units).
Parameters
----------
name : str
Parameter name like "Temperature [K]".
Returns
-------
tuple[str, str | None]
(base_name, units) where units may be None.
Raises
------
ValueError
If the name doesn't match the expected grammar.
Example
-------
>>> ParameterNameParser.split("Temperature [K]")
('Temperature', 'K')
"""
match = cls._SIMPLE_RE.match(name)
if not match:
raise ValueError(f"Illegal parameter name {name!r}")
return match["base"].rstrip(), match["tag"]
@classmethod
def combine(cls, base: str, idx: int, units: str | None = None) -> str:
"""
Combine base name, index, and optional units into a parameter name.
Parameters
----------
base : str
Base parameter name.
idx : int
Index (must be >= 0).
units : str, optional
Units to append.
Returns
-------
str
Combined name like "base (idx) [units]".
Example
-------
>>> ParameterNameParser.combine("a", 0, "V")
'a (0) [V]'
"""
if idx < 0:
raise ValueError("idx must be ≥ 0")
result = f"{base} ({idx})"
if units:
result += f" [{units}]"
return result
@classmethod
def add_units(cls, base: str, units: str | None) -> str:
"""
Add units to a base name.
Example
-------
>>> ParameterNameParser.add_units("Temperature", "K")
'Temperature [K]'
"""
if units:
return f"{base} [{units}]"
return base
@classmethod
def parse_indexed(cls, name: str) -> tuple[str, int, str | None] | None:
"""
Parse an indexed parameter name.
Parameters
----------
name : str
Parameter name like "param (0) [unit]".
Returns
-------
tuple[str, int, str | None] | None
(base, index, units) or None if not an indexed name.
Example
-------
>>> ParameterNameParser.parse_indexed("a (1) [V]")
('a', 1, 'V')
>>> ParameterNameParser.parse_indexed("not indexed") # Returns None
"""
match = cls._INDEXED_RE.match(name)
if not match:
return None
return match["base"], int(match["idx"]), match["tag"]
# Dictionary Transformation Utilities
def scalarize_dict(
params: dict[str, Any], ignored_keys: list[str] | None = None
) -> dict[str, Any]:
"""
Expand list-valued items into scalar keys while preserving tags.
This is useful for serialization where lists need to be flattened.
Parameters
----------
params : dict[str, Any]
The dictionary to scalarize.
ignored_keys : list[str], optional
Keys to skip (not expand). Defaults to ["citations"].
Returns
-------
dict[str, Any]
The scalarized dictionary.
Examples
--------
>>> scalarize_dict({'a [V]': [1, 2]})
{'a (0) [V]': 1, 'a (1) [V]': 2}
"""
out = {}
ignored_keys = ignored_keys or ["citations"]
for key, val in params.items():
if key not in ignored_keys and isinstance(val, list):
base, units = ParameterNameParser.split(key)
for i, item in enumerate(val):
indexed_key = ParameterNameParser.combine(base, i, units)
if indexed_key in out:
raise ValueError(f"Duplicate key {indexed_key!r}")
out[indexed_key] = item
else:
if key in out:
raise ValueError(f"Duplicate key {key!r}")
out[key] = val
return out
def arrayize_dict(scalar_dict: dict[str, Any]) -> dict[str, Any]:
"""
Collapse indexed scalar keys back into lists.
This is the inverse of scalarize_dict. A sequence is collapsed
only if indices 0…N are all present.
Parameters
----------
scalar_dict : dict[str, Any]
The dictionary with indexed keys.
Returns
-------
dict[str, Any]
The arrayized dictionary.
Examples
--------
>>> arrayize_dict({'a (0) [V]': 1, 'a (1) [V]': 2})
{'a [V]': [1, 2]}
"""
out = {}
processed = set()
# Discover (base, tag) pairs that appear indexed
pairs = set()
for key in scalar_dict:
parsed = ParameterNameParser.parse_indexed(key)
if parsed:
base, _, units = parsed
pairs.add((base, units))
# Rebuild each pair
for base, units in pairs:
idx_val = {}
own_keys = []
for key, val in scalar_dict.items():
parsed = ParameterNameParser.parse_indexed(key)
if parsed and parsed[0] == base and parsed[2] == units:
idx = parsed[1]
if idx in idx_val:
raise ValueError(f"Duplicate index {idx} for '{base}'")
idx_val[idx] = val
own_keys.append(key)
if not idx_val:
raise ValueError(
f"No indices found for '{base}'. "
"This should not happen. Please report this bug."
)
indices = set(idx_val)
if not _is_contiguous(indices):
missing = sorted(set(range(max(indices) + 1)) - indices)
raise ValueError(f"Missing indices {missing} for '{base}'")
collapsed_key = ParameterNameParser.add_units(base, units)
if collapsed_key in out:
raise ValueError(f"Duplicate key after rebuild: {collapsed_key!r}")
out[collapsed_key] = [idx_val[i] for i in range(max(idx_val) + 1)]
processed.update(own_keys)
# Copy non-indexed entries
for key, val in scalar_dict.items():
if key not in processed:
if key in out:
raise ValueError(f"Duplicate key: {key!r}")
out[key] = val
return out
def _is_contiguous(indices: set[int]) -> bool:
"""Check if indices form a contiguous sequence starting from 0."""
return bool(indices) and indices == set(range(max(indices) + 1))
def _is_iterable(val: Any) -> bool:
"""Check if a value is iterable (but not string, dict, or bytes)."""
return hasattr(val, "__iter__") and not isinstance(val, (str, dict, bytes))
# JSON Conversion Utilities
def convert_symbols_in_dict(data_dict: dict | None = None) -> dict:
"""
Recursively convert nested dicts using convert_symbol_from_json.
Parameters
----------
data_dict : dict, optional
Dictionary to process. Returns empty dict if None.
Returns
-------
dict
Dictionary with symbols converted.
Examples
--------
>>> # Simple conversion
>>> data = {"Temperature [K]": "298.15", "Voltage [V]": 3.7}
>>> converted = convert_symbols_in_dict(data)
>>> converted["Temperature [K]"]
298.15
"""
if not data_dict:
return {}
for key, value in data_dict.items():
if isinstance(value, dict) and "interpolator" in value:
# Handle interpolant specification
interpolator = value.get("interpolator", "linear")
x = value.get("x", [])
y = value.get("y", [])
def interpolant_function(sto, x=x, y=y, interpolator=interpolator):
try:
return pybamm.Interpolant(x, y, sto, interpolator=interpolator)
except Exception as e:
print(e)
return pybamm.Scalar(0)
data_dict[key] = interpolant_function
elif isinstance(value, dict):
data_dict[key] = convert_symbol_from_json(value)
elif isinstance(value, list):
data_dict[key] = [
convert_symbol_from_json(item) if isinstance(item, dict) else item
for item in value
]
elif isinstance(value, str):
data_dict[key] = float(value)
return data_dict
def convert_parameter_values_to_json(
parameter_values: ParameterValues, filename: str | None = None
) -> dict:
"""
Convert a ParameterValues object to a JSON-serializable dictionary.
Optionally saves it to a file.
Parameters
----------
parameter_values : ParameterValues
The ParameterValues object to convert.
filename : str, optional
The filename to save the JSON file to.
Returns
-------
dict
The JSON-serializable dictionary.
Examples
--------
>>> param = pybamm.ParameterValues({"Temperature [K]": 298.15})
>>> json_dict = convert_parameter_values_to_json(param)
>>> isinstance(json_dict, dict)
True
>>> convert_parameter_values_to_json(param, "params.json")
{'Temperature [K]': 298.15}
"""
parameter_values_dict = {}
for k, v in parameter_values.items():
if callable(v):
parameter_values_dict[k] = convert_symbol_to_json(
convert_function_to_symbolic_expression(v, k)
)
else:
parameter_values_dict[k] = convert_symbol_to_json(v)
if filename is not None:
with open(filename, "w") as f:
json.dump(
parameter_values_dict, f, indent=2, default=Serialise._json_encoder
)
return parameter_values_dict