import copy
import json
import types
from numbers import Number
from typing import Optional, Union
import numpy as np
from pybamm import (
FunctionParameter,
LithiumIonParameters,
Parameter,
ParameterValues,
Scalar,
Symbol,
parameter_sets,
)
[docs]
class ParameterSet:
"""
Handles the import and export of parameter sets for battery models.
This class provides methods to load parameters from a JSON file and to export them
back to a JSON file. It also includes custom logic to handle special cases, such
as parameter values that require specific initialization.
Parameters
----------
parameter_set : Union[str, dict, ParameterValues], optional
A dictionary of parameters to initialise the ParameterSet with. If not provided, an empty dictionary is used.
json_path : str, optional
Path to a JSON file containing parameter data. If provided, parameters will be imported from this file during
initialisation.
formation_concentrations : bool, optional
If True, re-calculates the initial concentrations of lithium in the active material (default: False).
"""
def __init__(
self,
parameter_set: Union[str, dict, ParameterValues] = None,
json_path: Optional[str] = None,
formation_concentrations: Optional[bool] = False,
):
if parameter_set is not None and json_path is not None:
raise ValueError(
"ParameterSet needs either a parameter_set or json_path as an input, not both."
)
[docs]
self.parameter_values = None
if json_path is not None:
self.import_parameters(json_path)
else:
self.parameter_values = self.to_pybamm(parameter_set)
if self.parameter_values is not None:
self.chemistry = self.parameter_values.get("chemistry", None)
if self.formation_concentrations:
set_formation_concentrations(self.parameter_values)
[docs]
def __setitem__(self, key, value):
self.parameter_values[key] = value
[docs]
def __getitem__(self, key):
return self.parameter_values[key]
@staticmethod
[docs]
def evaluate_symbol(symbol: Union[Symbol, Number], params: dict):
"""
Evaluate a parameter in the parameter set.
Parameters
----------
symbol : pybamm.Symbol or Number
The parameter to evaluate.
Returns
-------
float
The value of the parameter.
"""
if isinstance(symbol, (Number, np.float64)):
return symbol
if isinstance(symbol, Scalar):
return symbol.value
if isinstance(symbol, (Parameter, FunctionParameter)):
return ParameterSet.evaluate_symbol(params[symbol.name], params)
new_children = [
Scalar(ParameterSet.evaluate_symbol(child, params))
for child in symbol.children
]
return symbol.create_copy(new_children).evaluate()
[docs]
def keys(self) -> list:
"""
A list of parameter names
"""
return list(self.parameter_values.keys())
[docs]
def update(self, params_dict: dict = None, check_already_exists: bool = True):
"""
Update the parameter dictionary.
Parameters
----------
params_dict : dict
A dictionary of parameters and values used to update the parameter values
check_already_exists : bool, optional
Whether to check that a parameter in `params_dict` already exists when trying
to update it. This is to avoid cases where an intended change in the parameters
is ignored due a typo in the parameter name (default: True).
"""
self.parameter_values.update(
params_dict, check_already_exists=check_already_exists
)
[docs]
def import_parameters(self, json_path: Optional[str] = None):
"""
Imports parameters from a JSON file specified by the `json_path` attribute.
If a `json_path` is provided at initialization or as an argument, that JSON file
is loaded and the parameters are stored in the `params` attribute. Special cases
are handled appropriately.
Parameters
----------
json_path : str, optional
Path to the JSON file from which to import parameters. If provided, it overrides the instance's `json_path`.
Returns
-------
dict
The dictionary containing the imported parameters.
Raises
------
FileNotFoundError
If the specified JSON file cannot be found.
"""
if self.parameter_values:
raise ValueError("Parameter set already constructed.")
# Read JSON file
if json_path:
self._json_path = json_path
try:
self.parameter_values = ParameterValues.create_from_bpx(self._json_path)
except Exception:
print(
"The JSON file was not recognised as a BPX parameter set. Importing as a JSON file."
)
with open(self._json_path) as file:
params = json.load(file)
self.parameter_values = ParameterValues(params)
else:
raise ValueError("No path was provided.")
self.chemistry = self.parameter_values.get("chemistry", None)
if self.formation_concentrations:
set_formation_concentrations(self.parameter_values)
return self.parameter_values
[docs]
def export_parameters(self, output_json_path, fit_params=None):
"""
Exports parameters to a JSON file specified by `output_json_path`.
The current state of the `params` attribute is written to the file. If `fit_params`
is provided, these parameters are updated before export. Non-serializable values
are handled and noted in the output JSON.
Parameters
----------
output_json_path : str
The file path where the JSON output will be saved.
fit_params : list of fitted parameter objects, optional
Parameters that have been fitted and need to be included in the export.
Raises
------
ValueError
If there are no parameters to export.
"""
if not self.parameter_values:
raise ValueError("No parameters to export. Please import parameters first.")
# Prepare a copy of the params to avoid modifying the original dict
exportable_params = {
**{"chemistry": self.chemistry},
**self.parameter_values.copy(),
}
# Update parameter set
if fit_params is not None:
for _i, param in enumerate(fit_params):
exportable_params.update({param.name: param.value})
# Replace non-serializable values
for key, value in exportable_params.items():
if isinstance(value, types.FunctionType) or not self.is_json_serializable(
value
):
exportable_params[key] = "Unable to write value to JSON file"
# Write parameters to JSON file
with open(output_json_path, "w") as file:
json.dump(exportable_params, file, indent=4)
[docs]
def is_json_serializable(self, value):
"""
Determines if the given `value` can be serialized to JSON format.
Parameters
----------
value : any
The value to check for JSON serializability.
Returns
-------
bool
True if the value is JSON serializable, False otherwise.
"""
try:
json.dumps(value)
return True
except (TypeError, OverflowError):
return False
[docs]
def copy(self):
new_copy = ParameterSet(
parameter_set=self.parameter_values.copy(),
json_path=copy.copy(self._json_path),
formation_concentrations=copy.copy(self.formation_concentrations),
)
return new_copy
@classmethod
[docs]
def pybamm(cls, name):
"""
Retrieves a PyBaMM parameter set by name.
Parameters
----------
name : str
The name of the PyBaMM parameter set to retrieve.
Returns
-------
pybamm.ParameterValues
A PyBaMM parameter set corresponding to the provided name.
"""
msg = f"Parameter set '{name}' is not a valid PyBaMM parameter set. Available parameter sets are: {list(parameter_sets)}"
if name not in list(parameter_sets):
raise ValueError(msg)
return ParameterValues(name).copy()
@classmethod
[docs]
def to_pybamm(cls, parameter_set):
"""
Converts a parameter set to a new PyBaMM ParameterValues object.
"""
if parameter_set is None:
return None
elif isinstance(parameter_set, str):
# Use class method
return cls.pybamm(parameter_set)
elif isinstance(parameter_set, dict):
return ParameterValues(parameter_set)
elif isinstance(parameter_set, ParameterValues):
return parameter_set.copy()
else:
return parameter_set.parameter_values.copy()