Backend.arange(stop, start=0, step=1, type_as=None) accepts a type_as argument in every backend, but it is silently ignored (partially or fully) for dtype/device, unlike Backend.ones/zeros/full, which do respect it correctly. This is a footgun: code that reasonably assumes arange follows the same type_as convention as the rest of the backend API (as I did) will silently get the wrong dtype and/or device, with no error or warning.
Repro
import numpy as np
from ot.backend import NumpyBackend
nx = NumpyBackend()
a = np.array([0.1, 0.2, 0.3], dtype=np.float64)
x = nx.arange(3, type_as=a)
print(x.dtype) # int64 -- expected float64, matching `a`
Compare with nx.ones(3, type_as=a).dtype, which correctly gives float64.
Current behavior per backend (ot/backend.py)
NumpyBackend.arange (line 1294): return np.arange(start, stop, step) — type_as unused, dtype always inferred by numpy (int for integer bounds).
JaxBackend.arange (line 1732): return jnp.arange(start, stop, step) — type_as unused.
TorchBackend.arange (line 2236): sets device=type_as.device but not dtype=type_as.dtype — device is respected, dtype is not.
CupyBackend.arange (line 2789): return cp.arange(start, stop, step) — type_as unused.
TensorflowBackend.arange (line 3242): return tnp.arange(start, stop, step) — type_as unused.
None of them pass dtype=type_as.dtype (or device=type_as.device for cupy/tf), unlike e.g. NumpyBackend.ones:
def ones(self, shape, type_as=None):
if type_as is None:
return np.ones(shape)
else:
return np.ones(shape, dtype=type_as.dtype)
Where I hit this
While prototyping a change to ot/lp/_grid.py (in the still-open #863), I built x = nx.arange(n, type_as=A) to get float grid positions matching histogram A's dtype, then passed it into ot.lp.emd_1d, which derives its output coupling's dtype from the positions array (x_a) rather than the weights. Since x silently stayed int64, the resulting sparse transport plan was silently truncated to all zeros — no exception, no warning, just wrong numbers. I've since worked around it locally by building the position array via nx.cumsum(nx.ones(n, type_as=A)) - 1.0 instead, since ones does respect type_as.
Suggested fix
Make arange respect type_as consistently with ones/zeros/full, e.g. for NumpyBackend:
def arange(self, stop, start=0, step=1, type_as=None):
if type_as is None:
return np.arange(start, stop, step)
else:
return np.arange(start, stop, step, dtype=type_as.dtype)
and similarly for the other backends (adding device=type_as.device where applicable, and completing TorchBackend.arange's existing partial handling with dtype=type_as.dtype).
Backend.arange(stop, start=0, step=1, type_as=None)accepts atype_asargument in every backend, but it is silently ignored (partially or fully) for dtype/device, unlikeBackend.ones/zeros/full, which do respect it correctly. This is a footgun: code that reasonably assumesarangefollows the sametype_asconvention as the rest of the backend API (as I did) will silently get the wrong dtype and/or device, with no error or warning.Repro
Compare with
nx.ones(3, type_as=a).dtype, which correctly givesfloat64.Current behavior per backend (
ot/backend.py)NumpyBackend.arange(line 1294):return np.arange(start, stop, step)—type_asunused, dtype always inferred by numpy (int for integer bounds).JaxBackend.arange(line 1732):return jnp.arange(start, stop, step)—type_asunused.TorchBackend.arange(line 2236): setsdevice=type_as.devicebut notdtype=type_as.dtype— device is respected, dtype is not.CupyBackend.arange(line 2789):return cp.arange(start, stop, step)—type_asunused.TensorflowBackend.arange(line 3242):return tnp.arange(start, stop, step)—type_asunused.None of them pass
dtype=type_as.dtype(ordevice=type_as.devicefor cupy/tf), unlike e.g.NumpyBackend.ones:Where I hit this
While prototyping a change to
ot/lp/_grid.py(in the still-open #863), I builtx = nx.arange(n, type_as=A)to get float grid positions matching histogramA's dtype, then passed it intoot.lp.emd_1d, which derives its output coupling's dtype from the positions array (x_a) rather than the weights. Sincexsilently stayedint64, the resulting sparse transport plan was silently truncated to all zeros — no exception, no warning, just wrong numbers. I've since worked around it locally by building the position array vianx.cumsum(nx.ones(n, type_as=A)) - 1.0instead, sinceonesdoes respecttype_as.Suggested fix
Make
arangerespecttype_asconsistently withones/zeros/full, e.g. forNumpyBackend:and similarly for the other backends (adding
device=type_as.devicewhere applicable, and completingTorchBackend.arange's existing partial handling withdtype=type_as.dtype).