Repository: pymc-devs/pymc

Issue:
BUG: `Model.add_coord` can add multiple `SharedVariable` dimension length with the same name
### Describe the issue:

`Model.add_coord` can add multiple `dim_length` shared variables for the same dimension, which can lead to serious problems down the line.

### Reproduceable code example:

```python
import pymc as pm
from pytensor.graph.basic import get_var_by_name

coord = {"dim1": ["a", "b", "c"]}
with pm.Model(coords=coord) as m:
    a = pm.Normal("a", dims="dim1")
    with pm.Model(coords=coord) as nested_m:
        b = pm.Normal("b", dims="dim1")
    m.add_coords(coord)
    c = pm.Normal("c", dims="dim1")
    d = pm.Deterministic("d", a + b + c)
d.dprint(print_shape=True)
print(get_var_by_name([d], "dim1"))
```

### Error message:

```shell
Identity [id A] shape=(?,) 'd'
 └─ Add [id B] shape=(?,)
    ├─ Add [id C] shape=(?,)
    │  ├─ normal_rv{"(),()->()"}.1 [id D] shape=(?,) 'a'
    │  │  ├─ RNG(<Generator(PCG64) at 0x15D5A8AC0>) [id E]
    │  │  ├─ MakeVector{dtype='int64'} [id F] shape=(1,)
    │  │  │  └─ dim1 [id G] shape=()
    │  │  ├─ ExpandDims{axis=0} [id H] shape=(1,)
    │  │  │  └─ 0 [id I] shape=()
    │  │  └─ ExpandDims{axis=0} [id J] shape=(1,)
    │  │     └─ 1.0 [id K] shape=()
    │  └─ normal_rv{"(),()->()"}.1 [id L] shape=(?,) 'b'
    │     ├─ RNG(<Generator(PCG64) at 0x15E429700>) [id M]
    │     ├─ MakeVector{dtype='int64'} [id N] shape=(1,)
    │     │  └─ dim1 [id O] shape=()
    │     ├─ ExpandDims{axis=0} [id P] shape=(1,)
    │     │  └─ 0 [id Q] shape=()
    │     └─ ExpandDims{axis=0} [id R] shape=(1,)
    │        └─ 1.0 [id S] shape=()
    └─ normal_rv{"(),()->()"}.1 [id T] shape=(?,) 'c'
       ├─ RNG(<Generator(PCG64) at 0x15E4299A0>) [id U]
       ├─ MakeVector{dtype='int64'} [id V] shape=(1,)
       │  └─ dim1 [id W] shape=()
       ├─ ExpandDims{axis=0} [id X] shape=(1,)
       │  └─ 0 [id Y] shape=()
       └─ ExpandDims{axis=0} [id Z] shape=(1,)
          └─ 1.0 [id BA] shape=()
(dim1, dim1, dim1)
```

### PyMC version information:

Current main

### Context for the issue:

This problem popped up because `freeze_dims_and_data` was not replacing all `SharedVariable` dim length variables in the model. When digging deeper, I found that this was happening because there were multiple dimension lengths that had different identities but shared the same name. I'm not sure why `freeze_dims_and_data` didn't still work though.

Discussion:

Solution patch:
diff --git a/pymc/model/core.py b/pymc/model/core.py
index d04011407..469001e80 100644
--- a/pymc/model/core.py
+++ b/pymc/model/core.py
@@ -964,6 +964,7 @@ class Model(WithMemoization, metaclass=ContextMeta):
         if name in self.coords:
             if not np.array_equal(values, self.coords[name]):
                 raise ValueError(f"Duplicate and incompatible coordinate: {name}.")
+            return
         if length is not None and not isinstance(length, int | Variable):
             raise ValueError(
                 f"The `length` passed for the '{name}' coord must be an int, PyTensor Variable or None."


Test patch:
diff --git a/tests/model/test_core.py b/tests/model/test_core.py
index 4375a17ad..b26a9d96b 100644
--- a/tests/model/test_core.py
+++ b/tests/model/test_core.py
@@ -33,6 +33,7 @@ import scipy.sparse as sps
 import scipy.stats as st
 
 from pytensor.graph import graph_inputs
+from pytensor.graph.basic import get_var_by_name
 from pytensor.raise_op import Assert
 from pytensor.tensor.random.op import RandomVariable
 from pytensor.tensor.variable import TensorConstant
@@ -858,6 +859,19 @@ def test_nested_model_coords():
     assert set(m2.named_vars_to_dims) < set(m1.named_vars_to_dims)
 
 
+def test_multiple_add_coords_with_same_name():
+    coord = {"dim1": ["a", "b", "c"]}
+    with pm.Model(coords=coord) as m:
+        a = pm.Normal("a", dims="dim1")
+        with pm.Model(coords=coord) as nested_m:
+            b = pm.Normal("b", dims="dim1")
+        m.add_coords(coord)
+        c = pm.Normal("c", dims="dim1")
+        d = pm.Deterministic("d", a + b + c)
+    variables = get_var_by_name([d], "dim1")
+    assert len(variables) == 1 and variables[0] is m.dim_lengths["dim1"]
+
+
 class TestSetUpdateCoords:
     def test_shapeerror_from_set_data_dimensionality(self):
         with pm.Model() as pmodel: