diff --git a/RELEASES.md b/RELEASES.md index 33be0db8e..9a5ddef50 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -11,6 +11,7 @@ - Load triton before TensorFlow in `ot.backend` so that building a torch optimizer no longer segfaults the interpreter, and remove the `torch<2.12` pin from the doctest and documentation requirements (PR #839, Issue #816) - Fix mean centering in `ot.dr.fda` and `ot.dr.wda`: `np.mean(X)` returned a scalar instead of the per-feature mean, so `proj` did not center the data as documented. In `ot.dr.fda` the same pattern in the class means made the between-class scatter matrix independent of which features separate the classes, and FDA returned a non-discriminant direction (PR #840) - `ot.dr.fda` and `ot.dr.wda` no longer modify the input array `X` in place (PR #840) +- Fix `UnbalancedSinkhornTransport` `transform` failing with `AttributeError: 'NoneType' object has no attribute 'array_equal'` when `fit` was called with missing parameters (PR #837, Issue #650) ## 0.9.7.post1 diff --git a/ot/da.py b/ot/da.py index 7b4ed7ba2..7e1a7491f 100644 --- a/ot/da.py +++ b/ot/da.py @@ -2287,6 +2287,8 @@ class label Returns self. """ + self._get_backend(Xs, ys, Xt, yt) + # check the necessary inputs parameters are here if check_params(Xs=Xs, Xt=Xt): super(UnbalancedSinkhornTransport, self).fit(Xs, ys, Xt, yt) diff --git a/test/test_da.py b/test/test_da.py index df224a247..0a89ebb79 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -483,6 +483,34 @@ def test_unbalanced_sinkhorn_transport_class(nx): assert len(otda.log_.keys()) != 0 +@pytest.skip_backend("jax") +@pytest.skip_backend("tf") +def test_unbalanced_sinkhorn_transport_nx_initialized(nx): + """non-regression test for issue #650 + + fit must always initialize the backend attribute nx, even when called + with missing parameters, so that transform fails on a clear error + instead of 'NoneType' object has no attribute 'array_equal' + """ + + ns = 50 + Xs, ys = make_data_classif("3gauss", ns) + Xt, yt = make_data_classif("3gauss2", ns) + + Xs, ys, Xt, yt = nx.from_numpy(Xs, ys, Xt, yt) + + # incomplete fit (Xt missing) still initializes the backend + otda = ot.da.UnbalancedSinkhornTransport() + otda.fit(Xs=Xs) + assert otda.nx is not None + + # complete fit followed by a separate transform call + otda = ot.da.UnbalancedSinkhornTransport() + otda.fit(Xs=Xs, Xt=Xt) + transp_Xs = otda.transform(Xs) + assert_equal(transp_Xs.shape, Xs.shape) + + @pytest.skip_backend("jax") @pytest.skip_backend("tf") def test_emd_transport_class(nx):