CoolFace
Apppublic

bbqddt2/Antigravity

sourceHugging Faceupdated 2mo agoView on Hugging Face
0likes
_ndarray.py734 linesDownload Raw Back to root
1# mypy: ignore-errors
2
3from __future__ import annotations
4
5import builtins
6import math
7import operator
8from collections.abc import Sequence
9
10import torch
11
12from . import _dtypes, _dtypes_impl, _funcs, _ufuncs, _util
13from ._normalizations import (
14    ArrayLike,
15    normalize_array_like,
16    normalizer,
17    NotImplementedType,
18)
19
20
21newaxis = None
22
23FLAGS = [
24    "C_CONTIGUOUS",
25    "F_CONTIGUOUS",
26    "OWNDATA",
27    "WRITEABLE",
28    "ALIGNED",
29    "WRITEBACKIFCOPY",
30    "FNC",
31    "FORC",
32    "BEHAVED",
33    "CARRAY",
34    "FARRAY",
35]
36
37SHORTHAND_TO_FLAGS = {
38    "C": "C_CONTIGUOUS",
39    "F": "F_CONTIGUOUS",
40    "O": "OWNDATA",
41    "W": "WRITEABLE",
42    "A": "ALIGNED",
43    "X": "WRITEBACKIFCOPY",
44    "B": "BEHAVED",
45    "CA": "CARRAY",
46    "FA": "FARRAY",
47}
48
49
50class Flags:
51    def __init__(self, flag_to_value: dict):
52        invalid_keys = [k for k in flag_to_value if k not in FLAGS]
53        if invalid_keys:
54            raise AssertionError(f"Invalid flag keys: {invalid_keys}")
55        self._flag_to_value = flag_to_value
56
57    def __getattr__(self, attr: str):
58        if attr.islower() and attr.upper() in FLAGS:
59            return self[attr.upper()]
60        else:
61            raise AttributeError(f"No flag attribute '{attr}'")
62
63    def __getitem__(self, key):
64        if key in SHORTHAND_TO_FLAGS:
65            key = SHORTHAND_TO_FLAGS[key]
66        if key in FLAGS:
67            try:
68                return self._flag_to_value[key]
69            except KeyError as e:
70                raise NotImplementedError(f"{key=}") from e
71        else:
72            raise KeyError(f"No flag key '{key}'")
73
74    def __setattr__(self, attr, value):
75        if attr.islower() and attr.upper() in FLAGS:
76            self[attr.upper()] = value
77        else:
78            super().__setattr__(attr, value)
79
80    def __setitem__(self, key, value):
81        if key in FLAGS or key in SHORTHAND_TO_FLAGS:
82            raise NotImplementedError("Modifying flags is not implemented")
83        else:
84            raise KeyError(f"No flag key '{key}'")
85
86
87def create_method(fn, name=None):
88    name = name or fn.__name__
89
90    def f(*args, **kwargs):
91        return fn(*args, **kwargs)
92
93    f.__name__ = name
94    f.__qualname__ = f"ndarray.{name}"
95    return f
96
97
98# Map ndarray.name_method -> np.name_func
99# If name_func == None, it means that name_method == name_func
100methods = {
101    "clip": None,
102    "nonzero": None,
103    "repeat": None,
104    "round": None,
105    "squeeze": None,
106    "swapaxes": None,
107    "ravel": None,
108    # linalg
109    "diagonal": None,
110    "dot": None,
111    "trace": None,
112    # sorting
113    "argsort": None,
114    "searchsorted": None,
115    # reductions
116    "argmax": None,
117    "argmin": None,
118    "any": None,
119    "all": None,
120    "max": None,
121    "min": None,
122    "ptp": None,
123    "sum": None,
124    "prod": None,
125    "mean": None,
126    "var": None,
127    "std": None,
128    # scans
129    "cumsum": None,
130    "cumprod": None,
131    # advanced indexing
132    "take": None,
133    "choose": None,
134}
135
136dunder = {
137    "abs": "absolute",
138    "invert": None,
139    "pos": "positive",
140    "neg": "negative",
141    "gt": "greater",
142    "lt": "less",
143    "ge": "greater_equal",
144    "le": "less_equal",
145}
146
147# dunder methods with right-looking and in-place variants
148ri_dunder = {
149    "add": None,
150    "sub": "subtract",
151    "mul": "multiply",
152    "truediv": "divide",
153    "floordiv": "floor_divide",
154    "pow": "power",
155    "mod": "remainder",
156    "and": "bitwise_and",
157    "or": "bitwise_or",
158    "xor": "bitwise_xor",
159    "lshift": "left_shift",
160    "rshift": "right_shift",
161    "matmul": None,
162}
163
164
165def _upcast_int_indices(index):
166    if isinstance(index, torch.Tensor):
167        if index.dtype in (torch.int8, torch.int16, torch.int32, torch.uint8):
168            return index.to(torch.int64)
169    elif isinstance(index, tuple):
170        return tuple(_upcast_int_indices(i) for i in index)
171    return index
172
173
174def _has_advanced_indexing(index):
175    """Check if there's any advanced indexing"""
176    return any(
177        isinstance(idx, (Sequence, bool))
178        or (isinstance(idx, torch.Tensor) and (idx.dtype == torch.bool or idx.ndim > 0))
179        for idx in index
180    )
181
182
183def _numpy_compatible_indexing(index):
184    """Convert scalar indices to lists when advanced indexing is present for NumPy compatibility."""
185    if not isinstance(index, tuple):
186        index = (index,)
187
188    # Check if there's any advanced indexing (sequences, booleans, or tensors)
189    has_advanced = _has_advanced_indexing(index)
190
191    if not has_advanced:
192        return index
193
194    # Convert integer scalar indices to single-element lists when advanced indexing is present
195    # Note: Do NOT convert boolean scalars (True/False) as they have special meaning in NumPy
196    converted = []
197    for idx in index:
198        if isinstance(idx, int) and not isinstance(idx, bool):
199            # Integer scalars should be converted to lists
200            converted.append([idx])
201        elif (
202            isinstance(idx, torch.Tensor)
203            and idx.ndim == 0
204            and not torch.is_floating_point(idx)
205            and idx.dtype != torch.bool
206        ):
207            # Zero-dimensional tensors holding integers should be treated the same as integer scalars
208            converted.append([idx])
209        else:
210            # Everything else (booleans, lists, slices, etc.) stays as is
211            converted.append(idx)
212
213    return tuple(converted)
214
215
216def _get_bool_depth(s):
217    """Returns the depth of a boolean sequence/tensor"""
218    if isinstance(s, bool):
219        return True, 0
220    if isinstance(s, torch.Tensor) and s.dtype == torch.bool:
221        return True, s.ndim
222    if not (isinstance(s, Sequence) and s and s[0] != s):
223        return False, 0
224    is_bool, depth = _get_bool_depth(s[0])
225    return is_bool, depth + 1
226
227
228def _numpy_empty_ellipsis_patch(index, tensor_ndim):
229    """
230    Patch for NumPy-compatible ellipsis behavior when ellipsis doesn't match any dimensions.
231
232    In NumPy, when an ellipsis (...) doesn't actually match any dimensions of the input array,
233    it still acts as a separator between advanced indices. PyTorch doesn't have this behavior.
234
235    This function detects when we have:
236    1. Advanced indexing on both sides of an ellipsis
237    2. The ellipsis doesn't actually match any dimensions
238    """
239    if not isinstance(index, tuple):
240        index = (index,)
241
242    # Find ellipsis position
243    ellipsis_pos = None
244    for i, idx in enumerate(index):
245        if idx is Ellipsis:
246            ellipsis_pos = i
247            break
248
249    # If no ellipsis, no patch needed
250    if ellipsis_pos is None:
251        return index, lambda x: x, lambda x: x
252
253    # Count non-ellipsis dimensions consumed by the index
254    consumed_dims = 0
255    for idx in index:
256        is_bool, depth = _get_bool_depth(idx)
257        if is_bool:
258            consumed_dims += depth
259        elif idx is Ellipsis or idx is None:
260            continue
261        else:
262            consumed_dims += 1
263
264    # Calculate how many dimensions the ellipsis should match
265    ellipsis_dims = tensor_ndim - consumed_dims
266
267    # Check if ellipsis doesn't match any dimensions
268    if ellipsis_dims == 0:
269        # Check if we have advanced indexing on both sides of ellipsis
270        left_advanced = _has_advanced_indexing(index[:ellipsis_pos])
271        right_advanced = _has_advanced_indexing(index[ellipsis_pos + 1 :])
272
273        if left_advanced and right_advanced:
274            # This is the case where NumPy and PyTorch differ
275            # We need to ensure the advanced indices are treated as separated
276            new_index = index[:ellipsis_pos] + (None,) + index[ellipsis_pos + 1 :]
277            end_ndims = 1 + sum(
278                1 for idx in index[ellipsis_pos + 1 :] if isinstance(idx, slice)
279            )
280
281            def squeeze_fn(x):
282                return x.squeeze(-end_ndims)
283
284            def unsqueeze_fn(x):
285                if isinstance(x, torch.Tensor) and x.ndim >= end_ndims:
286                    return x.unsqueeze(-end_ndims)
287                return x
288
289            return new_index, squeeze_fn, unsqueeze_fn
290
291    return index, lambda x: x, lambda x: x
292
293
294# Used to indicate that a parameter is unspecified (as opposed to explicitly
295# `None`)
296class _Unspecified:
297    pass
298
299
300_Unspecified.unspecified = _Unspecified()
301
302###############################################################
303#                      ndarray class                          #
304###############################################################
305
306
307class ndarray:
308    def __init__(self, t=None):
309        if t is None:
310            self.tensor = torch.Tensor()
311        elif isinstance(t, torch.Tensor):
312            self.tensor = t
313        else:
314            raise ValueError(
315                "ndarray constructor is not recommended; prefer"
316                "either array(...) or zeros/empty(...)"
317            )
318
319    # Register NumPy functions as methods
320    for method, name in methods.items():
321        fn = getattr(_funcs, name or method)
322        vars()[method] = create_method(fn, method)
323
324    # Regular methods but coming from ufuncs
325    conj = create_method(_ufuncs.conjugate, "conj")
326    conjugate = create_method(_ufuncs.conjugate)
327
328    for method, name in dunder.items():
329        fn = getattr(_ufuncs, name or method)
330        method = f"__{method}__"
331        vars()[method] = create_method(fn, method)
332
333    for method, name in ri_dunder.items():
334        fn = getattr(_ufuncs, name or method)
335        plain = f"__{method}__"
336        vars()[plain] = create_method(fn, plain)
337        rvar = f"__r{method}__"
338        vars()[rvar] = create_method(lambda self, other, fn=fn: fn(other, self), rvar)
339        ivar = f"__i{method}__"
340        vars()[ivar] = create_method(
341            lambda self, other, fn=fn: fn(self, other, out=self), ivar
342        )
343
344    # There's no __idivmod__
345    __divmod__ = create_method(_ufuncs.divmod, "__divmod__")
346    __rdivmod__ = create_method(
347        lambda self, other: _ufuncs.divmod(other, self), "__rdivmod__"
348    )
349
350    # prevent loop variables leaking into the ndarray class namespace
351    del ivar, rvar, name, plain, fn, method
352
353    @property
354    def shape(self):
355        return tuple(self.tensor.shape)
356
357    @property
358    def size(self):
359        return self.tensor.numel()
360
361    @property
362    def ndim(self):
363        return self.tensor.ndim
364
365    @property
366    def dtype(self):
367        return _dtypes.dtype(self.tensor.dtype)
368
369    @property
370    def strides(self):
371        elsize = self.tensor.element_size()
372        return tuple(stride * elsize for stride in self.tensor.stride())
373
374    @property
375    def itemsize(self):
376        return self.tensor.element_size()
377
378    @property
379    def flags(self):
380        # Note contiguous in torch is assumed C-style
381        return Flags(
382            {
383                "C_CONTIGUOUS": self.tensor.is_contiguous(),
384                "F_CONTIGUOUS": self.T.tensor.is_contiguous(),
385                "OWNDATA": self.tensor._base is None,
386                "WRITEABLE": True,  # pytorch does not have readonly tensors
387            }
388        )
389
390    @property
391    def data(self):
392        return self.tensor.data_ptr()
393
394    @property
395    def nbytes(self):
396        return self.tensor.storage().nbytes()
397
398    @property
399    def T(self):
400        return self.transpose()
401
402    @property
403    def real(self):
404        return _funcs.real(self)
405
406    @real.setter
407    def real(self, value):
408        self.tensor.real = asarray(value).tensor
409
410    @property
411    def imag(self):
412        return _funcs.imag(self)
413
414    @imag.setter
415    def imag(self, value):
416        self.tensor.imag = asarray(value).tensor
417
418    @property
419    def flat(self):
420        return self.ravel()
421
422    # ctors
423    def astype(self, dtype, order="K", casting="unsafe", subok=True, copy=True):
424        if order != "K":
425            raise NotImplementedError(f"astype(..., order={order} is not implemented.")
426        if casting != "unsafe":
427            raise NotImplementedError(
428                f"astype(..., casting={casting} is not implemented."
429            )
430        if not subok:
431            raise NotImplementedError(f"astype(..., subok={subok} is not implemented.")
432        if not copy:
433            raise NotImplementedError(f"astype(..., copy={copy} is not implemented.")
434        torch_dtype = _dtypes.dtype(dtype).torch_dtype
435        t = self.tensor.to(torch_dtype)
436        return ndarray(t)
437
438    @normalizer
439    def copy(self: ArrayLike, order: NotImplementedType = "C"):
440        return self.clone()
441
442    @normalizer
443    def flatten(self: ArrayLike, order: NotImplementedType = "C"):
444        return torch.flatten(self)
445
446    def resize(self, *new_shape, refcheck=False):
447        # NB: differs from np.resize: fills with zeros instead of making repeated copies of input.
448        if refcheck:
449            raise NotImplementedError(
450                f"resize(..., refcheck={refcheck} is not implemented."
451            )
452        if new_shape in [(), (None,)]:
453            return
454
455        # support both x.resize((2, 2)) and x.resize(2, 2)
456        if len(new_shape) == 1:
457            new_shape = new_shape[0]
458        if isinstance(new_shape, int):
459            new_shape = (new_shape,)
460
461        if builtins.any(x < 0 for x in new_shape):
462            raise ValueError("all elements of `new_shape` must be non-negative")
463
464        new_numel, old_numel = math.prod(new_shape), self.tensor.numel()
465
466        self.tensor.resize_(new_shape)
467
468        if new_numel >= old_numel:
469            # zero-fill new elements
470            if not self.tensor.is_contiguous():
471                raise AssertionError("tensor must be contiguous for resize with growth")
472            b = self.tensor.flatten()  # does not copy
473            b[old_numel:].zero_()
474
475    def view(self, dtype=_Unspecified.unspecified, type=_Unspecified.unspecified):
476        if dtype is _Unspecified.unspecified:
477            dtype = self.dtype
478        if type is not _Unspecified.unspecified:
479            raise NotImplementedError(f"view(..., type={type} is not implemented.")
480        torch_dtype = _dtypes.dtype(dtype).torch_dtype
481        tview = self.tensor.view(torch_dtype)
482        return ndarray(tview)
483
484    @normalizer
485    def fill(self, value: ArrayLike):
486        # Both Pytorch and NumPy accept 0D arrays/tensors and scalars, and
487        # error out on D > 0 arrays
488        self.tensor.fill_(value)
489
490    def tolist(self):
491        return self.tensor.tolist()
492
493    def __iter__(self):
494        return (ndarray(x) for x in self.tensor.__iter__())
495
496    def __str__(self):
497        return (
498            str(self.tensor)
499            .replace("tensor", "torch.ndarray")
500            .replace("dtype=torch.", "dtype=")
501        )
502
503    __repr__ = create_method(__str__)
504
505    def __eq__(self, other):
506        try:
507            return _ufuncs.equal(self, other)
508        except (RuntimeError, TypeError):
509            # Failed to convert other to array: definitely not equal.
510            falsy = torch.full(self.shape, fill_value=False, dtype=bool)
511            return asarray(falsy)
512
513    def __ne__(self, other):
514        return ~(self == other)
515
516    def __index__(self):
517        try:
518            return operator.index(self.tensor.item())
519        except Exception as exc:
520            raise TypeError(
521                "only integer scalar arrays can be converted to a scalar index"
522            ) from exc
523
524    def __bool__(self):
525        return bool(self.tensor)
526
527    def __int__(self):
528        return int(self.tensor)
529
530    def __float__(self):
531        return float(self.tensor)
532
533    def __complex__(self):
534        return complex(self.tensor)
535
536    def is_integer(self):
537        try:
538            v = self.tensor.item()
539            result = int(v) == v
540        except Exception:
541            result = False
542        return result
543
544    def __len__(self):
545        return self.tensor.shape[0]
546
547    def __contains__(self, x):
548        return self.tensor.__contains__(x)
549
550    def transpose(self, *axes):
551        # np.transpose(arr, axis=None) but arr.transpose(*axes)
552        return _funcs.transpose(self, axes)
553
554    def reshape(self, *shape, order="C"):
555        # arr.reshape(shape) and arr.reshape(*shape)
556        return _funcs.reshape(self, shape, order=order)
557
558    def sort(self, axis=-1, kind=None, order=None):
559        # ndarray.sort works in-place
560        _funcs.copyto(self, _funcs.sort(self, axis, kind, order))
561
562    def item(self, *args):
563        # Mimic NumPy's implementation with three special cases (no arguments,
564        # a flat index and a multi-index):
565        # https://github.com/numpy/numpy/blob/main/numpy/_core/src/multiarray/methods.c#L702
566        if args == ():
567            return self.tensor.item()
568        elif len(args) == 1:
569            # int argument
570            return self.ravel()[args[0]]
571        else:
572            return self.__getitem__(args)
573
574    def __getitem__(self, index):
575        tensor = self.tensor
576
577        def neg_step(i, s):
578            if not (isinstance(s, slice) and s.step is not None and s.step < 0):
579                return s
580
581            nonlocal tensor
582            tensor = torch.flip(tensor, (i,))
583
584            # Account for the fact that a slice includes the start but not the end
585            if not (isinstance(s.start, int) or s.start is None):
586                raise AssertionError(
587                    f"slice start must be int or None, got {type(s.start).__name__}"
588                )
589            if not (isinstance(s.stop, int) or s.stop is None):
590                raise AssertionError(
591                    f"slice stop must be int or None, got {type(s.stop).__name__}"
592                )
593            start = s.stop + 1 if s.stop else None
594            stop = s.start + 1 if s.start else None
595
596            return slice(start, stop, -s.step)
597
598        if isinstance(index, Sequence):
599            index = type(index)(neg_step(i, s) for i, s in enumerate(index))
600        else:
601            index = neg_step(0, index)
602        index = _util.ndarrays_to_tensors(index)
603        index = _upcast_int_indices(index)
604        # Apply NumPy-compatible indexing conversion
605        index = _numpy_compatible_indexing(index)
606        # Apply NumPy-compatible empty ellipsis behavior
607        index, maybe_squeeze, _ = _numpy_empty_ellipsis_patch(index, tensor.ndim)
608        return maybe_squeeze(ndarray(tensor.__getitem__(index)))
609
610    def __setitem__(self, index, value):
611        index = _util.ndarrays_to_tensors(index)
612        index = _upcast_int_indices(index)
613        # Apply NumPy-compatible indexing conversion
614        index = _numpy_compatible_indexing(index)
615        # Apply NumPy-compatible empty ellipsis behavior
616        index, _, maybe_unsqueeze = _numpy_empty_ellipsis_patch(index, self.tensor.ndim)
617
618        if not _dtypes_impl.is_scalar(value):
619            value = normalize_array_like(value)
620            value = _util.cast_if_needed(value, self.tensor.dtype)
621
622        return self.tensor.__setitem__(index, maybe_unsqueeze(value))
623
624    take = _funcs.take
625    put = _funcs.put
626
627    def __dlpack__(self, *, stream=None):
628        return self.tensor.__dlpack__(stream=stream)
629
630    def __dlpack_device__(self):
631        return self.tensor.__dlpack_device__()
632
633
634def _tolist(obj):
635    """Recursively convert tensors into lists."""
636    a1 = []
637    for elem in obj:
638        if isinstance(elem, (list, tuple)):
639            elem = _tolist(elem)
640        if isinstance(elem, ndarray):
641            a1.append(elem.tensor.tolist())
642        else:
643            a1.append(elem)
644    return a1
645
646
647# This is the ideally the only place which talks to ndarray directly.
648# The rest goes through asarray (preferred) or array.
649
650
651def array(obj, dtype=None, *, copy=True, order="K", subok=False, ndmin=0, like=None):
652    if subok is not False:
653        raise NotImplementedError("'subok' parameter is not supported.")
654    if like is not None:
655        raise NotImplementedError("'like' parameter is not supported.")
656    if order != "K":
657        raise NotImplementedError
658
659    # a happy path
660    if (
661        isinstance(obj, ndarray)
662        and copy is False
663        and dtype is None
664        and ndmin <= obj.ndim
665    ):
666        return obj
667
668    if isinstance(obj, (list, tuple)):
669        # FIXME and they have the same dtype, device, etc
670        if obj and all(isinstance(x, torch.Tensor) for x in obj):
671            # list of arrays: *under torch.Dynamo* these are FakeTensors
672            obj = torch.stack(obj)
673        else:
674            # XXX: remove tolist
675            # lists of ndarrays: [1, [2, 3], ndarray(4)] convert to lists of lists
676            obj = _tolist(obj)
677
678    # is obj an ndarray already?
679    if isinstance(obj, ndarray):
680        obj = obj.tensor
681
682    # is a specific dtype requested?
683    torch_dtype = None
684    if dtype is not None:
685        torch_dtype = _dtypes.dtype(dtype).torch_dtype
686
687    tensor = _util._coerce_to_tensor(obj, torch_dtype, copy, ndmin)
688    return ndarray(tensor)
689
690
691def asarray(a, dtype=None, order="K", *, like=None):
692    return array(a, dtype=dtype, order=order, like=like, copy=False, ndmin=0)
693
694
695def ascontiguousarray(a, dtype=None, *, like=None):
696    arr = asarray(a, dtype=dtype, like=like)
697    if not arr.tensor.is_contiguous():
698        arr.tensor = arr.tensor.contiguous()
699    return arr
700
701
702def from_dlpack(x, /):
703    t = torch.from_dlpack(x)
704    return ndarray(t)
705
706
707def _extract_dtype(entry):
708    try:
709        dty = _dtypes.dtype(entry)
710    except Exception:
711        dty = asarray(entry).dtype
712    return dty
713
714
715def can_cast(from_, to, casting="safe"):
716    from_ = _extract_dtype(from_)
717    to_ = _extract_dtype(to)
718
719    return _dtypes_impl.can_cast_impl(from_.torch_dtype, to_.torch_dtype, casting)
720
721
722def result_type(*arrays_and_dtypes):
723    tensors = []
724    for entry in arrays_and_dtypes:
725        try:
726            t = asarray(entry).tensor
727        except (RuntimeError, ValueError, TypeError):
728            dty = _dtypes.dtype(entry)
729            t = torch.empty(1, dtype=dty.torch_dtype)
730        tensors.append(t)
731
732    torch_dtype = _dtypes_impl.result_type_impl(*tensors)
733    return _dtypes.dtype(torch_dtype)
734