Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## Bug fixes

- Fixed `ParameterValues.to_json` (and `Serialise` of parameter values) raising `TypeError: ... got multiple values for argument` on `create_from_bpx` outputs, whose functional parameters are `functools.partial` objects with keyword-bound arguments (e.g. constant diffusivity, tabular OCPs, exchange-current density). The serialiser now only creates symbols for a partial's remaining required arguments, leaving bound and defaulted arguments in place. ([#5641](https://github.com/pybamm-team/PyBaMM/pull/5641))
- Fixed the `pybammsolvers` source distribution bundling the SUNDIALS/SuiteSparse submodule trees (~291 MB, over PyPI's per-file limit) when built from a checkout with the submodules initialised; the sdist now excludes them. ([#5623](https://github.com/pybamm-team/PyBaMM/pull/5623))
- Fixed the `pybammsolvers` editable auto-rebuild failing to configure when the SUNDIALS/SuiteSparse submodules are absent even though the libraries were already built in `.idaklu`; the from-source bootstrap (and its submodule requirement) is now skipped once the libraries exist. ([#5623](https://github.com/pybamm-team/PyBaMM/pull/5623))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2201,24 +2201,23 @@ def convert_function_to_symbolic_expression(func, name=None):
"""
# Create symbolic parameters for each input argument
try:
func_name = func.get_name()
func_args = func.get_args()
# Use the underlying function for evaluation
func_to_eval = func.func
func_name = func.__name__
func_args = list(inspect.signature(func).parameters)
except AttributeError:
try:
func_name = func.__name__
func_args = list(inspect.signature(func).parameters)
func_to_eval = func
except AttributeError:
# One more fallback, in case it's a partial
func_name = func.func.__name__
func_args = list(inspect.signature(func).parameters)
func_to_eval = func
# A functools.partial's signature still lists its bound/defaulted params
# (e.g. create_from_bpx's D_ref/Ea/constant); keep only the unfilled ones.
func_name = func.func.__name__
func_args = [
name
for name, parameter in inspect.signature(func).parameters.items()
if parameter.default is inspect.Parameter.empty
and parameter.kind not in (parameter.VAR_POSITIONAL, parameter.VAR_KEYWORD)
]

sym_inputs = [pybamm.Parameter(arg) for arg in func_args]
# Pass the symbols by keyword so a partial's bound (keyword-only) arguments
# keep their values rather than being displaced by a positional input.
with tracing():
sym_output = func_to_eval(*sym_inputs)
sym_output = func(**{arg: pybamm.Parameter(arg) for arg in func_args})

# Wrap the symbolic expression in an ExpressionFunctionParameter to allow access
# to the function name and arguments
Expand Down
21 changes: 21 additions & 0 deletions packages/pybamm/tests/unit/test_parameters/test_bpx.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,27 @@ def test_table_data(self, tmp_path):
dUdT = param[f"{electrode} electrode OCP entropic change [V.K-1]"](c)
assert isinstance(dUdT, pybamm.Interpolant)

@pytest.mark.parametrize("form", ["function", "constant", "table"])
def test_bpx_to_json_roundtrip(self, tmp_path, form):
"""create_from_bpx stores functional parameters as keyword-bound partials;
serialising them must not raise (regression for the partial arg handling
in convert_function_to_symbolic_expression)."""
bpx_obj = copy.deepcopy(self.base)
if form != "function":
value = 1 if form == "constant" else {"x": [0, 1], "y": [0, 1]}
for section in ["Electrolyte", "Negative electrode", "Positive electrode"]:
bpx_obj["Parameterisation"][section]["Diffusivity [m2.s-1]"] = value
bpx_obj["Parameterisation"]["Electrolyte"]["Conductivity [S.m-1]"] = value

temp_file = tmp_path / "tmp.json"
temp_file.write_text(json.dumps(bpx_obj))
param = pybamm.ParameterValues.create_from_bpx(temp_file)

out_file = tmp_path / "out.json"
param.to_json(out_file)
# the round-tripped values load back without error
pybamm.ParameterValues.from_json(out_file)

def test_bpx_soc_error(self):
bpx_obj = copy.deepcopy(self.base)
with (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
SUPPORTED_SCHEMA_VERSION,
ExpressionFunctionParameter,
Serialise,
convert_function_to_symbolic_expression,
convert_symbol_from_json,
convert_symbol_to_json,
)
Expand Down Expand Up @@ -1495,6 +1496,57 @@ def test_parameter_serialisation(self, tmp_path):
sim.plot(show_plot=False)


class TestConvertFunctionToSymbolicExpression:
def test_partial_with_bound_kwargs(self):
"""A partial with keyword-bound args (as create_from_bpx produces) must
only receive symbols for its remaining required args, not the bound ones."""
import functools

def diffusivity(sto, T, D_ref, Ea, constant=False):
return D_ref * pybamm.exp(-Ea * (sto + T))

func = functools.partial(diffusivity, D_ref=1.5, Ea=2.0, constant=True)
efp = convert_function_to_symbolic_expression(func, "D")

assert efp.func_args == ["sto", "T"]
assert efp.name == "D"
assert efp.func_name == "diffusivity"

def test_partial_leaves_unbound_defaulted_arg(self):
"""A defaulted parameter left unbound (e.g. constant diffusivity's
``constant`` flag) must not be turned into a symbol, or a positional
call would collide with the following keyword-bound argument."""
import functools

def diffusivity(sto, T, D_ref, Ea, constant=False):
# constant is a Python control flag, not a symbolic input
if constant:
return D_ref * pybamm.exp(-Ea * T)
return D_ref(sto) * pybamm.exp(-Ea * T)

func = functools.partial(diffusivity, D_ref=lambda s: s, Ea=2.0)
efp = convert_function_to_symbolic_expression(func, "D")

assert efp.func_args == ["sto", "T"]

def test_partial_returning_interpolant(self):
"""OCP-style partials bind name/x/y and return an Interpolant."""
import functools

def interpolant_func(var, name, x, y):
return pybamm.Interpolant(x, y, var, name=name, interpolator="linear")

x_data = np.array([0.0, 0.5, 1.0])
y_data = np.array([1.0, 2.0, 3.0])
func = functools.partial(interpolant_func, name="U", x=x_data, y=y_data)
efp = convert_function_to_symbolic_expression(func, "U")

assert efp.func_args == ["var"]
src = efp.to_source()
assert "def interpolant_func(var):" in src
assert 'name="U"' in src


class TestExpressionFunctionParameter:
def test_basic_serialisation(self):
x = pybamm.SpatialVariable("x", domain="negative electrode")
Expand Down
Loading