CoolFace
Apppublic

Aluode/PerceptionLabPortable

sourceHugging Faceupdated 9mo agoView on Hugging Face
0likes
test_array_api.py606 linesDownload Raw Back to tests
1import os
2from functools import partial
3
4import numpy
5import pytest
6from numpy.testing import assert_allclose
7
8from sklearn._config import config_context
9from sklearn.base import BaseEstimator
10from sklearn.utils._array_api import (
11    _asarray_with_order,
12    _atol_for_type,
13    _average,
14    _convert_to_numpy,
15    _count_nonzero,
16    _estimator_with_converted_arrays,
17    _fill_or_add_to_diagonal,
18    _get_namespace_device_dtype_ids,
19    _is_numpy_namespace,
20    _isin,
21    _max_precision_float_dtype,
22    _nanmax,
23    _nanmean,
24    _nanmin,
25    _ravel,
26    device,
27    get_namespace,
28    get_namespace_and_device,
29    indexing_dtype,
30    np_compat,
31    yield_namespace_device_dtype_combinations,
32)
33from sklearn.utils._testing import (
34    SkipTest,
35    _array_api_for_tests,
36    assert_array_equal,
37    skip_if_array_api_compat_not_configured,
38)
39from sklearn.utils.fixes import _IS_32BIT, CSR_CONTAINERS, np_version, parse_version
40
41
42@pytest.mark.parametrize("X", [numpy.asarray([1, 2, 3]), [1, 2, 3]])
43def test_get_namespace_ndarray_default(X):
44    """Check that get_namespace returns NumPy wrapper"""
45    xp_out, is_array_api_compliant = get_namespace(X)
46    assert xp_out is np_compat
47    assert not is_array_api_compliant
48
49
50def test_get_namespace_ndarray_creation_device():
51    """Check expected behavior with device and creation functions."""
52    X = numpy.asarray([1, 2, 3])
53    xp_out, _ = get_namespace(X)
54
55    full_array = xp_out.full(10, fill_value=2.0, device="cpu")
56    assert_allclose(full_array, [2.0] * 10)
57
58    with pytest.raises(ValueError, match="Unsupported device"):
59        xp_out.zeros(10, device="cuda")
60
61
62@skip_if_array_api_compat_not_configured
63def test_get_namespace_ndarray_with_dispatch():
64    """Test get_namespace on NumPy ndarrays."""
65
66    X_np = numpy.asarray([[1, 2, 3]])
67
68    with config_context(array_api_dispatch=True):
69        xp_out, is_array_api_compliant = get_namespace(X_np)
70        assert is_array_api_compliant
71
72        # In the future, NumPy should become API compliant library and we should have
73        # assert xp_out is numpy
74        assert xp_out is np_compat
75
76
77@skip_if_array_api_compat_not_configured
78def test_get_namespace_array_api(monkeypatch):
79    """Test get_namespace for ArrayAPI arrays."""
80    xp = pytest.importorskip("array_api_strict")
81
82    X_np = numpy.asarray([[1, 2, 3]])
83    X_xp = xp.asarray(X_np)
84    with config_context(array_api_dispatch=True):
85        xp_out, is_array_api_compliant = get_namespace(X_xp)
86        assert is_array_api_compliant
87
88        with pytest.raises(TypeError):
89            xp_out, is_array_api_compliant = get_namespace(X_xp, X_np)
90
91        def mock_getenv(key):
92            if key == "SCIPY_ARRAY_API":
93                return "0"
94
95        monkeypatch.setattr("os.environ.get", mock_getenv)
96        assert os.environ.get("SCIPY_ARRAY_API") != "1"
97        with pytest.raises(
98            RuntimeError,
99            match="scipy's own support is not enabled.",
100        ):
101            get_namespace(X_xp)
102
103
104@pytest.mark.parametrize("array_api", ["numpy", "array_api_strict"])
105def test_asarray_with_order(array_api):
106    """Test _asarray_with_order passes along order for NumPy arrays."""
107    xp = pytest.importorskip(array_api)
108
109    X = xp.asarray([1.2, 3.4, 5.1])
110    X_new = _asarray_with_order(X, order="F", xp=xp)
111
112    X_new_np = numpy.asarray(X_new)
113    assert X_new_np.flags["F_CONTIGUOUS"]
114
115
116@pytest.mark.parametrize(
117    "array_namespace, device_, dtype_name",
118    yield_namespace_device_dtype_combinations(),
119    ids=_get_namespace_device_dtype_ids,
120)
121@pytest.mark.parametrize(
122    "weights, axis, normalize, expected",
123    [
124        # normalize = True
125        (None, None, True, 3.5),
126        (None, 0, True, [2.5, 3.5, 4.5]),
127        (None, 1, True, [2, 5]),
128        ([True, False], 0, True, [1, 2, 3]),  # boolean weights
129        ([True, True, False], 1, True, [1.5, 4.5]),  # boolean weights
130        ([0.4, 0.1], 0, True, [1.6, 2.6, 3.6]),
131        ([0.4, 0.2, 0.2], 1, True, [1.75, 4.75]),
132        ([1, 2], 0, True, [3, 4, 5]),
133        ([1, 1, 2], 1, True, [2.25, 5.25]),
134        ([[1, 2, 3], [1, 2, 3]], 0, True, [2.5, 3.5, 4.5]),
135        ([[1, 2, 1], [2, 2, 2]], 1, True, [2, 5]),
136        # normalize = False
137        (None, None, False, 21),
138        (None, 0, False, [5, 7, 9]),
139        (None, 1, False, [6, 15]),
140        ([True, False], 0, False, [1, 2, 3]),  # boolean weights
141        ([True, True, False], 1, False, [3, 9]),  # boolean weights
142        ([0.4, 0.1], 0, False, [0.8, 1.3, 1.8]),
143        ([0.4, 0.2, 0.2], 1, False, [1.4, 3.8]),
144        ([1, 2], 0, False, [9, 12, 15]),
145        ([1, 1, 2], 1, False, [9, 21]),
146        ([[1, 2, 3], [1, 2, 3]], 0, False, [5, 14, 27]),
147        ([[1, 2, 1], [2, 2, 2]], 1, False, [8, 30]),
148    ],
149)
150def test_average(
151    array_namespace, device_, dtype_name, weights, axis, normalize, expected
152):
153    xp = _array_api_for_tests(array_namespace, device_)
154    array_in = numpy.asarray([[1, 2, 3], [4, 5, 6]], dtype=dtype_name)
155    array_in = xp.asarray(array_in, device=device_)
156    if weights is not None:
157        weights = numpy.asarray(weights, dtype=dtype_name)
158        weights = xp.asarray(weights, device=device_)
159
160    with config_context(array_api_dispatch=True):
161        result = _average(array_in, axis=axis, weights=weights, normalize=normalize)
162
163    if np_version < parse_version("2.0.0") or np_version >= parse_version("2.1.0"):
164        # NumPy 2.0 has a problem with the device attribute of scalar arrays:
165        # https://github.com/numpy/numpy/issues/26850
166        assert device(array_in) == device(result)
167
168    result = _convert_to_numpy(result, xp)
169    assert_allclose(result, expected, atol=_atol_for_type(dtype_name))
170
171
172@pytest.mark.parametrize(
173    "array_namespace, device, dtype_name",
174    yield_namespace_device_dtype_combinations(include_numpy_namespaces=False),
175    ids=_get_namespace_device_dtype_ids,
176)
177def test_average_raises_with_wrong_dtype(array_namespace, device, dtype_name):
178    xp = _array_api_for_tests(array_namespace, device)
179
180    array_in = numpy.asarray([2, 0], dtype=dtype_name) + 1j * numpy.asarray(
181        [4, 3], dtype=dtype_name
182    )
183    complex_type_name = array_in.dtype.name
184    if not hasattr(xp, complex_type_name):
185        # This is the case for cupy as of March 2024 for instance.
186        pytest.skip(f"{array_namespace} does not support {complex_type_name}")
187
188    array_in = xp.asarray(array_in, device=device)
189
190    err_msg = "Complex floating point values are not supported by average."
191    with (
192        config_context(array_api_dispatch=True),
193        pytest.raises(NotImplementedError, match=err_msg),
194    ):
195        _average(array_in)
196
197
198@pytest.mark.parametrize(
199    "array_namespace, device, dtype_name",
200    yield_namespace_device_dtype_combinations(include_numpy_namespaces=True),
201    ids=_get_namespace_device_dtype_ids,
202)
203@pytest.mark.parametrize(
204    "axis, weights, error, error_msg",
205    (
206        (
207            None,
208            [1, 2],
209            TypeError,
210            "Axis must be specified",
211        ),
212        (
213            0,
214            [[1, 2]],
215            # NumPy 2 raises ValueError, NumPy 1 raises TypeError
216            (ValueError, TypeError),
217            "weights",  # the message is different for NumPy 1 and 2...
218        ),
219        (
220            0,
221            [1, 2, 3, 4],
222            ValueError,
223            "weights",
224        ),
225        (0, [-1, 1], ZeroDivisionError, "Weights sum to zero, can't be normalized"),
226    ),
227)
228def test_average_raises_with_invalid_parameters(
229    array_namespace, device, dtype_name, axis, weights, error, error_msg
230):
231    xp = _array_api_for_tests(array_namespace, device)
232
233    array_in = numpy.asarray([[1, 2, 3], [4, 5, 6]], dtype=dtype_name)
234    array_in = xp.asarray(array_in, device=device)
235
236    weights = numpy.asarray(weights, dtype=dtype_name)
237    weights = xp.asarray(weights, device=device)
238
239    with config_context(array_api_dispatch=True), pytest.raises(error, match=error_msg):
240        _average(array_in, axis=axis, weights=weights)
241
242
243def test_device_none_if_no_input():
244    assert device() is None
245
246    assert device(None, "name") is None
247
248
249@skip_if_array_api_compat_not_configured
250def test_device_inspection():
251    class Device:
252        def __init__(self, name):
253            self.name = name
254
255        def __eq__(self, device):
256            return self.name == device.name
257
258        def __hash__(self):
259            raise TypeError("Device object is not hashable")
260
261        def __str__(self):
262            return self.name
263
264    class Array:
265        def __init__(self, device_name):
266            self.device = Device(device_name)
267
268    # Sanity check: ensure our Device mock class is non hashable, to
269    # accurately account for non-hashable device objects in some array
270    # libraries, because of which the `device` inspection function shouldn't
271    # make use of hash lookup tables (in particular, not use `set`)
272    with pytest.raises(TypeError):
273        hash(Array("device").device)
274
275    # If array API dispatch is disabled the device should be ignored. Erroring
276    # early for different devices would prevent the np.asarray conversion to
277    # happen. For example, `r2_score(np.ones(5), torch.ones(5))` should work
278    # fine with array API disabled.
279    assert device(Array("cpu"), Array("mygpu")) is None
280
281    # Test that ValueError is raised if on different devices and array API dispatch is
282    # enabled.
283    err_msg = "Input arrays use different devices: cpu, mygpu"
284    with config_context(array_api_dispatch=True):
285        with pytest.raises(ValueError, match=err_msg):
286            device(Array("cpu"), Array("mygpu"))
287
288        # Test expected value is returned otherwise
289        array1 = Array("device")
290        array2 = Array("device")
291
292        assert array1.device == device(array1)
293        assert array1.device == device(array1, array2)
294        assert array1.device == device(array1, array1, array2)
295
296
297# TODO: add cupy to the list of libraries once the following upstream issue
298# has been fixed:
299# https://github.com/cupy/cupy/issues/8180
300@skip_if_array_api_compat_not_configured
301@pytest.mark.parametrize("library", ["numpy", "array_api_strict", "torch"])
302@pytest.mark.parametrize(
303    "X,reduction,expected",
304    [
305        ([1, 2, numpy.nan], _nanmin, 1),
306        ([1, -2, -numpy.nan], _nanmin, -2),
307        ([numpy.inf, numpy.inf], _nanmin, numpy.inf),
308        (
309            [[1, 2, 3], [numpy.nan, numpy.nan, numpy.nan], [4, 5, 6.0]],
310            partial(_nanmin, axis=0),
311            [1.0, 2.0, 3.0],
312        ),
313        (
314            [[1, 2, 3], [numpy.nan, numpy.nan, numpy.nan], [4, 5, 6.0]],
315            partial(_nanmin, axis=1),
316            [1.0, numpy.nan, 4.0],
317        ),
318        ([1, 2, numpy.nan], _nanmax, 2),
319        ([1, 2, numpy.nan], _nanmax, 2),
320        ([-numpy.inf, -numpy.inf], _nanmax, -numpy.inf),
321        (
322            [[1, 2, 3], [numpy.nan, numpy.nan, numpy.nan], [4, 5, 6.0]],
323            partial(_nanmax, axis=0),
324            [4.0, 5.0, 6.0],
325        ),
326        (
327            [[1, 2, 3], [numpy.nan, numpy.nan, numpy.nan], [4, 5, 6.0]],
328            partial(_nanmax, axis=1),
329            [3.0, numpy.nan, 6.0],
330        ),
331        ([1, 2, numpy.nan], _nanmean, 1.5),
332        ([1, -2, -numpy.nan], _nanmean, -0.5),
333        ([-numpy.inf, -numpy.inf], _nanmean, -numpy.inf),
334        (
335            [[1, 2, 3], [numpy.nan, numpy.nan, numpy.nan], [4, 5, 6.0]],
336            partial(_nanmean, axis=0),
337            [2.5, 3.5, 4.5],
338        ),
339        (
340            [[1, 2, 3], [numpy.nan, numpy.nan, numpy.nan], [4, 5, 6.0]],
341            partial(_nanmean, axis=1),
342            [2.0, numpy.nan, 5.0],
343        ),
344    ],
345)
346def test_nan_reductions(library, X, reduction, expected):
347    """Check NaN reductions like _nanmin and _nanmax"""
348    xp = pytest.importorskip(library)
349
350    with config_context(array_api_dispatch=True):
351        result = reduction(xp.asarray(X))
352
353    result = _convert_to_numpy(result, xp)
354    assert_allclose(result, expected)
355
356
357@pytest.mark.parametrize(
358    "namespace, _device, _dtype",
359    yield_namespace_device_dtype_combinations(),
360    ids=_get_namespace_device_dtype_ids,
361)
362def test_ravel(namespace, _device, _dtype):
363    xp = _array_api_for_tests(namespace, _device)
364
365    array = [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]
366    array_xp = xp.asarray(array, device=_device)
367    with config_context(array_api_dispatch=True):
368        result = _ravel(array_xp)
369
370    result = _convert_to_numpy(result, xp)
371    expected = numpy.ravel(array, order="C")
372
373    assert_allclose(expected, result)
374
375    if _is_numpy_namespace(xp):
376        assert numpy.asarray(result).flags["C_CONTIGUOUS"]
377
378
379@skip_if_array_api_compat_not_configured
380@pytest.mark.parametrize("library", ["cupy", "torch"])
381def test_convert_to_numpy_gpu(library):  # pragma: nocover
382    """Check convert_to_numpy for GPU backed libraries."""
383    xp = pytest.importorskip(library)
384
385    if library == "torch":
386        if not xp.backends.cuda.is_built():
387            pytest.skip("test requires cuda")
388        X_gpu = xp.asarray([1.0, 2.0, 3.0], device="cuda")
389    else:
390        X_gpu = xp.asarray([1.0, 2.0, 3.0])
391
392    X_cpu = _convert_to_numpy(X_gpu, xp=xp)
393    expected_output = numpy.asarray([1.0, 2.0, 3.0])
394    assert_allclose(X_cpu, expected_output)
395
396
397def test_convert_to_numpy_cpu():
398    """Check convert_to_numpy for PyTorch CPU arrays."""
399    torch = pytest.importorskip("torch")
400    X_torch = torch.asarray([1.0, 2.0, 3.0], device="cpu")
401
402    X_cpu = _convert_to_numpy(X_torch, xp=torch)
403    expected_output = numpy.asarray([1.0, 2.0, 3.0])
404    assert_allclose(X_cpu, expected_output)
405
406
407class SimpleEstimator(BaseEstimator):
408    def fit(self, X, y=None):
409        self.X_ = X
410        self.n_features_ = X.shape[0]
411        return self
412
413
414@skip_if_array_api_compat_not_configured
415@pytest.mark.parametrize(
416    "array_namespace, converter",
417    [
418        ("torch", lambda array: array.cpu().numpy()),
419        ("array_api_strict", lambda array: numpy.asarray(array)),
420        ("cupy", lambda array: array.get()),
421    ],
422)
423def test_convert_estimator_to_ndarray(array_namespace, converter):
424    """Convert estimator attributes to ndarray."""
425    xp = pytest.importorskip(array_namespace)
426
427    X = xp.asarray([[1.3, 4.5]])
428    est = SimpleEstimator().fit(X)
429
430    new_est = _estimator_with_converted_arrays(est, converter)
431    assert isinstance(new_est.X_, numpy.ndarray)
432
433
434@skip_if_array_api_compat_not_configured
435def test_convert_estimator_to_array_api():
436    """Convert estimator attributes to ArrayAPI arrays."""
437    xp = pytest.importorskip("array_api_strict")
438
439    X_np = numpy.asarray([[1.3, 4.5]])
440    est = SimpleEstimator().fit(X_np)
441
442    new_est = _estimator_with_converted_arrays(est, lambda array: xp.asarray(array))
443    assert hasattr(new_est.X_, "__array_namespace__")
444
445
446@pytest.mark.parametrize(
447    "namespace, _device, _dtype",
448    yield_namespace_device_dtype_combinations(),
449    ids=_get_namespace_device_dtype_ids,
450)
451def test_indexing_dtype(namespace, _device, _dtype):
452    xp = _array_api_for_tests(namespace, _device)
453
454    if _IS_32BIT:
455        assert indexing_dtype(xp) == xp.int32
456    else:
457        assert indexing_dtype(xp) == xp.int64
458
459
460@pytest.mark.parametrize(
461    "namespace, _device, _dtype",
462    yield_namespace_device_dtype_combinations(),
463    ids=_get_namespace_device_dtype_ids,
464)
465def test_max_precision_float_dtype(namespace, _device, _dtype):
466    xp = _array_api_for_tests(namespace, _device)
467    expected_dtype = xp.float32 if _device == "mps" else xp.float64
468    assert _max_precision_float_dtype(xp, _device) == expected_dtype
469
470
471@pytest.mark.parametrize(
472    "array_namespace, device, _",
473    yield_namespace_device_dtype_combinations(),
474    ids=_get_namespace_device_dtype_ids,
475)
476@pytest.mark.parametrize("invert", [True, False])
477@pytest.mark.parametrize("assume_unique", [True, False])
478@pytest.mark.parametrize("element_size", [6, 10, 14])
479@pytest.mark.parametrize("int_dtype", ["int16", "int32", "int64", "uint8"])
480def test_isin(
481    array_namespace, device, _, invert, assume_unique, element_size, int_dtype
482):
483    xp = _array_api_for_tests(array_namespace, device)
484    r = element_size // 2
485    element = 2 * numpy.arange(element_size).reshape((r, 2)).astype(int_dtype)
486    test_elements = numpy.array(numpy.arange(14), dtype=int_dtype)
487    element_xp = xp.asarray(element, device=device)
488    test_elements_xp = xp.asarray(test_elements, device=device)
489    expected = numpy.isin(
490        element=element,
491        test_elements=test_elements,
492        assume_unique=assume_unique,
493        invert=invert,
494    )
495    with config_context(array_api_dispatch=True):
496        result = _isin(
497            element=element_xp,
498            test_elements=test_elements_xp,
499            xp=xp,
500            assume_unique=assume_unique,
501            invert=invert,
502        )
503
504    assert_array_equal(_convert_to_numpy(result, xp=xp), expected)
505
506
507@pytest.mark.skipif(
508    os.environ.get("SCIPY_ARRAY_API") != "1", reason="SCIPY_ARRAY_API not set to 1."
509)
510def test_get_namespace_and_device():
511    # Use torch as a library with custom Device objects:
512    torch = pytest.importorskip("torch")
513
514    from sklearn.externals.array_api_compat import torch as torch_compat
515
516    some_torch_tensor = torch.arange(3, device="cpu")
517    some_numpy_array = numpy.arange(3)
518
519    # When dispatch is disabled, get_namespace_and_device should return the
520    # default NumPy wrapper namespace and "cpu" device. Our code will handle such
521    # inputs via the usual __array__ interface without attempting to dispatch
522    # via the array API.
523    namespace, is_array_api, device = get_namespace_and_device(some_torch_tensor)
524    assert namespace is get_namespace(some_numpy_array)[0]
525    assert not is_array_api
526    assert device is None
527
528    # Otherwise, expose the torch namespace and device via array API compat
529    # wrapper.
530    with config_context(array_api_dispatch=True):
531        namespace, is_array_api, device = get_namespace_and_device(some_torch_tensor)
532        assert namespace is torch_compat
533        assert is_array_api
534        assert device == some_torch_tensor.device
535
536
537@pytest.mark.parametrize(
538    "array_namespace, device_, dtype_name",
539    yield_namespace_device_dtype_combinations(),
540    ids=_get_namespace_device_dtype_ids,
541)
542@pytest.mark.parametrize("csr_container", CSR_CONTAINERS)
543@pytest.mark.parametrize("axis", [0, 1, None, -1, -2])
544@pytest.mark.parametrize("sample_weight_type", [None, "int", "float"])
545def test_count_nonzero(
546    array_namespace, device_, dtype_name, csr_container, axis, sample_weight_type
547):
548    from sklearn.utils.sparsefuncs import count_nonzero as sparse_count_nonzero
549
550    xp = _array_api_for_tests(array_namespace, device_)
551    array = numpy.array([[0, 3, 0], [2, -1, 0], [0, 0, 0], [9, 8, 7], [4, 0, 5]])
552    if sample_weight_type == "int":
553        sample_weight = numpy.asarray([1, 2, 2, 3, 1])
554    elif sample_weight_type == "float":
555        sample_weight = numpy.asarray([0.5, 1.5, 0.8, 3.2, 2.4], dtype=dtype_name)
556    else:
557        sample_weight = None
558    expected = sparse_count_nonzero(
559        csr_container(array), axis=axis, sample_weight=sample_weight
560    )
561    array_xp = xp.asarray(array, device=device_)
562
563    with config_context(array_api_dispatch=True):
564        result = _count_nonzero(
565            array_xp, axis=axis, sample_weight=sample_weight, xp=xp, device=device_
566        )
567
568    assert_allclose(_convert_to_numpy(result, xp=xp), expected)
569
570    if np_version < parse_version("2.0.0") or np_version >= parse_version("2.1.0"):
571        # NumPy 2.0 has a problem with the device attribute of scalar arrays:
572        # https://github.com/numpy/numpy/issues/26850
573        assert device(array_xp) == device(result)
574
575
576@pytest.mark.parametrize(
577    "array_namespace, device_, dtype_name",
578    yield_namespace_device_dtype_combinations(),
579    ids=_get_namespace_device_dtype_ids,
580)
581@pytest.mark.parametrize("wrap", [True, False])
582def test_fill_or_add_to_diagonal(array_namespace, device_, dtype_name, wrap):
583    xp = _array_api_for_tests(array_namespace, device_)
584
585    array_np = numpy.zeros((5, 4), dtype=dtype_name)
586    array_xp = xp.asarray(array_np.copy(), device=device_)
587
588    numpy.fill_diagonal(array_np, val=1, wrap=wrap)
589    with config_context(array_api_dispatch=True):
590        _fill_or_add_to_diagonal(array_xp, value=1, xp=xp, add_value=False, wrap=wrap)
591
592    assert_array_equal(_convert_to_numpy(array_xp, xp=xp), array_np)
593
594
595@pytest.mark.parametrize("csr_container", CSR_CONTAINERS)
596@pytest.mark.parametrize("dispatch", [True, False])
597def test_sparse_device(csr_container, dispatch):
598    a, b = csr_container(numpy.array([[1]])), csr_container(numpy.array([[2]]))
599    if dispatch and os.environ.get("SCIPY_ARRAY_API") is None:
600        raise SkipTest("SCIPY_ARRAY_API is not set: not checking array_api input")
601    with config_context(array_api_dispatch=dispatch):
602        assert device(a, b) is None
603        assert device(a, numpy.array([1])) is None
604        assert get_namespace_and_device(a, b)[2] is None
605        assert get_namespace_and_device(a, numpy.array([1]))[2] is None
606