Repository: matplotlib/matplotlib

Issue:
[Bug]:  Layout Managers are confused by complex arrangement of sub-figures and  gridspec's
### Bug summary

When I run the code below

1. `compressed.png` and `constrained.png`  are (visually) identical, and the axes in the left of each sub-figure are getting smaller and smaller
2. `tight.png` has all the axes in each sub-figure the same sizes, but those sizes are wrong
3. `none.png` is correct (all the sizes remain unchanged, the sizes are OK) but the axes decorations overlap the plotting area

For your convenience I've uploaded the image files produced by my code.

Thank you in advance — gb

---

This behaviour was brought to my attention by
https://stackoverflow.com/questions/79614691/why-are-the-subplots-in-the-subfigures-getting-smaller


### Code for reproduction

```Python
import matplotlib.pyplot as plt


def test(layout):
    fig = plt.figure(figsize=(12, 16), layout=layout)
    figures = fig.subfigures(4, 2)

    for f in figures.flatten():
        gs = f.add_gridspec(3, 2)
        for i in range(3):
            f.add_subplot(gs[i, 0]).plot()
        f.add_subplot(gs[:, 1]).plot()
    fig.savefig(layout + ".png")


test("compressed")
test("constrained")
test("none")
test("tight")
```

### Actual outcome

![Image](https://github.com/user-attachments/assets/bd1f7f46-bc4a-4487-aa17-d0c32a398eaa)
![Image](https://github.com/user-attachments/assets/22d6c6d4-768b-4fa4-b3b6-5af446591be4)
![Image](https://github.com/user-attachments/assets/317ffcf6-e6dc-4626-b63b-dbf7ad5d2138)
![Image](https://github.com/user-attachments/assets/e9dc70ea-d07e-444c-8a49-3752bcc3caac)

### Expected outcome

Something like `none.png` w/o the axes decoration overlapping the plot areas

### Additional information

_No response_

### Operating system

Open Suse Tumbleweed

### Matplotlib Version

3.10.1

### Matplotlib Backend

qtagg

### Python version

Python 3.13.3

### Jupyter version

_No response_

### Installation

Linux package manager


Discussion:
I have seen similar things with code that set a fixed aspect ratio and manually adjusted the locations before running the layout code.


dropping to either the figures rows or cols being 1 makes it work and 

```python
import matplotlib.pyplot as plt


def test(layout):
    fig = plt.figure(figsize=(12, 16), layout=layout)
    figures = fig.subfigures(4, 2)
    fig.suptitle(layout)
    for f in figures.flatten():
        axd = fig.subplot_mosaic('AD;BD;CD')
        for ax in axd.values():
            ax.plot()
    # fig.savefig(layout + ".png")


test("compressed")
test("constrained")
test("none")
test("tight")
```

is broken even worse
Hmmm, well somehow the margin for the first subfigure is the _starting_ point for the second figure, and then the margin gets made even larger for the second.  

```
import matplotlib.pyplot as plt



def test(layout, labelled=False):
    ll = labelled
    fig = plt.figure(figsize=(4, 5), layout=layout)
    fig.suptitle(f'Labelled = {not labelled}', fontsize='small')
    figures = fig.subfigures(2, 1)
    for f in figures.flatten():
        gs = f.add_gridspec(2, 2)
        for i in range(2):
            ax = f.add_subplot(gs[i, 0])
            ax.plot()
            if not labelled:
                ax.set_xlabel('BOOO', fontsize='large')
                labelled = True
        f.add_subplot(gs[:, 1]).plot()
    fig.savefig(f'fig_{not ll}.png')
test("constrained")
test("constrained", labelled=True)
plt.show()
```

## No label
![Image](https://github.com/user-attachments/assets/8920e5d0-097c-4a50-af80-8b118d9894f8)

## big label
![Image](https://github.com/user-attachments/assets/1b4f1e08-53e0-47dc-a1a3-be6cc4a20c88)
I played with this for a while, and I do not see any obvious error in the logic.  

At its root, I think this is a limitation with the we we have chosen to set up the constraint solver, in that the behaviour above is following the constraints we have given it, but they are not rigorous enough to narrow the gap. 

I appreciate that it would be more sensible for the subfigures to not care about each other's layout, but practically, that is not how the constraint engine is set up, and the constraints are all solved at the same time.  

If you want deterministic behaviour here without changing the internals of constrained layout, you can simply create more subfigures so that the left-hand axes are not tied to what the right-hand column is doing:

```python
import matplotlib.pyplot as plt

def test(layout):
    fig = plt.figure(figsize=(4, 5), layout='constrained')
    figures = fig.subfigures(2, 1)
    figures[0].set_facecolor('grey')
    figures[1].set_facecolor('red')
    for nn, f in enumerate(figures.flatten()):
        ffigs = f.subfigures(1, 2)
        ffigs[0].set_facecolor('lightgreen')
        ax = ffigs[0].subplots(2, 1)
        for i in range(2):
            ax[i].plot()
            if (nn==0) and (i == 0):
                ax[i].set_xlabel('BOOOOO', fontsize=20)
        ffigs[1].set_facecolor('lightblue')
        ax = ffigs[1].add_subplot()
        ax.plot()
    fig.savefig(f'Fixed.png')

test("constrained")
plt.show()
``` 

![Image](https://github.com/user-attachments/assets/3abf2f48-200a-4ec5-ad0b-0ed667029e0c)

We can keep this open, but not sure anything will be done about it in the near future.  If we were to do anything, it might be to try and make subfigure1 independent of subfigure2.  The other thing that could be improved is the assignment of space between axes in one column when the other column does not have any axes spines at that row (eg the case here).  That case is mildly under constrained, and as shown here doesn't work consistently.  
Actually I found the bug #30089 is an easy fix.



Patch:
diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py
index 5623e12a3c41..f5f23581bd9d 100644
--- a/lib/matplotlib/_constrained_layout.py
+++ b/lib/matplotlib/_constrained_layout.py
@@ -521,11 +521,13 @@ def match_submerged_margins(layoutgrids, fig):
     See test_constrained_layout::test_constrained_layout12 for an example.
     """
 
+    axsdone = []
     for sfig in fig.subfigs:
-        match_submerged_margins(layoutgrids, sfig)
+        axsdone += match_submerged_margins(layoutgrids, sfig)
 
     axs = [a for a in fig.get_axes()
-           if a.get_subplotspec() is not None and a.get_in_layout()]
+           if (a.get_subplotspec() is not None and a.get_in_layout() and
+               a not in axsdone)]
 
     for ax1 in axs:
         ss1 = ax1.get_subplotspec()
@@ -592,6 +594,8 @@ def match_submerged_margins(layoutgrids, fig):
             for i in ss1.rowspan[:-1]:
                 lg1.edit_margin_min('bottom', maxsubb, cell=i)
 
+    return axs
+
 
 def get_cb_parent_spans(cbax):
     """


Test patch:
diff --git a/lib/matplotlib/tests/test_constrainedlayout.py b/lib/matplotlib/tests/test_constrainedlayout.py
index 7c7dd43a3115..df2dbd6f43bd 100644
--- a/lib/matplotlib/tests/test_constrainedlayout.py
+++ b/lib/matplotlib/tests/test_constrainedlayout.py
@@ -721,3 +721,23 @@ def test_layout_leak():
     gc.collect()
     assert not any(isinstance(obj, mpl._layoutgrid.LayoutGrid)
                    for obj in gc.get_objects())
+
+
+def test_submerged_subfig():
+    """
+    Test that the submerged margin logic does not get called multiple times
+    on same axes if it is already in a subfigure
+    """
+    fig = plt.figure(figsize=(4, 5), layout='constrained')
+    figures = fig.subfigures(3, 1)
+    axs = []
+    for f in figures.flatten():
+        gs = f.add_gridspec(2, 2)
+        for i in range(2):
+            axs += [f.add_subplot(gs[i, 0])]
+            axs[-1].plot()
+        f.add_subplot(gs[:, 1]).plot()
+    fig.draw_without_rendering()
+    for ax in axs[1:]:
+        assert np.allclose(ax.get_position().bounds[-1],
+                           axs[0].get_position().bounds[-1], atol=1e-6)