From 03c7f4414ecaf7b7abc532ff98c12ca7d8aaa392 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 10 Oct 2026 17:09:15 +0200 Subject: [PATCH] Fake cupyx.scipy: copy and deepcopy of proxy objects --- CHANGELOG.md | 3 +++ src/cunumpy/_fake_cupyx.py | 9 +++++++++ tests/unit/test_fake_cupyx.py | 20 ++++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bff2a62..41b0fb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 warnings as errors. Docstring style: `docs/source/contributing.md`. ### Fixed +- Splines and other objects of the fake `cupyx.scipy` can be copied with + `copy.copy` and `copy.deepcopy` (also inside a copied container), as real + CuPy splines can. - `xp.algorithms.segment_sum` and `SegmentPlan.sum` work on the fake CuPy: it cannot compile the CUDA kernel, so the sum runs on the host buffers of the fake arrays and the CuPy path of a program runs without a GPU. diff --git a/src/cunumpy/_fake_cupyx.py b/src/cunumpy/_fake_cupyx.py index 0a85e11..2ee28e6 100644 --- a/src/cunumpy/_fake_cupyx.py +++ b/src/cunumpy/_fake_cupyx.py @@ -21,6 +21,7 @@ from __future__ import annotations +import copy import importlib import sys import types @@ -263,6 +264,14 @@ def __call__(self, *args: Any, **kwargs: Any) -> Any: def __repr__(self) -> str: return f"" + # copies wrap a copy of the SciPy object and share the fake CuPy module (a + # module cannot be copied), as real CuPy splines can be copied + def __copy__(self) -> _Proxy: + return _Proxy(copy.copy(self._obj), self._cupy) + + def __deepcopy__(self, memo: dict) -> _Proxy: + return _Proxy(copy.deepcopy(self._obj, memo), self._cupy) + class _ProxyClass: """A SciPy class seen as a CuPy class: instances are :class:`_Proxy`.""" diff --git a/tests/unit/test_fake_cupyx.py b/tests/unit/test_fake_cupyx.py index f392ad2..36b1870 100644 --- a/tests/unit/test_fake_cupyx.py +++ b/tests/unit/test_fake_cupyx.py @@ -48,6 +48,26 @@ def test_splines_fit_and_evaluate_on_the_device(): assert "ok" in run(code) +def test_splines_can_be_copied(): + code = """ +import copy +import numpy as np +import cunumpy as xp +xp.set_backend("cupy") +x = xp.linspace(0.0, 1.0, 11) +spl = xp.scipy.interpolate.UnivariateSpline(x, xp.sin(3 * x), k=3, s=0.0, ext=3) +for c in (copy.copy(spl), copy.deepcopy(spl), copy.deepcopy({"spline": spl})["spline"]): + assert isinstance(c, xp.scipy.interpolate.UnivariateSpline) + assert xp.is_gpu(c(x)) + assert np.array_equal(xp.to_numpy(c(x)), xp.to_numpy(spl(x))) +# a deep copy is independent of the original +c = copy.deepcopy(spl) +assert c._obj is not spl._obj +print("ok") +""" + assert "ok" in run(code) + + def test_host_arrays_are_rejected_like_cupy(): code = """ import numpy as np