Repository: pybamm-team/PyBaMM

Issue:
Enable passing of inputs throughout set_initial_soc
### Description

For parameter estimation, it would be useful to be able to iterate through simulations with input parameters including the case where the initial state depends on the input parameter value.

### Motivation

Here are some example future use cases:
```
import pybamm
import numpy as np
import matplotlib.pyplot as plt

for model, options, param_set in zip(
    [pybamm.lithium_ion.SPM, pybamm.lithium_ion.MSMR],
    [{}, {"number of MSMR reactions": ("6", "4")}],
    ["Chen2020", "MSMR_Example"],
):
    # Define parameters
    parameter_values = pybamm.ParameterValues(param_set)
    uncertain_param = "Maximum concentration in positive electrode [mol.m-3]"
    value = parameter_values[uncertain_param]
    parameter_values[uncertain_param] = "[input]"

    # Set up and run simulation
    sim = pybamm.Simulation(model=model(options), parameter_values=parameter_values)
    sol = sim.solve(
        t_eval=np.linspace(0,10,11),
        initial_soc="3.5 V",
        inputs={uncertain_param: value},
    )
    sol.plot()
```

### Possible Implementation

Add the argument `inputs` to all relevant functions and calls to `evaluate` inside `set_initial_stoichiometries`, `set_initial_ocps`, etc.

### Additional context

_No response_


Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f46b5a2bc..6f6ef31994 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
 # [Unreleased](https://github.com/pybamm-team/PyBaMM/)
 
 ## Features
+- Enables the passing of `inputs` throughout `set_initial_soc`. ([#5122](https://github.com/pybamm-team/PyBaMM/pull/5122))
 - Adds `on_failure` option to `BaseSolver` with options for `"warn"`, `"ignore"`, and `"raise"` to change behaviour on solver failure. Defaults to "raise" to retain historic functionality. ([#5105](https://github.com/pybamm-team/PyBaMM/pull/5105))
 - Creates a boundary mesh size object that returns the distance from the center of the leftmost/rightmost control volume to the boundary of the domain ([#5108](https://github.com/pybamm-team/PyBaMM/pull/5108))
 - Introduced entry points for models, similar to parameter sets, and moved entry point handling to `pybamm.dispatch.entry_points`. There is now experimental support for loading third-party models outside of the PyBaMM framework via `pybamm.Model("model_name")`. This API is currently unstable until further notice and may be subject to change without warning. ([#4490](https://github.com/pybamm-team/PyBaMM/pull/4490))
diff --git a/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py b/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py
index 642866b2e5..172b41c06a 100644
--- a/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py
+++ b/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py
@@ -502,10 +502,10 @@ def _set_up_solve(self, inputs):
                 self.parameter_values, self.param
             )
             sol0 = pybamm.AlgebraicSolver().solve(
-                msmr_pot_model, inputs={"x": x0_init, "y": y0_init}
+                msmr_pot_model, inputs={**inputs, "x": x0_init, "y": y0_init}
             )
             sol100 = pybamm.AlgebraicSolver().solve(
-                msmr_pot_model, inputs={"x": x100_init, "y": y100_init}
+                msmr_pot_model, inputs={**inputs, "x": x100_init, "y": y100_init}
             )
             return {
                 "Un(x_100)": sol100["Un"].data,
@@ -603,8 +603,12 @@ def _check_esoh_feasible(self, inputs):
 
         # Parameterize the OCP functions
         if self.OCV_function is None:
-            self.V_max = self.parameter_values.evaluate(self.param.ocp_soc_100)
-            self.V_min = self.parameter_values.evaluate(self.param.ocp_soc_0)
+            self.V_max = self.parameter_values.evaluate(
+                self.param.ocp_soc_100, inputs=inputs
+            )
+            self.V_min = self.parameter_values.evaluate(
+                self.param.ocp_soc_0, inputs=inputs
+            )
             if self.options["open-circuit potential"] == "MSMR":
                 # will solve for potentials at the sto limits, so no need
                 # to store a function
@@ -623,10 +627,10 @@ def _check_esoh_feasible(self, inputs):
                 self.parameter_values, self.param
             )
             sol0 = pybamm.AlgebraicSolver(tol=1e-4).solve(
-                msmr_pot_model, inputs={"x": x0_min, "y": y0_max}
+                msmr_pot_model, inputs={**inputs, "x": x0_min, "y": y0_max}
             )
             sol100 = pybamm.AlgebraicSolver(tol=1e-4).solve(
-                msmr_pot_model, inputs={"x": x100_max, "y": y100_min}
+                msmr_pot_model, inputs={**inputs, "x": x100_max, "y": y100_min}
             )
             Up0 = sol0["Up"].data[0]
             Un0 = sol0["Un"].data[0]
@@ -693,8 +697,8 @@ def get_initial_stoichiometries(self, initial_value, tol=1e-6, inputs=None):
 
         if isinstance(initial_value, str) and initial_value.endswith("V"):
             V_init = float(initial_value[:-1])
-            V_min = parameter_values.evaluate(self.param.ocp_soc_0)
-            V_max = parameter_values.evaluate(self.param.ocp_soc_100)
+            V_min = parameter_values.evaluate(self.param.ocp_soc_0, inputs=inputs)
+            V_max = parameter_values.evaluate(self.param.ocp_soc_100, inputs=inputs)
 
             if not V_min - tol <= V_init <= V_max + tol:
                 raise ValueError(
@@ -728,7 +732,9 @@ def get_initial_stoichiometries(self, initial_value, tol=1e-6, inputs=None):
             soc_model.variables["soc"] = soc
             parameter_values.process_model(soc_model)
             initial_soc = (
-                pybamm.AlgebraicSolver(tol=tol).solve(soc_model, [0])["soc"].data[0]
+                pybamm.AlgebraicSolver(tol=tol)
+                .solve(soc_model, [0], inputs=inputs)["soc"]
+                .data[0]
             )
         elif isinstance(initial_value, int | float):
             initial_soc = initial_value
@@ -804,6 +810,7 @@ def get_initial_ocps(self, initial_value, tol=1e-6, inputs=None):
         Un, Up
             The initial open-circuit potentials at the desired initial state of charge
         """
+        inputs = inputs or {}
         parameter_values = self.parameter_values
         x, y = self.get_initial_stoichiometries(initial_value, tol, inputs=inputs)
         if self.options["open-circuit potential"] == "MSMR":
@@ -811,7 +818,7 @@ def get_initial_ocps(self, initial_value, tol=1e-6, inputs=None):
                 self.parameter_values, self.param
             )
             sol = pybamm.AlgebraicSolver().solve(
-                msmr_pot_model, inputs={"x": x, "y": y}
+                msmr_pot_model, inputs={**inputs, "x": x, "y": y}
             )
             Un = sol["Un"].data[0]
             Up = sol["Up"].data[0]
@@ -821,7 +828,7 @@ def get_initial_ocps(self, initial_value, tol=1e-6, inputs=None):
             Up = parameter_values.evaluate(self.param.p.prim.U(y, T_ref), inputs=inputs)
         return Un, Up
 
-    def get_min_max_ocps(self):
+    def get_min_max_ocps(self, inputs=None):
         """
         Calculate min/max open-circuit potentials
         given voltage limits, open-circuit potentials, etc defined by parameter_values
@@ -831,21 +838,24 @@ def get_min_max_ocps(self):
         Un_0, Un_100, Up_100, Up_0
             The min/max ocps
         """
+        inputs = inputs or {}
         parameter_values = self.parameter_values
 
-        Q_n = parameter_values.evaluate(self.param.n.Q_init)
-        Q_p = parameter_values.evaluate(self.param.p.Q_init)
+        Q_n = parameter_values.evaluate(self.param.n.Q_init, inputs=inputs)
+        Q_p = parameter_values.evaluate(self.param.p.Q_init, inputs=inputs)
 
         if self.known_value == "cyclable lithium capacity":
-            Q_Li = parameter_values.evaluate(self.param.Q_Li_particles_init)
-            inputs = {"Q_n": Q_n, "Q_p": Q_p, "Q_Li": Q_Li}
+            Q_Li = parameter_values.evaluate(
+                self.param.Q_Li_particles_init, inputs=inputs
+            )
+            all_inputs = {**inputs, "Q_n": Q_n, "Q_p": Q_p, "Q_Li": Q_Li}
         elif self.known_value == "cell capacity":
             Q = parameter_values.evaluate(
-                self.param.Q / self.param.n_electrodes_parallel
+                self.param.Q / self.param.n_electrodes_parallel, inputs=inputs
             )
-            inputs = {"Q_n": Q_n, "Q_p": Q_p, "Q": Q}
+            all_inputs = {**inputs, "Q_n": Q_n, "Q_p": Q_p, "Q": Q}
         # Solve the model and check outputs
-        sol = self.solve(inputs)
+        sol = self.solve(all_inputs)
         return [sol["Un(x_0)"], sol["Un(x_100)"], sol["Up(y_100)"], sol["Up(y_0)"]]
 
     def theoretical_energy_integral(self, inputs, points=1000):
@@ -918,7 +928,11 @@ def get_initial_stoichiometries(
 
 
 def get_min_max_stoichiometries(
-    parameter_values, param=None, known_value="cyclable lithium capacity", options=None
+    parameter_values,
+    param=None,
+    known_value="cyclable lithium capacity",
+    options=None,
+    inputs=None,
 ):
     """
     Calculate min/max stoichiometries
@@ -945,7 +959,7 @@ def get_min_max_stoichiometries(
         The min/max stoichiometries
     """
     esoh_solver = ElectrodeSOHSolver(parameter_values, param, known_value, options)
-    return esoh_solver.get_min_max_stoichiometries()
+    return esoh_solver.get_min_max_stoichiometries(inputs=inputs)
 
 
 def get_initial_ocps(
@@ -995,7 +1009,11 @@ def get_initial_ocps(
 
 
 def get_min_max_ocps(
-    parameter_values, param=None, known_value="cyclable lithium capacity", options=None
+    parameter_values,
+    param=None,
+    known_value="cyclable lithium capacity",
+    options=None,
+    inputs=None,
 ):
     """
     Calculate min/max open-circuit potentials
@@ -1022,7 +1040,7 @@ def get_min_max_ocps(
         The min/max OCPs
     """
     esoh_solver = ElectrodeSOHSolver(parameter_values, param, known_value, options)
-    return esoh_solver.get_min_max_ocps()
+    return esoh_solver.get_min_max_ocps(inputs=inputs)
 
 
 def theoretical_energy_integral(parameter_values, param, inputs, points=100):
@@ -1049,7 +1067,7 @@ def theoretical_energy_integral(parameter_values, param, inputs, points=100):
 
 
 def calculate_theoretical_energy(
-    parameter_values, initial_soc=1.0, final_soc=0.0, points=100, tol=1e-6
+    parameter_values, initial_soc=1.0, final_soc=0.0, points=100, tol=1e-6, inputs=None
 ):
     """
     Calculate maximum energy possible from a cell given OCV, initial soc, and final soc
@@ -1075,7 +1093,9 @@ def calculate_theoretical_energy(
     # Get initial and final stoichiometric values.
     x_100, y_100 = get_initial_stoichiometries(initial_soc, parameter_values, tol=tol)
     x_0, y_0 = get_initial_stoichiometries(final_soc, parameter_values, tol=tol)
-    Q_p = parameter_values.evaluate(pybamm.LithiumIonParameters().p.prim.Q_init)
+    Q_p = parameter_values.evaluate(
+        pybamm.LithiumIonParameters().p.prim.Q_init, inputs=inputs
+    )
     E = theoretical_energy_integral(
         parameter_values,
         pybamm.LithiumIonParameters(),
diff --git a/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh_half_cell.py b/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh_half_cell.py
index cf2165ae6f..aeebb7b86f 100644
--- a/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh_half_cell.py
+++ b/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh_half_cell.py
@@ -98,8 +98,8 @@ def get_initial_stoichiometry_half_cell(
 
     if isinstance(initial_value, str) and initial_value.endswith("V"):
         V_init = float(initial_value[:-1])
-        V_min = parameter_values.evaluate(param.voltage_low_cut)
-        V_max = parameter_values.evaluate(param.voltage_high_cut)
+        V_min = parameter_values.evaluate(param.voltage_low_cut, inputs=inputs)
+        V_max = parameter_values.evaluate(param.voltage_high_cut, inputs=inputs)
 
         if not V_min - tol <= V_init <= V_max + tol:
             raise ValueError(
@@ -121,7 +121,9 @@ def get_initial_stoichiometry_half_cell(
         soc_model.variables["soc"] = soc
         parameter_values.process_model(soc_model)
         initial_soc = (
-            pybamm.AlgebraicSolver(tol=tol).solve(soc_model, [0])["soc"].data[0]
+            pybamm.AlgebraicSolver(tol=tol)
+            .solve(soc_model, [0], inputs=inputs)["soc"]
+            .data[0]
         )
     elif isinstance(initial_value, int | float):
         initial_soc = initial_value
diff --git a/src/pybamm/parameters/parameter_values.py b/src/pybamm/parameters/parameter_values.py
index 19b41b530e..0246972634 100644
--- a/src/pybamm/parameters/parameter_values.py
+++ b/src/pybamm/parameters/parameter_values.py
@@ -346,7 +346,7 @@ def set_initial_stoichiometry_half_cell(
         else:
             parameter_values = self.copy()
 
-        c_max = self.evaluate(param.p.prim.c_max)
+        c_max = self.evaluate(param.p.prim.c_max, inputs=inputs)
 
         parameter_values.update(
             {
@@ -385,8 +385,8 @@ def set_initial_stoichiometries(
             parameter_values = self
         else:
             parameter_values = self.copy()
-        c_n_max = self.evaluate(param.n.prim.c_max)
-        c_p_max = self.evaluate(param.p.prim.c_max)
+        c_n_max = self.evaluate(param.n.prim.c_max, inputs=inputs)
+        c_p_max = self.evaluate(param.p.prim.c_max, inputs=inputs)
         parameter_values.update(
             {
                 "Initial concentration in negative electrode [mol.m-3]": x * c_n_max,
@@ -402,6 +402,7 @@ def set_initial_ocps(
         known_value="cyclable lithium capacity",
         inplace=True,
         options=None,
+        inputs=None,
     ):
         """
         Set the initial OCP of each electrode, based on the initial
@@ -409,7 +410,12 @@ def set_initial_ocps(
         """
         param = param or pybamm.LithiumIonParameters(options)
         Un, Up = pybamm.lithium_ion.get_initial_ocps(
-            initial_value, self, param=param, known_value=known_value, options=options
+            initial_value,
+            self,
+            param=param,
+            known_value=known_value,
+            options=options,
+            inputs=inputs,
         )
         if inplace:
             parameter_values = self
diff --git a/src/pybamm/simulation.py b/src/pybamm/simulation.py
index 766623fcce..c373fe6911 100644
--- a/src/pybamm/simulation.py
+++ b/src/pybamm/simulation.py
@@ -273,7 +273,11 @@ def set_initial_soc(self, initial_soc, inputs=None):
         if options["open-circuit potential"] == "MSMR":
             self._parameter_values = (
                 self._unprocessed_parameter_values.set_initial_ocps(
-                    initial_soc, param=param, inplace=False, options=options
+                    initial_soc,
+                    param=param,
+                    inplace=False,
+                    options=options,
+                    inputs=inputs,
                 )
             )
         elif options["working electrode"] == "positive":


Test patch:
diff --git a/tests/unit/test_parameters/test_parameter_values.py b/tests/unit/test_parameters/test_parameter_values.py
index b4efa3018d..81b0cb0e28 100644
--- a/tests/unit/test_parameters/test_parameter_values.py
+++ b/tests/unit/test_parameters/test_parameter_values.py
@@ -103,6 +103,18 @@ def test_set_initial_stoichiometries(self):
         y_100 = param_100["Initial concentration in positive electrode [mol.m-3]"]
         assert y == pytest.approx(y_0 - 0.4 * (y_0 - y_100))
 
+        # check that passing inputs gives the same result
+        input_param = "Maximum concentration in positive electrode [mol.m-3]"
+        input_value = param[input_param]
+        param[input_param] = "[input]"
+        param_0_inputs = param.set_initial_stoichiometries(
+            0, inplace=False, inputs={input_param: input_value}
+        )
+        assert (
+            param_0_inputs["Initial concentration in positive electrode [mol.m-3]"]
+            == y_0
+        )
+
     def test_set_initial_stoichiometry_half_cell(self):
         param = pybamm.lithium_ion.DFN(
             {"working electrode": "positive"}
@@ -146,6 +158,21 @@ def test_set_initial_stoichiometry_half_cell(self):
         y_100 = param_100["Initial concentration in positive electrode [mol.m-3]"]
         assert y == pytest.approx(y_0 - 0.4 * (y_0 - y_100))
 
+        # check that passing inputs gives the same result
+        input_param = "Maximum concentration in positive electrode [mol.m-3]"
+        input_value = param[input_param]
+        param[input_param] = "[input]"
+        param_0_inputs = param.set_initial_stoichiometry_half_cell(
+            0,
+            inplace=False,
+            options={"working electrode": "positive"},
+            inputs={input_param: input_value},
+        )
+        assert (
+            param_0_inputs["Initial concentration in positive electrode [mol.m-3]"]
+            == y_0
+        )
+
         # test error
         param = pybamm.ParameterValues("Chen2020")
         with pytest.raises(OptionError, match="working electrode"):
@@ -172,6 +199,15 @@ def test_set_initial_ocps(self):
         Up_100 = param_100["Initial voltage in positive electrode [V]"]
         assert Up_100 - Un_100 == pytest.approx(4.2)
 
+        # check that passing inputs gives the same result
+        input_param = "Maximum concentration in positive electrode [mol.m-3]"
+        input_value = param_100[input_param]
+        param_100[input_param] = "[input]"
+        param_0_inputs = param_100.set_initial_ocps(
+            0, inplace=False, options=options, inputs={input_param: input_value}
+        )
+        assert param_0_inputs["Initial voltage in positive electrode [V]"] == Up_0
+
     def test_check_parameter_values(self):
         with pytest.raises(ValueError, match="propotional term"):
             pybamm.ParameterValues(