Repository: pybamm-team/PyBaMM

Issue:
[Bug]: Interpolants seem to require x-data to be increasing (not decreasing)
### PyBaMM Version

25.6

### Python Version

3.12.3

### Describe the bug

In some edge cases, the initial voltage depends on the direction (monotonically increasing/decreasing) of the interpolation x-data. I am using a custom model but just flipping the interpolation data is enough to produce different results. The case with increasing x-data gives the expected result.

### Steps to Reproduce

Minimum working example:
```
import pybamm
import pybop
import numpy as np
import matplotlib.pyplot as plt


# Load the OCP charge data with decreasing stoichiometry
ocp_data = {
    "Stoichiometry": np.asarray([1, 0.8747, 0.84609, 0.80795, 0.76981, 0.73168]),
    "Voltage [V]": np.asarray([3.5, 3.5558, 3.5802, 3.622, 3.6626, 3.6972]),
}

for direction in ["decreasing", "increasing"]:

    if direction == "decreasing":
        def ocp(sto):
            return pybamm.Interpolant(ocp_data["Stoichiometry"], ocp_data["Voltage [V]"], sto)
    else:
        def ocp(sto):
            return pybamm.Interpolant(np.flip(ocp_data["Stoichiometry"]), np.flip(ocp_data["Voltage [V]"]), sto)

    # Define parameters
    parameter_set = {
        'Nominal cell capacity [A.h]': 0.003,
        'Current function [A]': 0.003,
        'Initial stoichiometry': 0.874707,
        'Electrode OCP [V]': ocp,
        'Theoretical electrode capacity [A.s]': 17.621737,
        'Particle diffusion time scale [s]': 35344.0,
        'Series resistance [Ohm]': 1,
    }

    # Define a charge pulse and relaxation
    time_span = np.linspace(0, 7200, 121)  # 2 hours, 1 point per minute
    current = 0 * time_span
    current[11:20] = -0.00035  # 10 minute charging pulse
    parameter_set["Current function [A]"] = pybamm.Interpolant(time_span, current, pybamm.t)

    # Set up model
    model = pybop.lithium_ion.SPDiffusion(parameter_set=parameter_set, electrode="positive")

    # Set up and run simulation
    sim = pybamm.Simulation(
        model=model.pybamm_model,
        parameter_values=model._unprocessed_parameter_set,
        solver=pybamm.CasadiSolver(),
    )
    sol = sim.solve(t_eval=time_span)
    sol.plot(
        [
            {"Particle stoichiometry", "Particle surface stoichiometry"},
            "Current [A]",
            {"Voltage [V]", "Open-circuit voltage [V]"},
        ],
    )
    plt.show()
```

The output (the first plot shows an inaccurate initial voltage and an unexpected jump at the indicated time):

![Image](https://github.com/user-attachments/assets/bae68ea9-dc7f-4308-ad22-f040ee3133ef)

![Image](https://github.com/user-attachments/assets/0f122dfe-9484-45b5-863d-bda80c09b9cf)

### Relevant log output

```shell

```

Discussion:

Fix (patch):
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 48c51adf3..04138bcc6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@
 ## Bug fixes
 
 - Converts sensitivities to numpy objects, fixing bug in `DiscreteTimeSum` sensitivity calculation ([#5037](https://github.com/pybamm-team/PyBaMM/pull/5037))
+- Raises error if `pybamm.Interpolant` given 1D x values that are not strictly increasing ([#5061](https://github.com/pybamm-team/PyBaMM/pull/5061))
 
 ## Breaking changes
 
diff --git a/src/pybamm/expression_tree/interpolant.py b/src/pybamm/expression_tree/interpolant.py
index ee25c4801..cab751765 100644
--- a/src/pybamm/expression_tree/interpolant.py
+++ b/src/pybamm/expression_tree/interpolant.py
@@ -107,6 +107,9 @@ class Interpolant(pybamm.Function):
                     "len(x1) should equal y=shape[0], "
                     f"but x1.shape={x1.shape} and y.shape={y.shape}"
                 )
+            # if 1d then x should be monotonically increasing
+            if np.any(x1[:-1] > x1[1:]):
+                raise ValueError("x should be monotonically increasing")
         # children should be a list not a symbol or a number
         if isinstance(children, pybamm.Symbol | numbers.Number):
             children = [children]


Test changes:
diff --git a/tests/unit/test_expression_tree/test_interpolant.py b/tests/unit/test_expression_tree/test_interpolant.py
index d004e4d65..b1eb55076 100644
--- a/tests/unit/test_expression_tree/test_interpolant.py
+++ b/tests/unit/test_expression_tree/test_interpolant.py
@@ -76,6 +76,11 @@ class TestInterpolant:
                 interp.evaluate(y=np.array([2]))[:, 0], np.array([np.nan])
             )
 
+    def test_interpolation_non_increasing(self):
+        x = np.flip(np.linspace(0, 1, 200))
+        with pytest.raises(ValueError, match="x should be monotonically increasing"):
+            pybamm.Interpolant(x, 2 * x, 0.5)
+
     def test_interpolation_float(self):
         x = np.linspace(0, 1, 200)
         interp = pybamm.Interpolant(x, 2 * x, 0.5)
diff --git a/tests/unit/test_expression_tree/test_unary_operators.py b/tests/unit/test_expression_tree/test_unary_operators.py
index df6bd86f5..fef1f898d 100644
--- a/tests/unit/test_expression_tree/test_unary_operators.py
+++ b/tests/unit/test_expression_tree/test_unary_operators.py
@@ -773,7 +773,7 @@ class TestUnaryOperators:
             pybamm.DiscreteTimeSum(2 * y)
 
         # check that raises error if two data are present
-        data2 = pybamm.DiscreteTimeData(values, times, "test2")
+        data2 = pybamm.DiscreteTimeData(times, values, "test2")
         with pytest.raises(pybamm.ModelError, match="only have one DiscreteTimeData"):
             pybamm.DiscreteTimeSum(data + data2)