Repository: simpeg/simpeg

Issue:
BUG: `GaussianMixtureWithPrior.fit_predict` fails with latest version of scikit-learn
### Describe the issue:

Running the `tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py` with the `scikit-learn==1.5.0` falls into an error due to a change in a private method from the upstream `BaseMixture` class.

### Reproducible code example:

```python
python tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py
```


### Error message:

```shell
Downloading https://storage.googleapis.com/simpeg/pgi_tutorial_assets/mesh_tutorial.ubc
   saved to: /home/santi/git/simpeg/mesh_tutorial.ubc
Download completed!
Downloading https://storage.googleapis.com/simpeg/pgi_tutorial_assets/geology_true.mod
   saved to: /home/santi/git/simpeg/geology_true.mod
Download completed!
Downloading https://storage.googleapis.com/simpeg/pgi_tutorial_assets/gravity_data.obs
   saved to: /home/santi/git/simpeg/gravity_data.obs
Download completed!
Downloading https://storage.googleapis.com/simpeg/pgi_tutorial_assets/magnetic_data.obs
   saved to: /home/santi/git/simpeg/magnetic_data.obs
Download completed!
Downloading https://storage.googleapis.com/simpeg/pgi_tutorial_assets/CDED_Lake_warp.xyz
   saved to: /home/santi/git/simpeg/CDED_Lake_warp.xyz
Download completed!

                    simpeg.InvProblem is setting bfgsH0 to the inverse of the eval2Deriv.
                    ***Done using the default solver Pardiso and no solver_opts.***

Alpha scales: [9476216.801984344, 0.0, 6214450.074856385, 0.0, 13503557.75164027, 0.0, 981099553.7221122, 0.0, 736291450.040535, 0.0, 1846725954.305293, 0.0]
<class 'simpeg.regularization.pgi.PGIsmallness'>
Initial data misfit scales:  [0.98049879 0.01950121]
model has any nan: 0
=============================== Projected GNCG ===============================
  #     beta     phi_d     phi_m       f      |proj(x-g)-x|  LS    Comment
-----------------------------------------------------------------------------
x0 has any nan: 0
   0  1.42e-08  4.27e+06  9.99e+04  4.27e+06    2.17e+02      0
Traceback (most recent call last):
  File "/home/santi/git/simpeg/tutorials/14-pgi/plot_inv_2_joint_pf_pgi_no_info_tutorial.py", line 448, in <module>
    pgi_model_no_info = inv.run(m0)
                        ^^^^^^^^^^^
  File "/home/santi/git/simpeg/simpeg/utils/counter_utils.py", line 136, in wrapper
    out = f(self, *args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/santi/git/simpeg/simpeg/inversion.py", line 113, in run
    self.m = self.opt.minimize(self.invProb.evalFunction, self.invProb.model)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/santi/git/simpeg/simpeg/utils/counter_utils.py", line 136, in wrapper
    out = f(self, *args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/santi/git/simpeg/simpeg/optimization.py", line 396, in minimize
    self.doEndIteration(xt)
  File "/home/santi/git/simpeg/simpeg/utils/counter_utils.py", line 116, in wrapper
    out = f(self, *args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/santi/git/simpeg/simpeg/utils/code_utils.py", line 357, in wrapper
    return f(self, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/santi/git/simpeg/simpeg/optimization.py", line 692, in doEndIteration
    self.callback(xt)
  File "/home/santi/git/simpeg/simpeg/inversion.py", line 119, in _optCallback
    self.directiveList.call("endIter")
  File "/home/santi/git/simpeg/simpeg/directives/directives.py", line 334, in call
    getattr(r, ruleType)()
  File "/home/santi/git/simpeg/simpeg/directives/pgi_directives.py", line 116, in endIter
    clfupdate = clfupdate.fit(model)
                ^^^^^^^^^^^^^^^^^^^^
  File "/home/santi/git/simpeg/simpeg/utils/pgi_utils.py", line 1082, in fit
    self.fit_predict(X, y, debug)
  File "/home/santi/git/simpeg/simpeg/utils/pgi_utils.py", line 1173, in fit_predict
    self._print_verbose_msg_init_end(lower_bound)
TypeError: BaseMixture._print_verbose_msg_init_end() missing 1 required positional argument: 'init_has_converged'
```


### Runtime information:

 ```
  Date: Wed Jun 05 08:51:27 2024 PDT

                OS : Linux (Debian GNU/Linux 12)
            CPU(s) : 8
           Machine : x86_64
      Architecture : 64bit
               RAM : 15.5 GiB
       Environment : Python
       File system : ext4

  Python 3.12.2 | packaged by conda-forge | (main, Feb 16 2024, 20:50:58) [GCC
  12.3.0]

            simpeg : 0.21.2.dev27+gcbd200b67
        discretize : 0.10.0
       pymatsolver : 0.2.0
             numpy : 1.26.4
             scipy : 1.13.1
           sklearn : 1.5.0
        matplotlib : 3.8.4
           empymod : 2.3.0
            geoana : 0.6.0
            pandas : 2.2.2
            pydiso : 0.0.5
             numba : 0.59.1
              dask : 2024.5.2
             sympy : 1.12
           IPython : 8.25.0
        ipywidgets : 8.1.3
            plotly : 5.22.0
               vtk : 9.2.6
   memory_profiler : 0.61.0
            choclo : 0.2.0
```

### Context for the issue:

_No response_

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/simpeg/simpeg/pull/1476?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simpeg) Report
Attention: Patch coverage is `0%` with `7 lines` in your changes missing coverage. Please review.
> Project coverage is 20.57%. Comparing base [(`cbd200b`)](https://app.codecov.io/gh/simpeg/simpeg/commit/cbd200b6774b201f2e6f60cda8b35b4118d512cd?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simpeg) to head [(`6ca08ce`)](https://app.codecov.io/gh/simpeg/simpeg/commit/6ca08ce2339292e41bd240bae8a5bc32e74c34c3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simpeg).

| [Files](https://app.codecov.io/gh/simpeg/simpeg/pull/1476?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simpeg) | Patch % | Lines |
|---|---|---|
| [simpeg/utils/pgi\_utils.py](https://app.codecov.io/gh/simpeg/simpeg/pull/1476?src=pr&el=tree&filepath=simpeg%2Futils%2Fpgi_utils.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simpeg#diff-c2ltcGVnL3V0aWxzL3BnaV91dGlscy5weQ==) | 0.00% | [7 Missing :warning: ](https://app.codecov.io/gh/simpeg/simpeg/pull/1476?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simpeg) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##             main    #1476       +/-   ##
===========================================
- Coverage   82.33%   20.57%   -61.76%     
===========================================
  Files         171      171               
  Lines       26011    26017        +6     
===========================================
- Hits        21416     5353    -16063     
- Misses       4595    20664    +16069     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/simpeg/simpeg/pull/1476?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simpeg).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=simpeg).

santisoler: @thibaut-kobold, I'm merging this because this new bug is making tests to fail in other PRs and I don't want to block them. I think this is a good solution for now. But feel free to leave any comments if there's something you don't like. We can always open another PR to fix any problem this one might have.

Patch:
diff --git a/simpeg/utils/pgi_utils.py b/simpeg/utils/pgi_utils.py
index 7141fccc9..638b94cfb 100644
--- a/simpeg/utils/pgi_utils.py
+++ b/simpeg/utils/pgi_utils.py
@@ -1170,7 +1170,7 @@ class GaussianMixtureWithPrior(WeightedGaussianMixture):
                     self.converged_ = True
                     break
 
-            self._print_verbose_msg_init_end(lower_bound)
+            self._custom_print_verbose_msg_init_end(lower_bound)
 
             if lower_bound > max_lower_bound or max_lower_bound == -np.inf:
                 max_lower_bound = lower_bound
@@ -1193,6 +1193,22 @@ class GaussianMixtureWithPrior(WeightedGaussianMixture):
 
         return self
 
+    def _custom_print_verbose_msg_init_end(self, ll):
+        """
+        Wrapper for the upstream _print_verbose_msg_init_end
+
+        This method was created to provide support of older versions
+        (scikit-learn<1.5.0) of this private method.
+        """
+        try:
+            self._print_verbose_msg_init_end(ll, init_has_converged=True)
+        except TypeError as exception:
+            # In scikit-learn<1.5.0, the method has a single argument
+            match = "got an unexpected keyword argument 'init_has_converged'"
+            if match not in str(exception):
+                raise
+            self._print_verbose_msg_init_end(ll)
+
 
 class GaussianMixtureWithNonlinearRelationships(WeightedGaussianMixture):
     """Gaussian mixture class for non-linear relationships.


Test patch:
diff --git a/tests/utils/test_gmm_utils.py b/tests/utils/test_gmm_utils.py
index dbef02a7f..f6e6c93d1 100644
--- a/tests/utils/test_gmm_utils.py
+++ b/tests/utils/test_gmm_utils.py
@@ -1,3 +1,4 @@
+import pytest
 import numpy as np
 import unittest
 import discretize
@@ -277,5 +278,71 @@ class TestGMMs(unittest.TestCase):
         )
 
 
+class MockGMMLatest(GaussianMixtureWithPrior):
+    """
+    Mock of ``GaussianMixtureWithPrior`` with a ``_print_verbose_msg_init_end``
+    method with two positional arguments (scikit-learn==1.5.0).
+    """
+
+    def _print_verbose_msg_init_end(self, ll, init_has_converged):
+        """Override upstream method just for test purposes."""
+        return None
+
+
+class MockGMMOlder(GaussianMixtureWithPrior):
+    """
+    Mock of ``GaussianMixtureWithPrior`` with a ``_print_verbose_msg_init_end``
+    method with a single positional argument (scikit-learn<1.5.0).
+    """
+
+    def _print_verbose_msg_init_end(self, ll):
+        """Override upstream method just for test purposes."""
+        return None
+
+
+class TestCustomPrintMethod:
+    """
+    Test the ``GaussianMixtureWithPrior._print_verbose_msg_init_end`` method
+    with different signatures of the upstream ``_print_verbose_msg_init_end``
+    private method.
+    """
+
+    @pytest.fixture
+    def mesh(self):
+        """Sample mesh"""
+        mesh = discretize.TensorMesh([8, 7, 6])
+        return mesh
+
+    @pytest.fixture
+    def model(self, mesh):
+        """Sample model."""
+        model = np.ones(mesh.n_cells, dtype=np.float64)
+        return model
+
+    @pytest.fixture
+    def gmmref(self, mesh, model):
+        """Sample GMM"""
+        active_cells = np.ones(mesh.n_cells, dtype=bool)
+        gmmref = WeightedGaussianMixture(
+            mesh=mesh,
+            actv=active_cells,
+            n_components=1,
+            covariance_type="full",
+            max_iter=1000,
+            n_init=10,
+            tol=1e-8,
+            warm_start=True,
+        )
+        gmmref.fit(model.reshape(-1, 1))
+        return gmmref
+
+    @pytest.mark.parametrize("gmm_class", (MockGMMLatest, MockGMMOlder))
+    def test_custom_print_verbose_method(self, gmmref, gmm_class):
+        """Test custom method against older and latest signature of the upstream one."""
+        gmm = gmm_class(gmmref=gmmref)
+        # Run the custom private method: it should not raise any error
+        gmm._custom_print_verbose_msg_init_end(3)
+
+
 if __name__ == "__main__":
     unittest.main()