"""gpuids=None must mean "every visible GPU", not "no GPU".
Every compiled entry point declares `gpuids=None` in its own signature, and
they all convert it through one helper, `convert_to_c_gpuids` in
_gpuUtils.pxd. That helper used to turn None into a VALID pointer holding
m_iCount = 0 and a NULL device list, and hand that to CUDA.
The algorithm classes hide it - they all do
`if self.gpuids is None: self.gpuids = GpuIds()` before calling down - so
tigre.Ax/Atb and every algs.* function looked fine. The kernels reachable
directly did not: `minTV(img, alpha, iters, None)` ran with zero devices and
killed the interpreter outright. No exception, no message, no traceback - the
process simply ended, which is why it reads as "TIGRE fails" rather than as a
bad argument.
NOTE ON FAILURE MODE: a regression here does not fail these tests, it ABORTS
the interpreter running them. pytest reports that as a crashed worker rather
than an assertion - loud, but not a normal failure. That is the nature of the
bug being guarded.
"""
import numpy as np
import pytest
import tigre
from tigre.utilities.Ax import Ax
from tigre.utilities.Atb import Atb
@pytest.fixture(scope="module")
def small():
geo = tigre.geometry(mode="cone", nVoxel=np.array([32, 32, 32]), default=True)
angles = np.linspace(0, 2 * np.pi, 30, endpoint=False, dtype=np.float32)
geo.check_geo(angles)
img = np.random.default_rng(0).random((32, 32, 32)).astype(np.float32)
return geo, angles, img, Ax(img, geo, angles, "Siddon")
def test_ax_atb_accept_none(small):
geo, angles, img, proj = small
assert np.isfinite(Ax(img, geo, angles, "Siddon", gpuids=None)).all()
assert np.isfinite(Atb(proj, geo, angles, backprojection_type="matched",
gpuids=None)).all()
def test_min_tv_accepts_none(small):
"""The case that killed the interpreter."""
from _minTV import minTV
geo, angles, img, proj = small
out = minTV(img, 1.0, 2, None)
assert out.shape == img.shape
assert np.isfinite(out).all()
assert np.any(out != 0), "denoiser returned an all-zero volume"
def test_awmin_tv_accepts_none(small):
from _AwminTV import AwminTV
geo, angles, img, proj = small
out = AwminTV(img, 1.0, 2, -0.005, None)
assert out.shape == img.shape
assert np.isfinite(out).all()
assert np.any(out != 0)
def test_tv_proximal_and_poisson_accept_none(small):
"""_tvdenoising declares the same default but is not among the built
extensions; _tv_proximal is the one that ships."""
from _tv_proximal import tvdenoise
from _RandomNumberGenerator import add_poisson
geo, angles, img, proj = small
out = tvdenoise(img, 2, 1.0, gpuids=None)
assert np.isfinite(out).all()
noisy = add_poisson(np.abs(proj) + 1.0, gpuids=None)
assert np.isfinite(noisy).all()
def test_none_resolves_to_all_visible_gpus():
"""The default is 'all of them', matching what the algorithm classes do
by hand, not 'the first one' and not 'none'."""
from tigre.utilities.gpu import GpuIds
assert len(GpuIds()) >= 1