Repository: pybamm-team/PyBaMM (python)

Issue:
[Bug]: Exponential1DSubMesh gives bad results
### PyBaMM Version

25.4.0

### Python Version

3.11.11

### Describe the bug

When running a simple DFN model and using the Exponential1DSubmesh, the results end up looking very strange. 

<img width="1056" alt="Image" src="https://github.com/user-attachments/assets/75575633-a5e8-4f91-b44a-9321dc0365eb" />

I assume this is not just a plotting issue, though I am not sure. 

### Steps to Reproduce

```
import pybamm

model = pybamm.lithium_ion.DFN()
parameter_values = model.default_parameter_values
parameter_values.process_model(model)

geometry = model.default_geometry
parameter_values.process_geometry(geometry)

submesh_types = model.default_submesh_types
submesh_types = {}
submesh_types.update({
    "negative electrode": pybamm.Exponential1DSubMesh,
    "negative particle": pybamm.Exponential1DSubMesh,
    "separator": pybamm.Exponential1DSubMesh,
    "positive electrode": pybamm.Exponential1DSubMesh,
    "positive particle": pybamm.Exponential1DSubMesh,
    "current collector": pybamm.SubMesh0D,
})
mesh = pybamm.Mesh(geometry, submesh_types, model.default_var_pts)
disc = pybamm.Discretisation(mesh, model.default_spatial_methods,check_model=True)

disc.process_model(model)

var_pts = {
    "x_n": 30,  # negative electrode
    "x_s": 20,  # separator
    "x_p": 30,  # positive electrode
    "r_n": 20,  # negative particle
    "r_p": 20,  # positive particle
}

sim = pybamm.Simulation(
    model=model,
    submesh_types=submesh_types,
    var_pts=var_pts,
    solver = pybamm.IDAKLUSolver()
)

sim.solve([0,3600])
```


### Relevant log output

```shell

```

Pull request description:
Fix exponential mesh bug
# Description

Fixes a bug with `Exponential1DSubMesh` when the left edge is non-zero.

Fixes #4988 

## 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 #)

# Important checks:

Please confirm the following before marking the PR as ready for review:
- No style issues: `nox -s pre-commit`
- All tests pass: `nox -s tests`
- The documentation builds: `nox -s doctests`
- Code is commented for 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 15b079848..e0a57f117 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # [Unreleased](https://github.com/pybamm-team/PyBaMM/)
 
+## Bug fixes
+
+- Fixed a bug in the `Exponential1DSubMesh` where the mesh was not being created correctly for non-zero minimum values. ([#4989](https://github.com/pybamm-team/PyBaMM/pull/4989))
+
 # [v25.4.2](https://github.com/pybamm-team/PyBaMM/tree/v25.4.2) - 2025-04-17
 
 ## Bug fixes
diff --git a/src/pybamm/meshes/one_dimensional_submeshes.py b/src/pybamm/meshes/one_dimensional_submeshes.py
index 0e95b2ac5..26d8f1293 100644
--- a/src/pybamm/meshes/one_dimensional_submeshes.py
+++ b/src/pybamm/meshes/one_dimensional_submeshes.py
@@ -213,36 +213,37 @@ class Exponential1DSubMesh(SubMesh1D):
             elif side in ["left", "right"]:
                 stretch = 2.3
 
-        # Create edges accoriding to "side"
+        # Create edges according to "side"
         if side == "left":
             ii = np.array(range(0, npts + 1))
-            edges = (b - a) * (np.exp(stretch * ii / npts) - 1) / (
+            edges = a + (b - a) * (np.exp(stretch * ii / npts) - 1) / (
                 np.exp(stretch) - 1
-            ) + a
+            )
 
         elif side == "right":
             ii = np.array(range(0, npts + 1))
-            edges = (b - a) * (np.exp(-stretch * ii / npts) - 1) / (
-                np.exp(-stretch) - 1
-            ) + a
+            edges = b - (b - a) * (np.exp(stretch * (npts - ii) / npts) - 1) / (
+                np.exp(stretch) - 1
+            )
 
         elif side == "symmetric":
-            # Mesh half-interval [a, b/2]
+            # Mesh half-interval [a, (a+b)/2]
             if npts % 2 == 0:
                 ii = np.array(range(0, int((npts) / 2)))
             else:
                 ii = np.array(range(0, int((npts + 1) / 2)))
-            x_exp_left = (b / 2 - a) * (np.exp(stretch * ii / npts) - 1) / (
+            midpoint = (a + b) / 2
+            x_exp_left = a + (midpoint - a) * (np.exp(stretch * ii / npts) - 1) / (
                 np.exp(stretch) - 1
-            ) + a
+            )
 
-            # Refelct mesh
-            x_exp_right = b * np.ones_like(x_exp_left) - (x_exp_left[::-1] - a)
+            # Reflect mesh
+            x_exp_right = b - (x_exp_left[::-1] - a)
 
             # Combine left and right halves of the mesh, adding a node at the
             # centre if npts is even (odd number of edges)
             if npts % 2 == 0:
-                edges = np.concatenate((x_exp_left, [(a + b) / 2], x_exp_right))
+                edges = np.concatenate((x_exp_left, [midpoint], x_exp_right))
             else:
                 edges = np.concatenate((x_exp_left, x_exp_right))
 


Test patch:
diff --git a/tests/unit/test_meshes/test_one_dimensional_submesh.py b/tests/unit/test_meshes/test_one_dimensional_submesh.py
index e50c91115..0f5aaafc7 100644
--- a/tests/unit/test_meshes/test_one_dimensional_submesh.py
+++ b/tests/unit/test_meshes/test_one_dimensional_submesh.py
@@ -160,37 +160,15 @@ class TestSymbolicUniform1DSubMesh:
 
 
 class TestExponential1DSubMesh:
-    def test_symmetric_mesh_creation_no_parameters_even(self, r, geometry):
-        submesh_params = {"side": "symmetric"}
-        submesh_types = {
-            "negative particle": pybamm.MeshGenerator(
-                pybamm.Exponential1DSubMesh, submesh_params
-            )
-        }
-        var_pts = {r: 20}
-
-        # create mesh
-        mesh = pybamm.Mesh(geometry, submesh_types, var_pts)
-
-        # check boundary locations
-        assert mesh["negative particle"].edges[0] == 0
-        assert mesh["negative particle"].edges[-1] == 1
-
-        # check number of edges and nodes
-        assert len(mesh["negative particle"].nodes) == var_pts[r]
-        assert (
-            len(mesh["negative particle"].edges)
-            == len(mesh["negative particle"].nodes) + 1
-        )
-
-    def test_symmetric_mesh_creation_no_parameters_odd(self, r, geometry):
+    @pytest.mark.parametrize("odd_even", [20, 21])
+    def test_symmetric_mesh_creation(self, r, geometry, odd_even):
         submesh_params = {"side": "symmetric", "stretch": 1.5}
         submesh_types = {
             "negative particle": pybamm.MeshGenerator(
                 pybamm.Exponential1DSubMesh, submesh_params
             )
         }
-        var_pts = {r: 21}
+        var_pts = {r: odd_even}
 
         # create mesh
         mesh = pybamm.Mesh(geometry, submesh_types, var_pts)
@@ -206,14 +184,15 @@ class TestExponential1DSubMesh:
             == len(mesh["negative particle"].nodes) + 1
         )
 
-    def test_left_mesh_creation_no_parameters(self, r, geometry):
-        submesh_params = {"side": "left"}
+    @pytest.mark.parametrize("side", ["left", "right"])
+    def test_mesh_creation_with_side(self, r, geometry, side):
+        submesh_params = {"side": side}
         submesh_types = {
             "negative particle": pybamm.MeshGenerator(
                 pybamm.Exponential1DSubMesh, submesh_params
             )
         }
-        var_pts = {r: 21}
+        var_pts = {r: 20}
 
         # create mesh
         mesh = pybamm.Mesh(geometry, submesh_types, var_pts)
@@ -229,28 +208,38 @@ class TestExponential1DSubMesh:
             == len(mesh["negative particle"].nodes) + 1
         )
 
-    def test_right_mesh_creation_no_parameters(self, r, geometry):
-        submesh_params = {"side": "right", "stretch": 2}
+        # check monotonicity based on side
+        if side == "left":
+            # spacing should increase from left to right
+            assert np.all(np.diff(np.diff(mesh["negative particle"].edges)) > 0)
+        else:  # right
+            # spacing should decrease from left to right
+            assert np.all(np.diff(np.diff(mesh["negative particle"].edges)) < 0)
+
+    @pytest.mark.parametrize("side", ["left", "right", "symmetric"])
+    def test_mesh_creation_non_zero_min(self, r, side):
+        geometry = {
+            "negative particle": {r: {"min": pybamm.Scalar(1), "max": pybamm.Scalar(2)}}
+        }
         submesh_types = {
             "negative particle": pybamm.MeshGenerator(
-                pybamm.Exponential1DSubMesh, submesh_params
+                pybamm.Exponential1DSubMesh, {"side": side}
             )
         }
         var_pts = {r: 20}
-
-        # create mesh
         mesh = pybamm.Mesh(geometry, submesh_types, var_pts)
 
         # check boundary locations
-        assert mesh["negative particle"].edges[0] == 0
-        assert mesh["negative particle"].edges[-1] == 1
-
+        assert mesh["negative particle"].edges[0] == 1
+        assert mesh["negative particle"].edges[-1] == 2
         # check number of edges and nodes
         assert len(mesh["negative particle"].nodes) == var_pts[r]
         assert (
             len(mesh["negative particle"].edges)
             == len(mesh["negative particle"].nodes) + 1
         )
+        # check monotonically increasing
+        assert np.all(np.diff(mesh["negative particle"].edges) > 0)
 
 
 class TestChebyshev1DSubMesh: