Skip to content

Commit 94dcfef

Browse files
authored
Fix dpnp.linspace returning NaN for equal infinite endpoints (#3043)
`dpnp.linspace` returned `NaN` instead of the endpoint value when `start` and `stop` are equal and infinite — e.g. `dpnp.linspace(inf, inf, 4)` gave `[nan inf inf nan]`. The bug had two independent sources: - The array path in `dpnp_linspace` computed `delta = stop - start`, so `inf - inf = NaN` propagated through the whole result. - The scalar path delegates to `dpnp.tensor.linspace`, whose affine kernel `LinearSequenceAffineFunctor` evaluates `start * w + stop * wc`; at the endpoints one weight is `0`, and `inf * 0 = NaN`. The PR proposes to fix all the places. Note, the tests compare against NumPy on versions that include the fix (NumPy >= 2.6.0) and fall back to explicit expected values on older NumPy.
1 parent 4db5bc3 commit 94dcfef

6 files changed

Lines changed: 108 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ This release is compatible with NumPy 2.5.
9494
* Fixed `astype` casting an out-of-range floating point value to a signed narrow integer type saturating to the destination min/max instead of wrapping like NumPy, generalizing the earlier unsigned-only fix [#3033](https://github.com/IntelPython/dpnp/pull/3033)
9595
* Fixed `dpnp.insert` silently ignoring out-of-bounds negative indices in a multi-element `obj`, so a mix of in-bounds and out-of-bounds indices now consistently raises `IndexError` [#3041](https://github.com/IntelPython/dpnp/pull/3041)
9696
* Fixed a per-call `sycl::queue` leak in `usm_ndarray::get_queue()`/`get_device()` [#3042](https://github.com/IntelPython/dpnp/pull/3042)
97+
* Fixed `dpnp.linspace` returning `nan` for equal infinite endpoints [#3043](https://github.com/IntelPython/dpnp/pull/3043)
9798

9899
### Security
99100

dpnp/dpnp_algo/dpnp_arraycreation.py

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -191,23 +191,37 @@ def dpnp_linspace(
191191
step_num = (num - 1) if endpoint else num
192192

193193
if dpnp.isscalar(start) and dpnp.isscalar(stop):
194-
# Call linspace() function for scalars.
195-
usm_res = dpt.linspace(
196-
start,
197-
stop,
198-
num,
199-
dtype=dt,
200-
usm_type=_usm_type,
201-
sycl_queue=sycl_queue_normalized,
202-
endpoint=endpoint,
203-
)
194+
if start == stop:
195+
# equal endpoints => constant array + zero step
196+
usm_res = dpt.full(
197+
num,
198+
start,
199+
dtype=dt,
200+
usm_type=_usm_type,
201+
sycl_queue=sycl_queue_normalized,
202+
)
204203

205-
# calculate the used step to return
206-
if retstep is True:
207-
if step_num > 0:
208-
step = (stop - start) / step_num
209-
else:
210-
step = dpnp.nan
204+
# calculate the used step to return
205+
if retstep is True:
206+
step = dt.type(0) if step_num > 0 else dpnp.nan
207+
else:
208+
# Call linspace() function for scalars.
209+
usm_res = dpt.linspace(
210+
start,
211+
stop,
212+
num,
213+
dtype=dt,
214+
usm_type=_usm_type,
215+
sycl_queue=sycl_queue_normalized,
216+
endpoint=endpoint,
217+
)
218+
219+
# calculate the used step to return
220+
if retstep is True:
221+
if step_num > 0:
222+
step = (stop - start) / step_num
223+
else:
224+
step = dpnp.nan
211225
else:
212226
usm_start = dpt.asarray(
213227
start,
@@ -219,7 +233,9 @@ def dpnp_linspace(
219233
stop, dtype=dt, usm_type=_usm_type, sycl_queue=sycl_queue_normalized
220234
)
221235

222-
delta = usm_stop - usm_start
236+
# zero the delta where endpoints coincide, else `inf - inf = NaN`
237+
# propagates (NaN != NaN untouched)
238+
delta = dpt.where((usm_stop == usm_start), 0, (usm_stop - usm_start))
223239

224240
usm_res = dpt.arange(
225241
0,

dpnp/dpnp_iface_arraycreation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2866,7 +2866,7 @@ def linspace(
28662866
There are `num` equally spaced samples in the closed interval
28672867
[`start`, `stop`] or the half-open interval [`start`, `stop`)
28682868
(depending on whether `endpoint` is ``True`` or ``False``).
2869-
step : float, optional
2869+
step : dpnp.ndarray, optional
28702870
Only returned if `retstep` is ``True``.
28712871
Size of spacing between samples.
28722872

dpnp/tensor/libtensor/include/kernels/constructors.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,13 @@ class LinearSequenceAffineFunctor
152152
void operator()(sycl::id<1> wiid) const
153153
{
154154
auto i = wiid.get(0);
155+
156+
// equal endpoints => constant, and avoids `inf * 0 = NaN`
157+
if (start_v == end_v) {
158+
p[i] = start_v;
159+
return;
160+
}
161+
155162
wTy wc = wTy(i) / n;
156163
wTy w = wTy(n - i) / n;
157164
using dpnp::tensor::type_utils::is_complex;

dpnp/tests/tensor/test_usm_ndarray_ctor.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1573,6 +1573,18 @@ def test_linspace_int():
15731573
assert np.array_equal(dpt.asnumpy(X), Xnp)
15741574

15751575

1576+
@pytest.mark.parametrize("dtype", ["f2", "f4", "f8", "c8", "c16"])
1577+
@pytest.mark.parametrize("endpoint", [True, False])
1578+
def test_linspace_inf_equal_endpoints(dtype, endpoint):
1579+
q = get_queue_or_skip()
1580+
skip_if_dtype_not_supported(dtype, q)
1581+
val = complex(np.inf, np.inf) if dpt.dtype(dtype).kind == "c" else np.inf
1582+
X = dpt.linspace(
1583+
val, val, num=5, endpoint=endpoint, dtype=dtype, sycl_queue=q
1584+
)
1585+
assert np.array_equal(dpt.asnumpy(X), np.full(5, val, dtype=dtype))
1586+
1587+
15761588
@pytest.mark.parametrize(
15771589
"dt",
15781590
_all_dtypes,

dpnp/tests/test_arraycreation.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
get_array,
2222
get_float_dtypes,
2323
has_support_aspect64,
24+
numpy_version,
2425
)
2526
from .third_party.cupy import testing
2627

@@ -264,6 +265,59 @@ def test_axis(self, axis):
264265
func = lambda xp: xp.linspace([2, 3], [20, 15], num=10, axis=axis)
265266
assert_allclose(func(dpnp), func(numpy))
266267

268+
@pytest.mark.parametrize("val", [numpy.inf, -numpy.inf, numpy.inf + 1j])
269+
@pytest.mark.parametrize("num", [1, 5])
270+
@pytest.mark.parametrize("endpoint", [True, False])
271+
def test_inf_equal_endpoints_scalar(self, val, num, endpoint):
272+
result, step = dpnp.linspace(
273+
val, val, num, endpoint=endpoint, retstep=True
274+
)
275+
if numpy_version() >= "2.6.0":
276+
expected, exp_step = numpy.linspace(
277+
val, val, num, endpoint=endpoint, retstep=True
278+
)
279+
assert_dtype_allclose(step, exp_step)
280+
else:
281+
expected = numpy.full(num, val)
282+
step_val = step.asnumpy()
283+
if (num - endpoint) > 0:
284+
assert step_val == 0
285+
else:
286+
assert numpy.isnan(step_val)
287+
assert_dtype_allclose(result, expected)
288+
289+
def test_inf_equal_endpoints_array(self):
290+
start = numpy.array([numpy.inf, -numpy.inf, 1.0])
291+
stop = numpy.array([numpy.inf, -numpy.inf, 1.0])
292+
293+
result = dpnp.linspace(start, stop, num=4)
294+
if numpy_version() >= "2.6.0":
295+
expected = numpy.linspace(start, stop, num=4)
296+
else:
297+
expected = numpy.full((4, 3), [numpy.inf, -numpy.inf, 1.0])
298+
assert_dtype_allclose(result, expected)
299+
300+
def test_inf_mixed_endpoints_array(self):
301+
start = numpy.array([numpy.inf, numpy.inf])
302+
stop = numpy.array([numpy.inf, 2.0])
303+
304+
result = dpnp.linspace(start, stop, num=3)
305+
if numpy_version() >= "2.6.0":
306+
expected = numpy.linspace(start, stop, num=3)
307+
assert_dtype_allclose(result, expected)
308+
else:
309+
# mixed infinities still yield NaN interior; equal column stays inf
310+
res = result.asnumpy()
311+
assert res[0, 0] == numpy.inf and res[-1, 0] == numpy.inf
312+
assert numpy.isnan(res[1, 1])
313+
assert res[-1, 1] == 2.0
314+
315+
@pytest.mark.parametrize("num", [1, 5])
316+
def test_nan_endpoints(self, num):
317+
result = dpnp.linspace(numpy.nan, numpy.nan, num)
318+
expected = numpy.linspace(numpy.nan, numpy.nan, num)
319+
assert_dtype_allclose(result, expected)
320+
267321
@pytest.mark.parametrize("xp", [dpnp, numpy])
268322
def test_negative_num(self, xp):
269323
with pytest.raises(ValueError, match="must be non-negative"):

0 commit comments

Comments
 (0)