Repository: pybamm-team/PyBaMM (python)

Issue:
[Bug]: `calc_esoh` does not take input parameters
### PyBaMM Version

23.4.1

### Python Version

3.9.16

### Describe the bug

Currently, the `calc_esoh` option of the simulation does not take into account input parameters. Therefore, if there is an input parameter in the OCVs the solver throws an error.

### Steps to Reproduce

```python3
import pybamm

model = pybamm.lithium_ion.DFN()
parameter_values = pybamm.ParameterValues("Chen2020")

def graphite_LGM50_ocp_Chen2020(sto):
    a = pybamm.Parameter("a")
    u_eq = a * (
        1.9793 * pybamm.exp(-39.3631 * sto)
        + 0.2482
        - 0.0909 * pybamm.tanh(29.8538 * (sto - 0.1234))
        - 0.04478 * pybamm.tanh(14.9159 * (sto - 0.2769))
        - 0.0205 * pybamm.tanh(30.4444 * (sto - 0.6103))
    )

    return u_eq

parameter_values.update({
        "Negative electrode OCP [V]": graphite_LGM50_ocp_Chen2020,
})
parameter_values.update({"a": "[input]"}, check_already_exists=False)
experiment = pybamm.Experiment(["Discharge at 1C until 2.5 V"])

sim = pybamm.Simulation(model, parameter_values=parameter_values, experiment=experiment)

sim.solve([0, 3600], inputs={"a": 1})
```
crashes, but changing the last line for
```python3
sim.solve([0, 3600], inputs={"a": 1}, calc_esoh=False)
```
works.

### Relevant log output

```shell
Traceback (most recent call last):
  File "/home/brosaplanella/support-pybamm/env/lib/python3.9/site-packages/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py", line 260, in solve
    sol = self._solve_full(inputs, ics)
  File "/home/brosaplanella/support-pybamm/env/lib/python3.9/site-packages/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py", line 337, in _solve_full
    sol = sim.solve([0], inputs=inputs)
  File "/home/brosaplanella/support-pybamm/env/lib/python3.9/site-packages/pybamm/simulation.py", line 649, in solve
    self._solution = solver.solve(self.built_model, t_eval, **kwargs)
  File "/home/brosaplanella/support-pybamm/env/lib/python3.9/site-packages/pybamm/solvers/base_solver.py", line 726, in solve
    model_inputs_list = [
  File "/home/brosaplanella/support-pybamm/env/lib/python3.9/site-packages/pybamm/solvers/base_solver.py", line 727, in <listcomp>
    self._set_up_model_inputs(model, inputs) for inputs in inputs_list
  File "/home/brosaplanella/support-pybamm/env/lib/python3.9/site-packages/pybamm/solvers/base_solver.py", line 1337, in _set_up_model_inputs
    raise pybamm.SolverError(f"No value provided for input '{name}'")
pybamm.expression_tree.exceptions.SolverError: No value provided for input 'a'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/brosaplanella/support-pybamm/env/lib/python3.9/site-packages/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py", line 264, in solve
    sol = self._solve_split(inputs, ics)
  File "/home/brosaplanella/support-pybamm/env/lib/python3.9/site-packages/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py", line 344, in _solve_split
    x100_sol = x100_sim.solve([0], inputs=inputs)
  File "/home/brosaplanella/support-pybamm/env/lib/python3.9/site-packages/pybamm/simulation.py", line 649, in solve
    self._solution = solver.solve(self.built_model, t_eval, **kwargs)
  File "/home/brosaplanella/support-pybamm/env/lib/python3.9/site-packages/pybamm/solvers/base_solver.py", line 726, in solve
    model_inputs_list = [
  File "/home/brosaplanella/support-pybamm/env/lib/python3.9/site-packages/pybamm/solvers/base_solver.py", line 727, in <listcomp>
    self._set_up_model_inputs(model, inputs) for inputs in inputs_list
  File "/home/brosaplanella/support-pybamm/env/lib/python3.9/site-packages/pybamm/solvers/base_solver.py", line 1337, in _set_up_model_inputs
    raise pybamm.SolverError(f"No value provided for input '{name}'")
pybamm.expression_tree.exceptions.SolverError: No value provided for input 'a'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/brosaplanella/support-pybamm/env/lib/python3.9/site-packages/pybamm/expression_tree/input_parameter.py", line 79, in _base_evaluate
    input_eval = inputs[self.name]
KeyError: 'a'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/brosaplanella/support-pybamm/mwe_cacl_esoh", line 26, in <module>
    sim.solve([0, 3600], inputs={"a": 1}, calc_esoh=True)
  File "/home/brosaplanella/support-pybamm/env/lib/python3.9/site-packages/pybamm/simulation.py", line 838, in solve
    cycle_sol = pybamm.make_cycle_solution(
  File "/home/brosaplanella/support-pybamm/env/lib/python3.9/site-packages/pybamm/solvers/solution.py", line 809, in make_cycle_solution
    cycle_summary_variables = _get_cycle_summary_variables(cycle_solution, esoh_solver)
  File "/home/brosaplanella/support-pybamm/env/lib/python3.9/site-packages/pybamm/solvers/solution.py", line 873, in _get_cycle_summary_variables
    esoh_sol = esoh_solver.solve(inputs)
  File "/home/brosaplanella/support-pybamm/env/lib/python3.9/site-packages/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py", line 267, in solve
    self._check_esoh_feasible(inputs)
  File "/home/brosaplanella/support-pybamm/env/lib/python3.9/site-packages/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py", line 426, in _check_esoh_feasible
    self.OCV_function.evaluate(inputs={"x": x0_min, "y": y0_max})
  File "/home/brosaplanella/support-pybamm/env/lib/python3.9/site-packages/pybamm/expression_tree/binary_operators.py", line 107, in evaluate
    right = self.right.evaluate(t, y, y_dot, inputs)
  File "/home/brosaplanella/support-pybamm/env/lib/python3.9/site-packages/pybamm/expression_tree/binary_operators.py", line 106, in evaluate
    left = self.left.evaluate(t, y, y_dot, inputs)
  File "/home/brosaplanella/support-pybamm/env/lib/python3.9/site-packages/pybamm/expression_tree/binary_operators.py", line 106, in evaluate
    left = self.left.evaluate(t, y, y_dot, inputs)
  File "/home/brosaplanella/support-pybamm/env/lib/python3.9/site-packages/pybamm/expression_tree/symbol.py", line 747, in evaluate
    return self._base_evaluate(t, y, y_dot, inputs)
  File "/home/brosaplanella/support-pybamm/env/lib/python3.9/site-packages/pybamm/expression_tree/input_parameter.py", line 82, in _base_evaluate
    raise KeyError("Input parameter '{}' not found".format(self.name))
KeyError: "Input parameter 'a' not found"
```


Pull request:
Allow input parameters in ESOH model
# Description

Allows input parameters in the ESOH model. This allows for use of the `initial_soc` keyword in `sim.solve` for variables such as `Positive electrode active material volume fraction`, which previously errored.

Fixes #2981 

## Type of change

Please add a line in the relevant section of [CHANGELOG.md](https://github.com/pybamm-team/PyBaMM/blob/develop/CHANGELOG.md) to document the change (include PR #) - note reverse order of PR #s. If necessary, also add to the list of breaking changes.

- [ ] New feature (non-breaking change which adds functionality)
- [ ] Optimization (back-end change that speeds up the code)
- [x] Bug fix (non-breaking change which fixes an issue)

# Key checklist:

- [ ] No style issues: `$ pre-commit run` (or `$ nox -s pre-commit`) (see [CONTRIBUTING.md](https://github.com/pybamm-team/PyBaMM/blob/develop/CONTRIBUTING.md#installing-and-using-pre-commit) for how to set this up to run automatically when committing locally, in just two lines of code)
- [ ] All tests pass: `$ python run-tests.py --all` (or `$ nox -s tests`)
- [ ] The documentation builds: `$ python run-tests.py --doctest` (or `$ nox -s doctests`)

You can run integration tests, unit tests, and doctests together at once, using `$ python run-tests.py --quick` (or `$ nox -s quick`).

## Further checks:

- [ ] Code is commented, particularly in hard-to-understand areas
- [ ] Tests added that prove fix is effective or that feature works


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5cab08069..68f9caa6a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,7 @@
 - Updated `_steps_util.py` to throw a specific exception when drive cycle starts at t>0 ([#3756](https://github.com/pybamm-team/PyBaMM/pull/3756))
 - Updated `plot_voltage_components.py` to support both `Simulation` and `Solution` objects. Added new methods in both `Simulation` and `Solution` classes for allow the syntax `simulation.plot_voltage_components` and `solution.plot_voltage_components`. Updated `test_plot_voltage_components.py` to reflect these changes ([#3723](https://github.com/pybamm-team/PyBaMM/pull/3723)).
 - The SEI thickness decreased at some intervals when the 'electron-migration limited' model was used. It has been corrected ([#3622](https://github.com/pybamm-team/PyBaMM/pull/3622))
+- Allow input parameters in ESOH model ([#3921](https://github.com/pybamm-team/PyBaMM/pull/3921))
 
 ## Optimizations
 
diff --git a/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py b/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py
index ad8ee0633..d67a998be 100644
--- a/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py
+++ b/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py
@@ -121,6 +121,10 @@ class _ElectrodeSOH(_BaseElectrodeSOH):
             Q_Li = pybamm.InputParameter("Q_Li")
         elif known_value == "cell capacity":
             Q = pybamm.InputParameter("Q")
+        else:
+            raise ValueError(
+                "Known value must be cell capacity or cyclable lithium capacity"
+            )
 
         # Define variables for 100% state of charge
         if "x_100" in solve_for:
@@ -207,6 +211,10 @@ class _ElectrodeSOHMSMR(_BaseElectrodeSOH):
             Q_Li = pybamm.InputParameter("Q_Li")
         elif known_value == "cell capacity":
             Q = pybamm.InputParameter("Q")
+        else:
+            raise ValueError(
+                "Known value must be cell capacity or cyclable lithium capacity"
+            )
 
         # Define variables for 0% state of charge
         # TODO: thermal effects (include dU/dT)
@@ -287,6 +295,10 @@ class ElectrodeSOHSolver:
     ):
         self.parameter_values = parameter_values
         self.param = param or pybamm.LithiumIonParameters(options)
+        if known_value not in ["cell capacity", "cyclable lithium capacity"]:
+            raise ValueError(
+                "Known value must be cell capacity or cyclable lithium capacity"
+            )
         self.known_value = known_value
         self.options = options or pybamm.BatteryModelOptions({})
 
@@ -380,7 +392,8 @@ class ElectrodeSOHSolver:
         # Calculate theoretical energy
         # TODO: energy calc for MSMR
         if self.options["open-circuit potential"] != "MSMR":
-            energy = self.theoretical_energy_integral(sol_dict)
+            energy_inputs = {**sol_dict, **inputs}
+            energy = self.theoretical_energy_integral(energy_inputs)
             sol_dict.update({"Maximum theoretical energy [W.h]": energy})
         return sol_dict
 
@@ -604,12 +617,10 @@ class ElectrodeSOHSolver:
         else:
             # address numpy 1.25 deprecation warning: array should have ndim=0
             # before conversion
-            V_lower_bound = float(
-                self.OCV_function.evaluate(inputs={"x": x0_min, "y": y0_max}).item()
-            )
-            V_upper_bound = float(
-                self.OCV_function.evaluate(inputs={"x": x100_max, "y": y100_min}).item()
-            )
+            all_inputs = {**inputs, "x": x0_min, "y": y0_max}
+            V_lower_bound = float(self.OCV_function.evaluate(inputs=all_inputs).item())
+            all_inputs.update({"x": x100_max, "y": y100_min})
+            V_upper_bound = float(self.OCV_function.evaluate(inputs=all_inputs).item())
 
         # Check that the min and max achievable voltages span wider than the desired
         # voltage range
@@ -632,7 +643,7 @@ class ElectrodeSOHSolver:
                 )
             )
 
-    def get_initial_stoichiometries(self, initial_value, tol=1e-6):
+    def get_initial_stoichiometries(self, initial_value, tol=1e-6, inputs=None):
         """
         Calculate initial stoichiometries to start off the simulation at a particular
         state of charge, given voltage limits, open-circuit potentials, etc defined by
@@ -657,7 +668,7 @@ class ElectrodeSOHSolver:
         """
         parameter_values = self.parameter_values
         param = self.param
-        x_0, x_100, y_100, y_0 = self.get_min_max_stoichiometries()
+        x_0, x_100, y_100, y_0 = self.get_min_max_stoichiometries(inputs=inputs)
 
         if isinstance(initial_value, str) and initial_value.endswith("V"):
             V_init = float(initial_value[:-1])
@@ -714,7 +725,7 @@ class ElectrodeSOHSolver:
 
         return x, y
 
-    def get_min_max_stoichiometries(self):
+    def get_min_max_stoichiometries(self, inputs=None):
         """
         Calculate min/max stoichiometries
         given voltage limits, open-circuit potentials, etc defined by parameter_values
@@ -724,20 +735,23 @@ class ElectrodeSOHSolver:
         x_0, x_100, y_100, y_0
             The min/max stoichiometries
         """
+        inputs = inputs or {}
         parameter_values = self.parameter_values
         param = self.param
 
-        Q_n = parameter_values.evaluate(param.n.Q_init)
-        Q_p = parameter_values.evaluate(param.p.Q_init)
+        Q_n = parameter_values.evaluate(param.n.Q_init, inputs=inputs)
+        Q_p = parameter_values.evaluate(param.p.Q_init, inputs=inputs)
 
         if self.known_value == "cyclable lithium capacity":
-            Q_Li = parameter_values.evaluate(param.Q_Li_particles_init)
-            inputs = {"Q_n": Q_n, "Q_p": Q_p, "Q_Li": Q_Li}
+            Q_Li = parameter_values.evaluate(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(param.Q / param.n_electrodes_parallel)
-            inputs = {"Q_n": Q_n, "Q_p": Q_p, "Q": Q}
+            Q = parameter_values.evaluate(
+                param.Q / param.n_electrodes_parallel, inputs=inputs
+            )
+            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["x_0"], sol["x_100"], sol["y_100"], sol["y_0"]]
 
     def get_initial_ocps(self, initial_value, tol=1e-6):
@@ -814,7 +828,7 @@ class ElectrodeSOHSolver:
         param = self.param
         T = param.T_amb_av(0)
         Vs = self.parameter_values.evaluate(
-            param.p.prim.U(y_vals, T) - param.n.prim.U(x_vals, T)
+            param.p.prim.U(y_vals, T) - param.n.prim.U(x_vals, T), inputs=inputs
         ).flatten()
         # Calculate dQ
         Q = Q_p * (y_0 - y_100)
@@ -831,6 +845,7 @@ def get_initial_stoichiometries(
     known_value="cyclable lithium capacity",
     options=None,
     tol=1e-6,
+    inputs=None,
 ):
     """
     Calculate initial stoichiometries to start off the simulation at a particular
@@ -866,7 +881,7 @@ def get_initial_stoichiometries(
         The initial stoichiometries that give the desired initial state of charge
     """
     esoh_solver = ElectrodeSOHSolver(parameter_values, param, known_value, options)
-    return esoh_solver.get_initial_stoichiometries(initial_value, tol)
+    return esoh_solver.get_initial_stoichiometries(initial_value, tol, inputs=inputs)
 
 
 def get_min_max_stoichiometries(
diff --git a/pybamm/models/full_battery_models/lithium_ion/electrode_soh_half_cell.py b/pybamm/models/full_battery_models/lithium_ion/electrode_soh_half_cell.py
index 92cbc73dc..4607e7d41 100644
--- a/pybamm/models/full_battery_models/lithium_ion/electrode_soh_half_cell.py
+++ b/pybamm/models/full_battery_models/lithium_ion/electrode_soh_half_cell.py
@@ -62,8 +62,9 @@ def get_initial_stoichiometry_half_cell(
     initial_value,
     parameter_values,
     param=None,
-    known_value="cyclable lithium capacity",
     options=None,
+    inputs=None,
+    **kwargs,
 ):
     """
     Calculate initial stoichiometry to start off the simulation at a particular
@@ -86,7 +87,7 @@ def get_initial_stoichiometry_half_cell(
         The initial stoichiometry that give the desired initial state of charge
     """
     param = pybamm.LithiumIonParameters(options)
-    x_0, x_100 = get_min_max_stoichiometries(parameter_values)
+    x_0, x_100 = get_min_max_stoichiometries(parameter_values, inputs=inputs)
 
     if isinstance(initial_value, str) and initial_value.endswith("V"):
         V_init = float(initial_value[:-1])
@@ -129,7 +130,7 @@ def get_initial_stoichiometry_half_cell(
     return x
 
 
-def get_min_max_stoichiometries(parameter_values, options=None):
+def get_min_max_stoichiometries(parameter_values, options=None, inputs=None):
     """
     Get the minimum and maximum stoichiometries from the parameter values
 
@@ -142,12 +143,15 @@ def get_min_max_stoichiometries(parameter_values, options=None):
         :class:`pybamm.BatteryModelOptions`.
         If None, the default is used: {"working electrode": "positive"}
     """
+    inputs = inputs or {}
     if options is None:
         options = {"working electrode": "positive"}
     esoh_model = pybamm.lithium_ion.ElectrodeSOHHalfCell("ElectrodeSOH")
     param = pybamm.LithiumIonParameters(options)
+    Q_w = parameter_values.evaluate(param.p.Q_init, inputs=inputs)
+    # Add Q_w to input parameters
+    all_inputs = {**inputs, "Q_w": Q_w}
     esoh_sim = pybamm.Simulation(esoh_model, parameter_values=parameter_values)
-    Q_w = parameter_values.evaluate(param.p.Q_init)
-    esoh_sol = esoh_sim.solve([0], inputs={"Q_w": Q_w})
+    esoh_sol = esoh_sim.solve([0], inputs=all_inputs)
     x_0, x_100 = esoh_sol["x_0"].data[0], esoh_sol["x_100"].data[0]
     return x_0, x_100
diff --git a/pybamm/parameters/parameter_values.py b/pybamm/parameters/parameter_values.py
index f2dd9ec63..402000b5a 100644
--- a/pybamm/parameters/parameter_values.py
+++ b/pybamm/parameters/parameter_values.py
@@ -292,6 +292,7 @@ class ParameterValues:
         known_value="cyclable lithium capacity",
         inplace=True,
         options=None,
+        inputs=None,
     ):
         """
         Set the initial stoichiometry of the working electrode, based on the initial
@@ -299,7 +300,12 @@ class ParameterValues:
         """
         param = param or pybamm.LithiumIonParameters(options)
         x = pybamm.lithium_ion.get_initial_stoichiometry_half_cell(
-            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
@@ -324,6 +330,7 @@ class ParameterValues:
         known_value="cyclable lithium capacity",
         inplace=True,
         options=None,
+        inputs=None,
         tol=1e-6,
     ):
         """
@@ -338,6 +345,7 @@ class ParameterValues:
             known_value=known_value,
             options=options,
             tol=tol,
+            inputs=inputs,
         )
         if inplace:
             parameter_values = self
@@ -769,7 +777,7 @@ class ParameterValues:
             # Backup option: return the object
             return symbol
 
-    def evaluate(self, symbol):
+    def evaluate(self, symbol, inputs=None):
         """
         Process and evaluate a symbol.
 
@@ -787,7 +795,15 @@ class ParameterValues:
         if processed_symbol.is_constant():
             return processed_symbol.evaluate()
         else:
-            raise ValueError("symbol must evaluate to a constant scalar or array")
+            # In the case that the only issue is an input parameter contained in inputs,
+            # go ahead and try and evaluate it with the inputs. If it doesn't work, raise
+            # the value error.
+            try:
+                return processed_symbol.evaluate(inputs=inputs)
+            except Exception as exc:
+                raise ValueError(
+                    "symbol must evaluate to a constant scalar or array"
+                ) from exc
 
     def _ipython_key_completions_(self):
         return list(self._dict_items.keys())
diff --git a/pybamm/simulation.py b/pybamm/simulation.py
index 24e1db30c..060d1cdd8 100644
--- a/pybamm/simulation.py
+++ b/pybamm/simulation.py
@@ -228,7 +228,7 @@ class Simulation:
         self._parameter_values.process_geometry(self._geometry)
         self._model = self._model_with_set_params
 
-    def set_initial_soc(self, initial_soc):
+    def set_initial_soc(self, initial_soc, inputs=None):
         if self._built_initial_soc != initial_soc:
             # reset
             self._model_with_set_params = None
@@ -247,20 +247,28 @@ class Simulation:
         elif options["working electrode"] == "positive":
             self._parameter_values = (
                 self._unprocessed_parameter_values.set_initial_stoichiometry_half_cell(
-                    initial_soc, param=param, inplace=False, options=options
+                    initial_soc,
+                    param=param,
+                    inplace=False,
+                    options=options,
+                    inputs=inputs,
                 )
             )
         else:
             self._parameter_values = (
                 self._unprocessed_parameter_values.set_initial_stoichiometries(
-                    initial_soc, param=param, inplace=False, options=options
+                    initial_soc,
+                    param=param,
+                    inplace=False,
+                    options=options,
+                    inputs=inputs,
                 )
             )
 
         # Save solved initial SOC in case we need to re-build the model
         self._built_initial_soc = initial_soc
 
-    def build(self, check_model=True, initial_soc=None):
+    def build(self, check_model=True, initial_soc=None, inputs=None):
         """
         A method to build the model into a system of matrices and vectors suitable for
         performing numerical computations. If the model has already been built or
@@ -279,7 +287,7 @@ class Simulation:
             set.
         """
         if initial_soc is not None:
-            self.set_initial_soc(initial_soc)
+            self.set_initial_soc(initial_soc, inputs=inputs)
 
         if self.built_model:
             return
@@ -296,13 +304,13 @@ class Simulation:
             # rebuilt model so clear solver setup
             self._solver._model_set_up = {}
 
-    def build_for_experiment(self, check_model=True, initial_soc=None):
+    def build_for_experiment(self, check_model=True, initial_soc=None, inputs=None):
         """
         Similar to :meth:`Simulation.build`, but for the case of simulating an
         experiment, where there may be several models and solvers to build.
         """
         if initial_soc is not None:
-            self.set_initial_soc(initial_soc)
+            self.set_initial_soc(initial_soc, inputs)
 
         if self.steps_to_built_models:
             return
@@ -342,6 +350,7 @@ class Simulation:
         initial_soc=None,
         callbacks=None,
         showprogress=False,
+        inputs=None,
         **kwargs,
     ):
         """
@@ -404,8 +413,10 @@ class Simulation:
         callbacks = pybamm.callbacks.setup_callbacks(callbacks)
         logs = {}
 
+        inputs = inputs or {}
+
         if self.operating_mode in ["without experiment", "drive cycle"]:
-            self.build(check_model=check_model, initial_soc=initial_soc)
+            self.build(check_model=check_model, initial_soc=initial_soc, inputs=inputs)
             if save_at_cycles is not None:
                 raise ValueError(
                     "'save_at_cycles' option can only be used if simulating an "
@@ -476,11 +487,15 @@ class Simulation:
                             stacklevel=2,
                         )
 
-            self._solution = solver.solve(self.built_model, t_eval, **kwargs)
+            self._solution = solver.solve(
+                self.built_model, t_eval, inputs=inputs, **kwargs
+            )
 
         elif self.operating_mode == "with experiment":
             callbacks.on_experiment_start(logs)
-            self.build_for_experiment(check_model=check_model, initial_soc=initial_soc)
+            self.build_for_experiment(
+                check_model=check_model, initial_soc=initial_soc, inputs=inputs
+            )
             if t_eval is not None:
                 pybamm.logger.warning(
                     "Ignoring t_eval as solution times are specified by the experiment"
@@ -489,7 +504,7 @@ class Simulation:
             # inputs without having to build the simulation again
             self._solution = starting_solution
             # Step through all experimental conditions
-            user_inputs = kwargs.get("inputs", {})
+            user_inputs = inputs
             timer = pybamm.Timer()
 
             # Set up eSOH solver (for summary variables)
@@ -508,6 +523,7 @@ class Simulation:
                     [starting_solution],
                     esoh_solver=esoh_solver,
                     save_this_cycle=True,
+                    inputs=user_inputs,
                 )
                 starting_solution_cycles = [cycle_solution]
                 starting_solution_summary_variables = [cycle_sum_vars]
@@ -565,7 +581,7 @@ class Simulation:
                         # logs["step operating conditions"] = "Initial rest for padding"
                         # callbacks.on_step_start(logs)
 
-                        kwargs["inputs"] = {
+                        inputs = {
                             **user_inputs,
                             "Ambient temperature [K]": (
                                 step.temperature
@@ -577,7 +593,7 @@ class Simulation:
                         step_solution = current_solution.cycles[-1].steps[-1]
 
                         step_solution_with_rest = self.run_padding_rest(
-                            kwargs, rest_time, step_solution
+                            kwargs, rest_time, step_solution, inputs
                         )
                         steps[-1] = step_solution + step_solution_with_rest
 
@@ -663,7 +679,7 @@ class Simulation:
                     logs["step operating conditions"] = step_str
                     callbacks.on_step_start(logs)
 
-                    kwargs["inputs"] = {
+                    inputs = {
                         **user_inputs,
                         "start time": start_time,
                     }
@@ -676,6 +692,7 @@ class Simulation:
                             dt,
                             t_eval=np.linspace(0, dt, npts),
                             save=False,
+                            inputs=inputs,
                             **kwargs,
                         )
                     except pybamm.SolverError as error:
@@ -712,7 +729,7 @@ class Simulation:
                             logs["step operating conditions"] = "Rest for padding"
                             callbacks.on_step_start(logs)
 
-                            kwargs["inputs"] = {
+                            inputs = {
                                 **user_inputs,
                                 "Ambient temperature [K]": (
                                     step.temperature
@@ -722,7 +739,7 @@ class Simulation:
                             }
 
                             step_solution_with_rest = self.run_padding_rest(
-                                kwargs, rest_time, step_solution
+                                kwargs, rest_time, step_solution, inputs=inputs
                             )
                             step_solution += step_solution_with_rest
 
@@ -786,7 +803,10 @@ class Simulation:
                                 "due to exceeded bounds at initial conditions."
                             )
                     cycle_sol = pybamm.make_cycle_solution(
-                        steps, esoh_solver=esoh_solver, save_this_cycle=save_this_cycle
+                        steps,
+                        esoh_solver=esoh_solver,
+                        save_this_cycle=save_this_cycle,
+                        inputs=user_inputs,
                     )
                     cycle_solution, cycle_sum_vars, cycle_first_state = cycle_sol
                     all_cycle_solutions.append(cycle_solution)
@@ -842,7 +862,7 @@ class Simulation:
 
         return self.solution
 
-    def run_padding_rest(self, kwargs, rest_time, step_solution):
+    def run_padding_rest(self, kwargs, rest_time, step_solution, inputs):
         model = self.steps_to_built_models["Rest for padding"]
         solver = self.steps_to_built_solvers["Rest for padding"]
 
@@ -856,6 +876,7 @@ class Simulation:
             rest_time,
             t_eval=np.linspace(0, rest_time, npts),
             save=False,
+            inputs=inputs,
             **kwargs,
         )
 
@@ -868,6 +889,7 @@ class Simulation:
         t_eval=None,
         save=True,
         starting_solution=None,
+        inputs=None,
         **kwargs,
     ):
         """
@@ -908,6 +930,7 @@ class Simulation:
             dt,
             t_eval=t_eval,
             save=save,
+            inputs=inputs,
             **kwargs,
         )
 
diff --git a/pybamm/solvers/solution.py b/pybamm/solvers/solution.py
index b456bfb2d..440d7381d 100644
--- a/pybamm/solvers/solution.py
+++ b/pybamm/solvers/solution.py
@@ -867,7 +867,9 @@ class EmptySolution:
         return EmptySolution(termination=self.termination, t=self.t)
 
 
-def make_cycle_solution(step_solutions, esoh_solver=None, save_this_cycle=True):
+def make_cycle_solution(
+    step_solutions, esoh_solver=None, save_this_cycle=True, inputs=None
+):
     """
     Function to create a Solution for an entire cycle, and associated summary variables
 
@@ -913,7 +915,9 @@ def make_cycle_solution(step_solutions, esoh_solver=None, save_this_cycle=True):
 
     cycle_solution.steps = step_solutions
 
-    cycle_summary_variables = _get_cycle_summary_variables(cycle_solution, esoh_solver)
+    cycle_summary_variables = _get_cycle_summary_variables(
+        cycle_solution, esoh_solver, user_inputs=inputs
+    )
 
     cycle_first_state = cycle_solution.first_state
 
@@ -925,7 +929,8 @@ def make_cycle_solution(step_solutions, esoh_solver=None, save_this_cycle=True):
     return cycle_solution, cycle_summary_variables, cycle_first_state
 
 
-def _get_cycle_summary_variables(cycle_solution, esoh_solver):
+def _get_cycle_summary_variables(cycle_solution, esoh_solver, user_inputs=None):
+    user_inputs = user_inputs or {}
     model = cycle_solution.all_models[0]
     cycle_summary_variables = pybamm.FuzzyDict({})
 
@@ -973,11 +978,9 @@ def _get_cycle_summary_variables(cycle_solution, esoh_solver):
         Q_n = last_state["Negative electrode capacity [A.h]"].data[0]
         Q_p = last_state["Positive electrode capacity [A.h]"].data[0]
         Q_Li = last_state["Total lithium capacity in particles [A.h]"].data[0]
-
-        inputs = {"Q_n": Q_n, "Q_p": Q_p, "Q_Li": Q_Li}
-
+        all_inputs = {**user_inputs, "Q_n": Q_n, "Q_p": Q_p, "Q_Li": Q_Li}
         try:
-            esoh_sol = esoh_solver.solve(inputs)
+            esoh_sol = esoh_solver.solve(inputs=all_inputs)
         except pybamm.SolverError as error:  # pragma: no cover
             raise pybamm.SolverError(
                 "Could not solve for summary variables, run "


Test patch:
diff --git a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_electrode_soh.py b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_electrode_soh.py
index bebdedd82..dd7d35b68 100644
--- a/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_electrode_soh.py
+++ b/tests/unit/test_models/test_full_battery_models/test_lithium_ion/test_electrode_soh.py
@@ -235,15 +235,12 @@ class TestElectrodeSOHMSMR(TestCase):
 class TestElectrodeSOHHalfCell(TestCase):
     def test_known_solution(self):
         model = pybamm.lithium_ion.ElectrodeSOHHalfCell()
-
         param = pybamm.LithiumIonParameters({"working electrode": "positive"})
         parameter_values = pybamm.ParameterValues("Xu2019")
+        Q_w = parameter_values.evaluate(param.p.Q_init)
         sim = pybamm.Simulation(model, parameter_values=parameter_values)
-
         V_min = 3.5
         V_max = 4.2
-        Q_w = parameter_values.evaluate(param.p.Q_init)
-
         # Solve the model and check outputs
         sol = sim.solve([0], inputs={"Q_w": Q_w})
         self.assertAlmostEqual(sol["Uw(x_100)"].data[0], V_max, places=5)
@@ -368,6 +365,30 @@ class TestGetInitialSOC(TestCase):
                 2, parameter_values_half_cell
             )
 
+        with self.assertRaisesRegex(
+            ValueError, "Known value must be cell capacity or cyclable lithium capacity"
+        ):
+            pybamm.lithium_ion.ElectrodeSOHSolver(
+                parameter_values, known_value="something else"
+            )
+
+        with self.assertRaisesRegex(
+            ValueError, "Known value must be cell capacity or cyclable lithium capacity"
+        ):
+            param_MSMR = pybamm.lithium_ion.MSMR(
+                {"number of MSMR reactions": "3"}
+            ).param
+            pybamm.models.full_battery_models.lithium_ion.electrode_soh._ElectrodeSOHMSMR(
+                param=param_MSMR, known_value="something else"
+            )
+
+        with self.assertRaisesRegex(
+            ValueError, "Known value must be cell capacity or cyclable lithium capacity"
+        ):
+            pybamm.models.full_battery_models.lithium_ion.electrode_soh._ElectrodeSOH(
+                known_value="something else"
+            )
+
 
 class TestGetInitialOCP(TestCase):
     def test_get_initial_ocp(self):
diff --git a/tests/unit/test_simulation.py b/tests/unit/test_simulation.py
index 734f22eb3..6565a6d93 100644
--- a/tests/unit/test_simulation.py
+++ b/tests/unit/test_simulation.py
@@ -204,6 +204,38 @@ class TestSimulation(TestCase):
         sim.build(initial_soc=0.5)
         self.assertEqual(sim._built_initial_soc, 0.5)
 
+        # Test that initial soc works with a relevant input parameter
+        model = pybamm.lithium_ion.DFN()
+        param = model.default_parameter_values
+        og_eps_p = param["Positive electrode active material volume fraction"]
+        param["Positive electrode active material volume fraction"] = (
+            pybamm.InputParameter("eps_p")
+        )
+        sim = pybamm.Simulation(model, parameter_values=param)
+        sim.solve(t_eval=[0, 1], initial_soc=0.8, inputs={"eps_p": og_eps_p})
+        self.assertEqual(sim._built_initial_soc, 0.8)
+
+        # test having an input parameter in the ocv function
+        model = pybamm.lithium_ion.SPM()
+        parameter_values = model.default_parameter_values
+        a = pybamm.Parameter("a")
+
+        def ocv_with_parameter(sto):
+            u_eq = (4.2 - 2.5) * (1 - sto) + 2.5
+            return a * u_eq
+
+        parameter_values.update(
+            {
+                "Positive electrode OCP [V]": ocv_with_parameter,
+            }
+        )
+        parameter_values.update({"a": "[input]"}, check_already_exists=False)
+        experiment = pybamm.Experiment(["Discharge at 1C until 2.5 V"])
+        sim = pybamm.Simulation(
+            model, parameter_values=parameter_values, experiment=experiment
+        )
+        sim.solve([0, 3600], inputs={"a": 1})
+
         # Test whether initial_soc works with half cell (solve)
         options = {"working electrode": "positive"}
         model = pybamm.lithium_ion.DFN(options)
@@ -238,6 +270,41 @@ class TestSimulation(TestCase):
         sim.build(initial_soc=0.5)
         self.assertEqual(sim._built_initial_soc, 0.5)
 
+    def test_solve_with_initial_soc_with_input_param_in_ocv(self):
+        # test having an input parameter in the ocv function
+        model = pybamm.lithium_ion.SPM()
+        parameter_values = model.default_parameter_values
+        a = pybamm.Parameter("a")
+
+        def ocv_with_parameter(sto):
+            u_eq = (4.2 - 2.5) * (1 - sto) + 2.5
+            return a * u_eq
+
+        parameter_values.update(
+            {
+                "Positive electrode OCP [V]": ocv_with_parameter,
+            }
+        )
+        parameter_values.update({"a": "[input]"}, check_already_exists=False)
+        experiment = pybamm.Experiment(["Discharge at 1C until 2.5 V"])
+        sim = pybamm.Simulation(
+            model, parameter_values=parameter_values, experiment=experiment
+        )
+        sim.solve([0, 3600], inputs={"a": 1}, initial_soc=0.8)
+        self.assertEqual(sim._built_initial_soc, 0.8)
+
+    def test_esoh_with_input_param(self):
+        # Test that initial soc works with a relevant input parameter
+        model = pybamm.lithium_ion.DFN({"working electrode": "positive"})
+        param = model.default_parameter_values
+        original_eps_p = param["Positive electrode active material volume fraction"]
+        param["Positive electrode active material volume fraction"] = (
+            pybamm.InputParameter("eps_p")
+        )
+        sim = pybamm.Simulation(model, parameter_values=param)
+        sim.solve(t_eval=[0, 1], initial_soc=0.8, inputs={"eps_p": original_eps_p})
+        self.assertEqual(sim._built_initial_soc, 0.8)
+
     def test_solve_with_inputs(self):
         model = pybamm.lithium_ion.SPM()
         param = model.default_parameter_values