galley-jl-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- galley_jl_python/__init__.py +361 -0
- galley_jl_python/_array_api_info.py +94 -0
- galley_jl_python/_sysimage.py +222 -0
- galley_jl_python/compiled.py +171 -0
- galley_jl_python/dtypes.py +61 -0
- galley_jl_python/einstein.py +561 -0
- galley_jl_python/errors.py +2 -0
- galley_jl_python/finch_fused.py +33 -0
- galley_jl_python/io.py +15 -0
- galley_jl_python/julia.py +37 -0
- galley_jl_python/juliapkg.json +386 -0
- galley_jl_python/juliapkg_dev.json +22 -0
- galley_jl_python/levels.py +110 -0
- galley_jl_python/linalg/__init__.py +3 -0
- galley_jl_python/linalg/_linalg.py +26 -0
- galley_jl_python/tensor.py +1739 -0
- galley_jl_python/typing.py +17 -0
- galley_jl_python-0.1.0.dist-info/METADATA +214 -0
- galley_jl_python-0.1.0.dist-info/RECORD +21 -0
- galley_jl_python-0.1.0.dist-info/WHEEL +4 -0
- galley_jl_python-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1739 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import builtins
|
|
4
|
+
import warnings
|
|
5
|
+
from collections.abc import Callable, Iterable
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from numpy.core.numeric import normalize_axis_index, normalize_axis_tuple
|
|
10
|
+
|
|
11
|
+
from . import dtypes as jl_dtypes
|
|
12
|
+
from .compiled import compiled, compute, lazy
|
|
13
|
+
from .einstein import einop_impl, einsum_impl
|
|
14
|
+
from .errors import PerformanceWarning
|
|
15
|
+
from .julia import jc, jl
|
|
16
|
+
from .levels import (
|
|
17
|
+
Dense,
|
|
18
|
+
DenseStorage,
|
|
19
|
+
Element,
|
|
20
|
+
SparseCOO,
|
|
21
|
+
SparseList,
|
|
22
|
+
Storage,
|
|
23
|
+
_Display,
|
|
24
|
+
sparse_formats_names,
|
|
25
|
+
)
|
|
26
|
+
from .typing import Device, DType, JuliaObj, OrderType, TupleOf3Arrays, spmatrix
|
|
27
|
+
|
|
28
|
+
# Builds a 0-d Finch tensor in Julia so the element keeps its Julia eltype.
|
|
29
|
+
_scalar_to_0d = jl.seval(
|
|
30
|
+
"(T, fv, x) -> Finch.Tensor(Finch.Element{convert(T, fv)}(T[convert(T, x)]))"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SparseArray:
|
|
35
|
+
"""
|
|
36
|
+
PyData/Sparse marker class
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Tensor(_Display, SparseArray):
|
|
41
|
+
"""
|
|
42
|
+
A wrapper class for Finch.Tensor and Finch.SwizzleArray.
|
|
43
|
+
|
|
44
|
+
Constructors
|
|
45
|
+
------------
|
|
46
|
+
Tensor(scipy.sparse.spmatrix)
|
|
47
|
+
Construct a Tensor out of a `scipy.sparse` object. Supported formats are: `COO`,
|
|
48
|
+
`CSC`, and `CSR`.
|
|
49
|
+
Tensor(numpy.ndarray)
|
|
50
|
+
Construct a Tensor out of a NumPy array object. This is a no-copy operation.
|
|
51
|
+
Tensor(Storage)
|
|
52
|
+
Initialize a Tensor with a `storage` description. `storage` can already hold
|
|
53
|
+
data.
|
|
54
|
+
Tensor(julia_object)
|
|
55
|
+
Tensor created from a compatible raw Julia object. Must be a `SwizzleArray` or
|
|
56
|
+
`LazyTensor`.
|
|
57
|
+
This is a no-copy operation.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
obj : np.ndarray or scipy.sparse or Storage or Finch.SwizzleArray
|
|
62
|
+
Input to construct a Tensor. It's a no-copy operation of for NumPy and
|
|
63
|
+
SciPy input. For Storage it's levels' description with order. The order
|
|
64
|
+
numbers the dimensions from the fastest to slowest. The leaf nodes have
|
|
65
|
+
mode `0` and the root node has mode `n-1`. If the tensor was square of
|
|
66
|
+
size `N`, then `N .^ order == strides`. Available options are "C"
|
|
67
|
+
(row-major), "F" (column-major), or a custom order. Default: row-major.
|
|
68
|
+
fill_value : np.number, optional
|
|
69
|
+
Only used when `numpy.ndarray` or `scipy.sparse` is passed.
|
|
70
|
+
copy : bool, optional
|
|
71
|
+
If ``True``, then the object is copied. If ``None`` then the object is
|
|
72
|
+
copied only if needed. For ``False`` it raises a ``ValueError`` if a
|
|
73
|
+
copy cannot be avoided. Default: ``None``.
|
|
74
|
+
|
|
75
|
+
Returns
|
|
76
|
+
-------
|
|
77
|
+
Tensor
|
|
78
|
+
Python wrapper for Finch Tensor.
|
|
79
|
+
|
|
80
|
+
Examples
|
|
81
|
+
--------
|
|
82
|
+
>>> import numpy as np
|
|
83
|
+
>>> import finch
|
|
84
|
+
>>> arr2d = np.arange(6).reshape((2, 3))
|
|
85
|
+
>>> t1 = finch.Tensor(arr2d)
|
|
86
|
+
>>> t1.todense()
|
|
87
|
+
array([[0, 1, 2],
|
|
88
|
+
[3, 4, 5]])
|
|
89
|
+
>>> np.shares_memory(t1.todense(), arr2d)
|
|
90
|
+
True
|
|
91
|
+
>>> storage = finch.Storage(
|
|
92
|
+
... finch.Dense(finch.SparseList(finch.Element(1))), order="C"
|
|
93
|
+
... )
|
|
94
|
+
>>> t2 = t1.to_storage(storage)
|
|
95
|
+
>>> t2.todense()
|
|
96
|
+
array([[0, 1, 2],
|
|
97
|
+
[3, 4, 5]])
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
row_major: str = "C"
|
|
101
|
+
column_major: str = "F"
|
|
102
|
+
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
obj: np.ndarray | spmatrix | Storage | JuliaObj,
|
|
106
|
+
/,
|
|
107
|
+
*,
|
|
108
|
+
fill_value: np.number | None = None,
|
|
109
|
+
copy: bool | None = None,
|
|
110
|
+
):
|
|
111
|
+
if isinstance(obj, int | float | complex | bool | list):
|
|
112
|
+
if copy is False:
|
|
113
|
+
raise ValueError(
|
|
114
|
+
"copy=False isn't supported for scalar inputs and Python lists"
|
|
115
|
+
)
|
|
116
|
+
obj = np.asarray(obj)
|
|
117
|
+
if fill_value is None:
|
|
118
|
+
fill_value = 0.0
|
|
119
|
+
|
|
120
|
+
if _is_scipy_sparse_obj(obj): # scipy constructor
|
|
121
|
+
jl_data = self._from_scipy_sparse(obj, fill_value=fill_value, copy=copy)
|
|
122
|
+
self._obj = jl_data
|
|
123
|
+
elif isinstance(obj, np.ndarray): # numpy constructor
|
|
124
|
+
jl_data = self._from_numpy(obj, fill_value=fill_value, copy=copy)
|
|
125
|
+
self._obj = jl_data
|
|
126
|
+
elif isinstance(obj, Storage): # from-storage constructor
|
|
127
|
+
if copy:
|
|
128
|
+
self._raise_julia_copy_not_supported()
|
|
129
|
+
order = self.preprocess_order(
|
|
130
|
+
obj.order, self.get_lvl_ndim(obj.levels_descr._obj)
|
|
131
|
+
)
|
|
132
|
+
self._obj = jl.swizzle(jl.Tensor(obj.levels_descr._obj), *order)
|
|
133
|
+
elif jl.isa(obj, jl.Finch.Tensor): # raw-Julia-object constructors
|
|
134
|
+
if copy:
|
|
135
|
+
self._raise_julia_copy_not_supported()
|
|
136
|
+
self._obj = jl.swizzle(obj, *tuple(range(1, jl.ndims(obj) + 1)))
|
|
137
|
+
elif jl.isa(obj, jl.Finch.SwizzleArray) or jl.isa(obj, jl.Finch.LazyTensor):
|
|
138
|
+
if copy:
|
|
139
|
+
self._raise_julia_copy_not_supported()
|
|
140
|
+
self._obj = obj
|
|
141
|
+
elif isinstance(obj, Tensor):
|
|
142
|
+
self._obj = obj._obj
|
|
143
|
+
else:
|
|
144
|
+
raise ValueError(
|
|
145
|
+
"Either scalar, numpy, scipy.sparse or a raw julia object should "
|
|
146
|
+
f"be provided. Found: {type(obj)}"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def __pos__(self):
|
|
150
|
+
return self._elemwise_op("+")
|
|
151
|
+
|
|
152
|
+
def __neg__(self):
|
|
153
|
+
return self._elemwise_op("-")
|
|
154
|
+
|
|
155
|
+
def __add__(self, other):
|
|
156
|
+
return self._elemwise_op("+", other)
|
|
157
|
+
|
|
158
|
+
def __mul__(self, other):
|
|
159
|
+
return self._elemwise_op("*", other)
|
|
160
|
+
|
|
161
|
+
def __sub__(self, other):
|
|
162
|
+
return self._elemwise_op("-", other)
|
|
163
|
+
|
|
164
|
+
def __truediv__(self, other):
|
|
165
|
+
return self._elemwise_op("/", other)
|
|
166
|
+
|
|
167
|
+
def __floordiv__(self, other):
|
|
168
|
+
return self._elemwise_op("Finch.fld_nothrow", other)
|
|
169
|
+
|
|
170
|
+
def __mod__(self, other):
|
|
171
|
+
return self._elemwise_op("Finch.mod_nothrow", other)
|
|
172
|
+
|
|
173
|
+
def __pow__(self, other):
|
|
174
|
+
return self._elemwise_op("^", other)
|
|
175
|
+
|
|
176
|
+
@compiled()
|
|
177
|
+
def __matmul__(self, other: Tensor) -> Tensor:
|
|
178
|
+
if self.ndim == 0 or other.ndim == 0:
|
|
179
|
+
raise ValueError(
|
|
180
|
+
f"`{self.ndim=}`, `{other.ndim=}`. Both must be greater than `0`."
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if other.ndim == 1:
|
|
184
|
+
return sum(self * other, axis=-1)
|
|
185
|
+
|
|
186
|
+
if self.ndim == 1:
|
|
187
|
+
return sum(self * other.mT, axis=-1)
|
|
188
|
+
|
|
189
|
+
return sum(self[..., :, None, :] * other.mT[..., None, :, :], axis=-1)
|
|
190
|
+
|
|
191
|
+
def __abs__(self):
|
|
192
|
+
return self._elemwise_op("abs")
|
|
193
|
+
|
|
194
|
+
def __invert__(self):
|
|
195
|
+
return self._elemwise_op("~")
|
|
196
|
+
|
|
197
|
+
def __and__(self, other):
|
|
198
|
+
return self._elemwise_op("&", other)
|
|
199
|
+
|
|
200
|
+
def __or__(self, other):
|
|
201
|
+
return self._elemwise_op("|", other)
|
|
202
|
+
|
|
203
|
+
def __xor__(self, other):
|
|
204
|
+
return self._elemwise_op("xor", other)
|
|
205
|
+
|
|
206
|
+
def __lshift__(self, other):
|
|
207
|
+
return self._elemwise_op("<<", other)
|
|
208
|
+
|
|
209
|
+
def __rshift__(self, other):
|
|
210
|
+
return self._elemwise_op(">>", other)
|
|
211
|
+
|
|
212
|
+
def __lt__(self, other):
|
|
213
|
+
return self._elemwise_op("<", other)
|
|
214
|
+
|
|
215
|
+
def __le__(self, other):
|
|
216
|
+
return self._elemwise_op("<=", other)
|
|
217
|
+
|
|
218
|
+
def __gt__(self, other):
|
|
219
|
+
return self._elemwise_op(">", other)
|
|
220
|
+
|
|
221
|
+
def __ge__(self, other):
|
|
222
|
+
return self._elemwise_op(">=", other)
|
|
223
|
+
|
|
224
|
+
def __eq__(self, other):
|
|
225
|
+
return self._elemwise_op("==", other)
|
|
226
|
+
|
|
227
|
+
def __ne__(self, other):
|
|
228
|
+
return self._elemwise_op("!=", other)
|
|
229
|
+
|
|
230
|
+
def _elemwise_op(self, op: str, other: Tensor | None = None) -> Tensor:
|
|
231
|
+
if other is None:
|
|
232
|
+
result = jl.broadcast(jl.seval(op), self._obj)
|
|
233
|
+
else:
|
|
234
|
+
if np.isscalar(other):
|
|
235
|
+
other = jc.convert(self.dtype, other)
|
|
236
|
+
else:
|
|
237
|
+
other = jl.permutedims(other._obj, tuple(range(other.ndim, 0, -1)))
|
|
238
|
+
# inverse swizzle, so `broadcast` appends new dims to the front
|
|
239
|
+
result = jl.broadcast(
|
|
240
|
+
jl.seval(op),
|
|
241
|
+
jl.permutedims(self._obj, tuple(range(self.ndim, 0, -1))),
|
|
242
|
+
other,
|
|
243
|
+
)
|
|
244
|
+
# swizzle back to the original order
|
|
245
|
+
result = jl.permutedims(result, tuple(range(jl.ndims(result), 0, -1)))
|
|
246
|
+
|
|
247
|
+
return Tensor(result)
|
|
248
|
+
|
|
249
|
+
def __bool__(self):
|
|
250
|
+
return self._to_scalar(bool)
|
|
251
|
+
|
|
252
|
+
def __float__(self):
|
|
253
|
+
return self._to_scalar(float)
|
|
254
|
+
|
|
255
|
+
def __int__(self):
|
|
256
|
+
return self._to_scalar(int)
|
|
257
|
+
|
|
258
|
+
def __index__(self):
|
|
259
|
+
return self._to_scalar(int)
|
|
260
|
+
|
|
261
|
+
def __complex__(self):
|
|
262
|
+
return self._to_scalar(complex)
|
|
263
|
+
|
|
264
|
+
def _to_scalar(self, builtin):
|
|
265
|
+
if self.ndim != 0:
|
|
266
|
+
raise ValueError(f"{builtin} can be computed for one-element tensors only.")
|
|
267
|
+
return builtin(self.todense().flatten()[0])
|
|
268
|
+
|
|
269
|
+
def __getitem__(self, key):
|
|
270
|
+
if not isinstance(key, tuple):
|
|
271
|
+
key = (key,)
|
|
272
|
+
|
|
273
|
+
if not self.is_computed():
|
|
274
|
+
# lazy indexing mode
|
|
275
|
+
key = _process_lazy_indexing(key, self.ndim)
|
|
276
|
+
else:
|
|
277
|
+
# standard indexing mode
|
|
278
|
+
key = _expand_ellipsis(key, self.shape)
|
|
279
|
+
key = _add_missing_dims(key, self.shape)
|
|
280
|
+
key = _add_plus_one(key, self.shape)
|
|
281
|
+
|
|
282
|
+
result = self._obj[key]
|
|
283
|
+
if jl.isa(result, jl.Finch.SwizzleArray) or jl.isa(result, jl.Finch.LazyTensor):
|
|
284
|
+
return Tensor(result)
|
|
285
|
+
if jl.isa(result, jl.Finch.Tensor):
|
|
286
|
+
return Tensor(jl.swizzle(result, *range(1, jl.ndims(result) + 1)))
|
|
287
|
+
# the Array API requires a 0-d array, not a scalar, for integer-only keys
|
|
288
|
+
return Tensor(jl.swizzle(_scalar_to_0d(self.dtype, self.fill_value, result)))
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def dtype(self) -> DType:
|
|
292
|
+
return jl.eltype(self._obj.body)
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def ndim(self) -> int:
|
|
296
|
+
return jl.ndims(self._obj)
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def shape(self) -> tuple[int, ...]:
|
|
300
|
+
return jl.size(self._obj)
|
|
301
|
+
|
|
302
|
+
@property
|
|
303
|
+
def size(self) -> int:
|
|
304
|
+
return np.prod(self.shape)
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def fill_value(self) -> np.number:
|
|
308
|
+
return jl.fill_value(self._obj)
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def _is_dense(self) -> bool:
|
|
312
|
+
lvl = self._obj.body.lvl
|
|
313
|
+
for _ in self.shape:
|
|
314
|
+
if not jl.isa(lvl, jl.Finch.Dense):
|
|
315
|
+
return False
|
|
316
|
+
lvl = lvl.lvl
|
|
317
|
+
return True
|
|
318
|
+
|
|
319
|
+
@property
|
|
320
|
+
def _order(self) -> tuple[int, ...]:
|
|
321
|
+
return jl.typeof(self._obj).parameters[1]
|
|
322
|
+
|
|
323
|
+
@property
|
|
324
|
+
def mT(self) -> Tensor:
|
|
325
|
+
axes = list(range(self.ndim))
|
|
326
|
+
axes[-2], axes[-1] = axes[-1], axes[-2]
|
|
327
|
+
axes = tuple(axes)
|
|
328
|
+
return self.permute_dims(axes)
|
|
329
|
+
|
|
330
|
+
@property
|
|
331
|
+
def device(self) -> str:
|
|
332
|
+
return "cpu"
|
|
333
|
+
|
|
334
|
+
def to_device(
|
|
335
|
+
self, device: Device, /, *, stream: int | Any | None = None
|
|
336
|
+
) -> Tensor:
|
|
337
|
+
if device != "cpu":
|
|
338
|
+
raise ValueError("Only `device='cpu'` is supported.")
|
|
339
|
+
|
|
340
|
+
return self
|
|
341
|
+
|
|
342
|
+
def is_computed(self) -> bool:
|
|
343
|
+
return not jl.isa(self._obj, jl.Finch.LazyTensor)
|
|
344
|
+
|
|
345
|
+
@classmethod
|
|
346
|
+
def preprocess_order(cls, order: OrderType, ndim: int) -> tuple[int, ...]:
|
|
347
|
+
if order == cls.column_major:
|
|
348
|
+
permutation = tuple(range(1, ndim + 1))
|
|
349
|
+
elif order == cls.row_major or order is None:
|
|
350
|
+
permutation = tuple(range(1, ndim + 1)[::-1])
|
|
351
|
+
elif isinstance(order, tuple):
|
|
352
|
+
if builtins.min(order) == 0:
|
|
353
|
+
order = tuple(i + 1 for i in order)
|
|
354
|
+
if len(order) == ndim and builtins.all(
|
|
355
|
+
i in order for i in range(1, ndim + 1)
|
|
356
|
+
):
|
|
357
|
+
permutation = order
|
|
358
|
+
else:
|
|
359
|
+
raise ValueError(f"Custom order is not a permutation: {order}.")
|
|
360
|
+
else:
|
|
361
|
+
raise ValueError(
|
|
362
|
+
f"order must be 'C', 'F' or a tuple, but is: {type(order)}."
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
return permutation
|
|
366
|
+
|
|
367
|
+
@classmethod
|
|
368
|
+
def get_lvl_ndim(cls, lvl: JuliaObj) -> int:
|
|
369
|
+
ndim = 0
|
|
370
|
+
while True:
|
|
371
|
+
ndim += 1
|
|
372
|
+
lvl = lvl.lvl
|
|
373
|
+
if jl.isa(lvl, jl.Finch.Element):
|
|
374
|
+
break
|
|
375
|
+
return ndim
|
|
376
|
+
|
|
377
|
+
def get_order(self, zero_indexing: bool = True) -> tuple[int, ...]:
|
|
378
|
+
order = self._order
|
|
379
|
+
if zero_indexing:
|
|
380
|
+
order = tuple(i - 1 for i in order)
|
|
381
|
+
return order
|
|
382
|
+
|
|
383
|
+
def get_inv_order(self, zero_indexing: bool = True) -> tuple[int, ...]:
|
|
384
|
+
inv_order = jl.invperm(self._order)
|
|
385
|
+
if zero_indexing:
|
|
386
|
+
inv_order = tuple(i - 1 for i in inv_order)
|
|
387
|
+
return inv_order
|
|
388
|
+
|
|
389
|
+
def todense(self) -> np.ndarray:
|
|
390
|
+
obj = self._obj
|
|
391
|
+
|
|
392
|
+
if self._is_dense:
|
|
393
|
+
# don't materialize a dense finch tensor
|
|
394
|
+
shape = jl.size(obj.body)
|
|
395
|
+
dense_tensor = obj.body.lvl
|
|
396
|
+
else:
|
|
397
|
+
# create materialized dense array
|
|
398
|
+
shape = jl.size(obj)
|
|
399
|
+
dense_lvls = jl.Element(jc.convert(self.dtype, jl.fill_value(obj)))
|
|
400
|
+
for _ in range(self.ndim):
|
|
401
|
+
dense_lvls = jl.Dense(dense_lvls)
|
|
402
|
+
dense_tensor = jl.Tensor(dense_lvls, obj).lvl # materialize
|
|
403
|
+
|
|
404
|
+
for _ in range(self.ndim):
|
|
405
|
+
dense_tensor = dense_tensor.lvl
|
|
406
|
+
|
|
407
|
+
result = np.asarray(jl.reshape(dense_tensor.val, shape))
|
|
408
|
+
return result.transpose(self.get_order()) if self._is_dense else result
|
|
409
|
+
|
|
410
|
+
def permute_dims(self, axes: tuple[int, ...]) -> Tensor:
|
|
411
|
+
axes = tuple(i + 1 for i in axes)
|
|
412
|
+
new_obj = jl.permutedims(self._obj, axes)
|
|
413
|
+
return Tensor(new_obj)
|
|
414
|
+
|
|
415
|
+
def to_storage(self, storage: Storage) -> Tensor:
|
|
416
|
+
return Tensor(self._from_other_tensor(self, storage=storage))
|
|
417
|
+
|
|
418
|
+
@classmethod
|
|
419
|
+
def _from_other_tensor(cls, tensor: Tensor, storage: Storage) -> JuliaObj:
|
|
420
|
+
order = cls.preprocess_order(storage.order, tensor.ndim)
|
|
421
|
+
result = jl.copyto_b(
|
|
422
|
+
jl.swizzle(jl.Tensor(storage.levels_descr._obj), *order), tensor._obj
|
|
423
|
+
)
|
|
424
|
+
return jl.dropfills(result) if tensor._is_dense else result
|
|
425
|
+
|
|
426
|
+
@classmethod
|
|
427
|
+
def _from_numpy(
|
|
428
|
+
cls, arr: np.ndarray, fill_value: np.number, copy: bool | None = None
|
|
429
|
+
) -> JuliaObj:
|
|
430
|
+
if copy:
|
|
431
|
+
arr = arr.copy()
|
|
432
|
+
order_char = "F" if np.isfortran(arr) else "C"
|
|
433
|
+
order = cls.preprocess_order(order_char, arr.ndim)
|
|
434
|
+
inv_order = tuple(i - 1 for i in jl.invperm(order))
|
|
435
|
+
|
|
436
|
+
dtype = arr.dtype.type
|
|
437
|
+
if (
|
|
438
|
+
dtype == np.bool_
|
|
439
|
+
): # Fails with: Finch currently only supports isbits defaults
|
|
440
|
+
dtype = jl_dtypes.bool
|
|
441
|
+
fill_value = dtype(fill_value)
|
|
442
|
+
lvl = Element(fill_value, arr.reshape(-1, order=order_char))
|
|
443
|
+
for i in inv_order:
|
|
444
|
+
lvl = Dense(lvl, arr.shape[i])
|
|
445
|
+
return jl.swizzle(jl.Tensor(lvl._obj), *order)
|
|
446
|
+
|
|
447
|
+
@classmethod
|
|
448
|
+
def from_scipy_sparse(
|
|
449
|
+
cls,
|
|
450
|
+
x,
|
|
451
|
+
fill_value: np.number | None = None,
|
|
452
|
+
copy: bool | None = None,
|
|
453
|
+
) -> Tensor:
|
|
454
|
+
if not _is_scipy_sparse_obj(x):
|
|
455
|
+
raise ValueError("{x} is not a SciPy sparse object.")
|
|
456
|
+
return Tensor(x, fill_value=fill_value, copy=copy)
|
|
457
|
+
|
|
458
|
+
@classmethod
|
|
459
|
+
def _from_scipy_sparse(
|
|
460
|
+
cls,
|
|
461
|
+
x,
|
|
462
|
+
*,
|
|
463
|
+
fill_value: np.number | None = None,
|
|
464
|
+
copy: bool | None = None,
|
|
465
|
+
) -> JuliaObj:
|
|
466
|
+
if copy is False and not (
|
|
467
|
+
x.format in ("coo", "csr", "csc") and x.has_canonical_format
|
|
468
|
+
):
|
|
469
|
+
raise ValueError(
|
|
470
|
+
"Unable to avoid copy while creating an array as requested."
|
|
471
|
+
)
|
|
472
|
+
if x.format not in ("coo", "csr", "csc"):
|
|
473
|
+
x = x.asformat("coo")
|
|
474
|
+
if copy:
|
|
475
|
+
x = x.copy()
|
|
476
|
+
if not x.has_canonical_format:
|
|
477
|
+
x.sum_duplicates()
|
|
478
|
+
assert x.has_canonical_format
|
|
479
|
+
|
|
480
|
+
if x.format == "coo":
|
|
481
|
+
return cls.construct_coo_jl_object(
|
|
482
|
+
coords=(x.col, x.row),
|
|
483
|
+
data=x.data,
|
|
484
|
+
shape=x.shape[::-1],
|
|
485
|
+
order=Tensor.row_major,
|
|
486
|
+
fill_value=fill_value,
|
|
487
|
+
)
|
|
488
|
+
if x.format == "csc":
|
|
489
|
+
return cls.construct_csc_jl_object(
|
|
490
|
+
arg=(x.data, x.indices, x.indptr),
|
|
491
|
+
shape=x.shape,
|
|
492
|
+
fill_value=fill_value,
|
|
493
|
+
)
|
|
494
|
+
if x.format == "csr":
|
|
495
|
+
return cls.construct_csr_jl_object(
|
|
496
|
+
arg=(x.data, x.indices, x.indptr),
|
|
497
|
+
shape=x.shape,
|
|
498
|
+
fill_value=fill_value,
|
|
499
|
+
)
|
|
500
|
+
raise ValueError(f"Unsupported SciPy format: {type(x)}")
|
|
501
|
+
|
|
502
|
+
@classmethod
|
|
503
|
+
def construct_coo_jl_object(
|
|
504
|
+
cls, coords, data, shape, order, fill_value=0.0
|
|
505
|
+
) -> JuliaObj:
|
|
506
|
+
assert len(coords) == 2
|
|
507
|
+
ndim = len(shape)
|
|
508
|
+
order = cls.preprocess_order(order, ndim)
|
|
509
|
+
|
|
510
|
+
lvl = jl.Element(data.dtype.type(fill_value), data)
|
|
511
|
+
ptr = jl.Vector[jl.Int]([1, len(data) + 1])
|
|
512
|
+
tbl = tuple(jl.PlusOneVector(arr) for arr in coords)
|
|
513
|
+
|
|
514
|
+
return jl.swizzle(jl.Tensor(jl.SparseCOO[ndim](lvl, shape, ptr, tbl)), *order)
|
|
515
|
+
|
|
516
|
+
@classmethod
|
|
517
|
+
def construct_coo(
|
|
518
|
+
cls, coords, data, shape, order=row_major, fill_value=0.0
|
|
519
|
+
) -> Tensor:
|
|
520
|
+
return Tensor(
|
|
521
|
+
cls.construct_coo_jl_object(coords, data, shape, order, fill_value)
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
@staticmethod
|
|
525
|
+
def _construct_compressed2d_jl_object(
|
|
526
|
+
arg: TupleOf3Arrays,
|
|
527
|
+
shape: tuple[int, ...],
|
|
528
|
+
order: tuple[int, ...],
|
|
529
|
+
fill_value: np.number = 0.0,
|
|
530
|
+
) -> JuliaObj:
|
|
531
|
+
assert isinstance(arg, tuple) and len(arg) == 3
|
|
532
|
+
assert len(shape) == 2
|
|
533
|
+
|
|
534
|
+
data, indices, indptr = arg
|
|
535
|
+
dtype = data.dtype.type
|
|
536
|
+
indices = jl.PlusOneVector(indices)
|
|
537
|
+
indptr = jl.PlusOneVector(indptr)
|
|
538
|
+
|
|
539
|
+
lvl = jl.Element(dtype(fill_value), data)
|
|
540
|
+
return jl.swizzle(
|
|
541
|
+
jl.Tensor(
|
|
542
|
+
jl.Dense(jl.SparseList(lvl, shape[0], indptr, indices), shape[1])
|
|
543
|
+
),
|
|
544
|
+
*order,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
@classmethod
|
|
548
|
+
def construct_csc_jl_object(
|
|
549
|
+
cls, arg: TupleOf3Arrays, shape: tuple[int, ...], fill_value: np.number = 0.0
|
|
550
|
+
) -> JuliaObj:
|
|
551
|
+
return cls._construct_compressed2d_jl_object(
|
|
552
|
+
arg=arg, shape=shape, order=(1, 2), fill_value=fill_value
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
@classmethod
|
|
556
|
+
def construct_csc(
|
|
557
|
+
cls, arg: TupleOf3Arrays, shape: tuple[int, ...], fill_value: np.number = 0.0
|
|
558
|
+
) -> Tensor:
|
|
559
|
+
return Tensor(cls.construct_csc_jl_object(arg, shape, fill_value))
|
|
560
|
+
|
|
561
|
+
@classmethod
|
|
562
|
+
def construct_csr_jl_object(
|
|
563
|
+
cls, arg: TupleOf3Arrays, shape: tuple[int, ...], fill_value: np.number = 0.0
|
|
564
|
+
) -> JuliaObj:
|
|
565
|
+
return cls._construct_compressed2d_jl_object(
|
|
566
|
+
arg=arg, shape=shape[::-1], order=(2, 1), fill_value=fill_value
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
@classmethod
|
|
570
|
+
def construct_csr(
|
|
571
|
+
cls, arg: TupleOf3Arrays, shape: tuple[int, ...], fill_value: np.number = 0.0
|
|
572
|
+
) -> Tensor:
|
|
573
|
+
return Tensor(cls.construct_csr_jl_object(arg, shape, fill_value))
|
|
574
|
+
|
|
575
|
+
@staticmethod
|
|
576
|
+
def construct_csf_jl_object(
|
|
577
|
+
arg: TupleOf3Arrays, shape: tuple[int, ...], fill_value: np.number = 0.0
|
|
578
|
+
) -> JuliaObj:
|
|
579
|
+
assert isinstance(arg, tuple) and len(arg) == 3
|
|
580
|
+
|
|
581
|
+
data, indices_list, indptr_list = arg
|
|
582
|
+
dtype = data.dtype.type
|
|
583
|
+
|
|
584
|
+
assert len(indices_list) == len(shape) - 1
|
|
585
|
+
assert len(indptr_list) == len(shape) - 1
|
|
586
|
+
|
|
587
|
+
indices_list = [jl.PlusOneVector(i) for i in indices_list]
|
|
588
|
+
indptr_list = [jl.PlusOneVector(i) for i in indptr_list]
|
|
589
|
+
|
|
590
|
+
lvl = jl.Element(dtype(fill_value), data)
|
|
591
|
+
for size, indices, indptr in zip(
|
|
592
|
+
shape[:-1], indices_list, indptr_list, strict=False
|
|
593
|
+
):
|
|
594
|
+
lvl = jl.SparseList(lvl, size, indptr, indices)
|
|
595
|
+
|
|
596
|
+
return jl.swizzle(
|
|
597
|
+
jl.Tensor(jl.Dense(lvl, shape[-1])), *range(1, len(shape) + 1)
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
@classmethod
|
|
601
|
+
def construct_csf(
|
|
602
|
+
cls, arg: TupleOf3Arrays, shape: tuple[int, ...], fill_value: np.number = 0.0
|
|
603
|
+
) -> Tensor:
|
|
604
|
+
return Tensor(cls.construct_csf_jl_object(arg, shape, fill_value))
|
|
605
|
+
|
|
606
|
+
def to_scipy_sparse(self, accept_fv=None):
|
|
607
|
+
import scipy.sparse as sp
|
|
608
|
+
|
|
609
|
+
if accept_fv is None:
|
|
610
|
+
accept_fv = [0]
|
|
611
|
+
elif not isinstance(accept_fv, Iterable):
|
|
612
|
+
accept_fv = [accept_fv]
|
|
613
|
+
|
|
614
|
+
if self.ndim != 2:
|
|
615
|
+
raise ValueError(
|
|
616
|
+
"Can only convert a 2-dimensional array to a Scipy sparse matrix."
|
|
617
|
+
)
|
|
618
|
+
if not builtins.any(_eq_scalars(self.fill_value, fv) for fv in accept_fv):
|
|
619
|
+
raise ValueError(
|
|
620
|
+
f"Can only convert arrays with {accept_fv} fill-values "
|
|
621
|
+
"to a Scipy sparse matrix."
|
|
622
|
+
)
|
|
623
|
+
order = self.get_order()
|
|
624
|
+
body = self._obj.body
|
|
625
|
+
|
|
626
|
+
if str(jl.typeof(body.lvl).name.name) == "SparseCOOLevel":
|
|
627
|
+
data = np.asarray(body.lvl.lvl.val)
|
|
628
|
+
coords = body.lvl.tbl
|
|
629
|
+
row, col = coords[::-1] if order == (1, 0) else coords
|
|
630
|
+
row, col = np.asarray(row) - 1, np.asarray(col) - 1
|
|
631
|
+
return sp.coo_matrix((data, (row, col)), shape=self.shape)
|
|
632
|
+
|
|
633
|
+
if (
|
|
634
|
+
str(jl.typeof(body.lvl).name.name) == "DenseLevel"
|
|
635
|
+
and str(jl.typeof(body.lvl.lvl).name.name) == "SparseListLevel"
|
|
636
|
+
):
|
|
637
|
+
data = np.asarray(body.lvl.lvl.lvl.val)
|
|
638
|
+
indices = np.asarray(body.lvl.lvl.idx) - 1
|
|
639
|
+
indptr = np.asarray(body.lvl.lvl.ptr) - 1
|
|
640
|
+
sp_class = sp.csr_matrix if order == (1, 0) else sp.csc_matrix
|
|
641
|
+
return sp_class((data, indices, indptr), shape=self.shape)
|
|
642
|
+
if (
|
|
643
|
+
jl.typeof(body.lvl).name.name in sparse_formats_names
|
|
644
|
+
or jl.typeof(body.lvl.lvl).name.name in sparse_formats_names
|
|
645
|
+
):
|
|
646
|
+
storage = Storage(SparseCOO(self.ndim, Element(self.fill_value)), order)
|
|
647
|
+
return self.to_storage(storage).to_scipy_sparse()
|
|
648
|
+
raise ValueError("Tensor can't be converted to scipy.sparse object.")
|
|
649
|
+
|
|
650
|
+
@staticmethod
|
|
651
|
+
def _raise_julia_copy_not_supported() -> None:
|
|
652
|
+
raise ValueError("copy=True isn't supported for Julia object inputs")
|
|
653
|
+
|
|
654
|
+
def __array_namespace__(self, *, api_version: str | None = None) -> Any:
|
|
655
|
+
if api_version is None:
|
|
656
|
+
api_version = "2024.12"
|
|
657
|
+
|
|
658
|
+
if api_version not in {"2021.12", "2022.12", "2023.12", "2024.12"}:
|
|
659
|
+
raise ValueError(f'"{api_version}" Array API version not supported.')
|
|
660
|
+
import galley_jl_python
|
|
661
|
+
|
|
662
|
+
return galley_jl_python
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def random(shape, density=0.01, random_state=None):
|
|
666
|
+
args = [*shape, density]
|
|
667
|
+
if random_state is not None:
|
|
668
|
+
if isinstance(random_state, np.random.Generator):
|
|
669
|
+
seed = random_state.integers(np.iinfo(np.int32).max)
|
|
670
|
+
else:
|
|
671
|
+
seed = random_state
|
|
672
|
+
rng = jl.Random.default_rng()
|
|
673
|
+
jl.Random.seed_b(rng, seed)
|
|
674
|
+
args = [rng] + args
|
|
675
|
+
return Tensor(jl.fsprand(*args))
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def asarray(
|
|
679
|
+
obj,
|
|
680
|
+
/,
|
|
681
|
+
*,
|
|
682
|
+
dtype: DType | None = None,
|
|
683
|
+
format: str | None = None,
|
|
684
|
+
fill_value: np.number | None = None,
|
|
685
|
+
device: Device | None = None,
|
|
686
|
+
copy: bool | None = None,
|
|
687
|
+
) -> Tensor:
|
|
688
|
+
if format not in {"coo", "csr", "csc", "csf", "dense", None}:
|
|
689
|
+
raise ValueError(f"{format} format not supported.")
|
|
690
|
+
_validate_device(device)
|
|
691
|
+
tensor = (
|
|
692
|
+
obj
|
|
693
|
+
if isinstance(obj, Tensor)
|
|
694
|
+
else Tensor(obj, fill_value=fill_value, copy=copy)
|
|
695
|
+
)
|
|
696
|
+
if format is not None:
|
|
697
|
+
if copy is False:
|
|
698
|
+
raise ValueError(
|
|
699
|
+
"Unable to avoid copy while creating an array as requested."
|
|
700
|
+
)
|
|
701
|
+
order = tensor.get_order()
|
|
702
|
+
if format == "coo":
|
|
703
|
+
storage = Storage(SparseCOO(tensor.ndim, Element(tensor.fill_value)), order)
|
|
704
|
+
elif format == "csr":
|
|
705
|
+
storage = Storage(Dense(SparseList(Element(tensor.fill_value))), (2, 1))
|
|
706
|
+
elif format == "csc":
|
|
707
|
+
storage = Storage(Dense(SparseList(Element(tensor.fill_value))), (1, 2))
|
|
708
|
+
elif format == "csf":
|
|
709
|
+
storage = Element(tensor.fill_value)
|
|
710
|
+
for _ in range(tensor.ndim - 1):
|
|
711
|
+
storage = SparseList(storage)
|
|
712
|
+
storage = Storage(Dense(storage), order)
|
|
713
|
+
elif format == "dense":
|
|
714
|
+
storage = DenseStorage(tensor.ndim, tensor.dtype, order)
|
|
715
|
+
tensor = tensor.to_storage(storage)
|
|
716
|
+
|
|
717
|
+
if dtype is not None:
|
|
718
|
+
return astype(tensor, dtype, copy=copy)
|
|
719
|
+
return tensor
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def reshape(
|
|
723
|
+
x: Tensor, /, shape: tuple[int, ...], *, copy: bool | None = None
|
|
724
|
+
) -> Tensor:
|
|
725
|
+
if copy is False:
|
|
726
|
+
raise ValueError("Unable to avoid copy during reshape.")
|
|
727
|
+
# TODO: https://github.com/finch-tensor/Finch.jl/issues/743
|
|
728
|
+
# Revert to `jl.reshape` implementation once aforementioned
|
|
729
|
+
# issue is solved.
|
|
730
|
+
warnings.warn(
|
|
731
|
+
"`reshape` densified the input tensor.", PerformanceWarning, stacklevel=2
|
|
732
|
+
)
|
|
733
|
+
arr = x.todense()
|
|
734
|
+
arr = arr.reshape(shape)
|
|
735
|
+
return Tensor(arr)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def full(
|
|
739
|
+
shape: int | tuple[int, ...],
|
|
740
|
+
fill_value: jl_dtypes.number,
|
|
741
|
+
*,
|
|
742
|
+
dtype: DType | None = None,
|
|
743
|
+
format: str = "coo",
|
|
744
|
+
device: Device = None,
|
|
745
|
+
) -> Tensor:
|
|
746
|
+
_validate_device(device)
|
|
747
|
+
if not np.isscalar(fill_value):
|
|
748
|
+
raise ValueError("`fill_value` must be a scalar")
|
|
749
|
+
if format not in ("coo", "dense"):
|
|
750
|
+
raise ValueError(f"{format} format not supported.")
|
|
751
|
+
if isinstance(shape, int):
|
|
752
|
+
shape = (shape,)
|
|
753
|
+
dtype = (
|
|
754
|
+
np.asarray(fill_value).dtype.type
|
|
755
|
+
if dtype is None
|
|
756
|
+
else jl_dtypes.jl_to_np_dtype[dtype]
|
|
757
|
+
)
|
|
758
|
+
if dtype == np.bool_: # Fails with: Finch currently only supports isbits defaults
|
|
759
|
+
dtype = bool
|
|
760
|
+
|
|
761
|
+
if format == "coo" and shape != ():
|
|
762
|
+
return Tensor(
|
|
763
|
+
jl.Tensor(jl.SparseCOO[len(shape)](jl.Element(dtype(fill_value))), *shape)
|
|
764
|
+
)
|
|
765
|
+
# for dense format or () shape
|
|
766
|
+
return Tensor(np.full(shape, fill_value, dtype=dtype))
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def full_like(
|
|
770
|
+
x: Tensor,
|
|
771
|
+
/,
|
|
772
|
+
fill_value: jl_dtypes.number,
|
|
773
|
+
*,
|
|
774
|
+
dtype: DType | None = None,
|
|
775
|
+
format: str = "coo",
|
|
776
|
+
device: Device = None,
|
|
777
|
+
) -> Tensor:
|
|
778
|
+
return full(x.shape, fill_value, dtype=dtype, format=format, device=device)
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def ones(
|
|
782
|
+
shape: int | tuple[int, ...],
|
|
783
|
+
*,
|
|
784
|
+
dtype: DType | None = None,
|
|
785
|
+
format: str = "coo",
|
|
786
|
+
device: Device = None,
|
|
787
|
+
) -> Tensor:
|
|
788
|
+
return full(shape, np.float64(1), dtype=dtype, format=format, device=device)
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def ones_like(
|
|
792
|
+
x: Tensor,
|
|
793
|
+
/,
|
|
794
|
+
*,
|
|
795
|
+
dtype: DType | None = None,
|
|
796
|
+
format: str = "coo",
|
|
797
|
+
device: Device = None,
|
|
798
|
+
) -> Tensor:
|
|
799
|
+
dtype = x.dtype if dtype is None else dtype
|
|
800
|
+
return ones(x.shape, dtype=dtype, format=format, device=device)
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def zeros(
|
|
804
|
+
shape: int | tuple[int, ...],
|
|
805
|
+
*,
|
|
806
|
+
dtype: DType | None = None,
|
|
807
|
+
format: str = "coo",
|
|
808
|
+
device: Device = None,
|
|
809
|
+
) -> Tensor:
|
|
810
|
+
return full(shape, np.float64(0), dtype=dtype, format=format, device=device)
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
def zeros_like(
|
|
814
|
+
x: Tensor,
|
|
815
|
+
/,
|
|
816
|
+
*,
|
|
817
|
+
dtype: DType | None = None,
|
|
818
|
+
format: str = "coo",
|
|
819
|
+
device: Device = None,
|
|
820
|
+
) -> Tensor:
|
|
821
|
+
dtype = x.dtype if dtype is None else dtype
|
|
822
|
+
return zeros(x.shape, dtype=dtype, format=format, device=device)
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def empty(
|
|
826
|
+
shape: int | tuple[int, ...],
|
|
827
|
+
*,
|
|
828
|
+
dtype: DType | None = None,
|
|
829
|
+
format: str = "coo",
|
|
830
|
+
device: Device = None,
|
|
831
|
+
) -> Tensor:
|
|
832
|
+
return full(shape, np.float64(0), dtype=dtype, format=format, device=device)
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
def empty_like(
|
|
836
|
+
x: Tensor,
|
|
837
|
+
/,
|
|
838
|
+
*,
|
|
839
|
+
dtype: DType | None = None,
|
|
840
|
+
format: str = "coo",
|
|
841
|
+
device: Device = None,
|
|
842
|
+
) -> Tensor:
|
|
843
|
+
dtype = x.dtype if dtype is None else dtype
|
|
844
|
+
return empty(x.shape, dtype=dtype, format=format, device=device)
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
def arange(
|
|
848
|
+
start: float,
|
|
849
|
+
/,
|
|
850
|
+
stop: float | None = None,
|
|
851
|
+
step: float = 1,
|
|
852
|
+
*,
|
|
853
|
+
dtype: DType | None = None,
|
|
854
|
+
device: Device = None,
|
|
855
|
+
) -> Tensor:
|
|
856
|
+
_validate_device(device)
|
|
857
|
+
return Tensor(np.arange(start, stop, step, jl_dtypes.jl_to_np_dtype[dtype]))
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def linspace(
|
|
861
|
+
start: complex,
|
|
862
|
+
stop: complex,
|
|
863
|
+
/,
|
|
864
|
+
num: int,
|
|
865
|
+
*,
|
|
866
|
+
dtype: DType | None = None,
|
|
867
|
+
device: Device = None,
|
|
868
|
+
endpoint: bool = True,
|
|
869
|
+
) -> Tensor:
|
|
870
|
+
_validate_device(device)
|
|
871
|
+
return Tensor(
|
|
872
|
+
np.linspace(
|
|
873
|
+
start,
|
|
874
|
+
stop,
|
|
875
|
+
num=num,
|
|
876
|
+
dtype=jl_dtypes.jl_to_np_dtype[dtype],
|
|
877
|
+
endpoint=endpoint,
|
|
878
|
+
)
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
def permute_dims(x: Tensor, axes: tuple[int, ...]) -> Tensor:
|
|
883
|
+
return x.permute_dims(axes)
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def moveaxis(x: Tensor, source: int, destination: int) -> Tensor:
|
|
887
|
+
axes = list(range(x.ndim))
|
|
888
|
+
norm_source = normalize_axis_index(source, x.ndim)
|
|
889
|
+
norm_dest = normalize_axis_index(destination, x.ndim)
|
|
890
|
+
axes.insert(norm_dest, axes.pop(norm_source))
|
|
891
|
+
return x.permute_dims(tuple(axes))
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def astype(x: Tensor, dtype: DType, /, *, copy: bool = True) -> Tensor:
|
|
895
|
+
if not copy:
|
|
896
|
+
if x.dtype == dtype:
|
|
897
|
+
return x
|
|
898
|
+
if copy is False:
|
|
899
|
+
raise ValueError("Unable to avoid a copy while casting in no-copy mode.")
|
|
900
|
+
|
|
901
|
+
finch_tns = x._obj.body
|
|
902
|
+
result = jl.copyto_b(
|
|
903
|
+
jl.similar(finch_tns, jc.convert(dtype, jl.fill_value(finch_tns)), dtype),
|
|
904
|
+
finch_tns,
|
|
905
|
+
)
|
|
906
|
+
return Tensor(jl.swizzle(result, *x.get_order(zero_indexing=False)))
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def where(condition: Tensor, x1: Tensor, x2: Tensor, /) -> Tensor:
|
|
910
|
+
axis_cond, axis_x1, axis_x2 = (
|
|
911
|
+
range(condition.ndim, 0, -1),
|
|
912
|
+
range(x1.ndim, 0, -1),
|
|
913
|
+
range(x2.ndim, 0, -1),
|
|
914
|
+
)
|
|
915
|
+
# inverse swizzle, so `broadcast` appends new dims to the front
|
|
916
|
+
result = jl.broadcast(
|
|
917
|
+
jl.ifelse,
|
|
918
|
+
jl.permutedims(condition._obj, tuple(axis_cond)),
|
|
919
|
+
jl.permutedims(x1._obj, tuple(axis_x1)),
|
|
920
|
+
jl.permutedims(x2._obj, tuple(axis_x2)),
|
|
921
|
+
)
|
|
922
|
+
# swizzle back to the original order
|
|
923
|
+
result = jl.permutedims(result, tuple(range(jl.ndims(result), 0, -1)))
|
|
924
|
+
return Tensor(result)
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
def nonzero(x: Tensor, /) -> tuple[np.ndarray, ...]:
|
|
928
|
+
indices = jl.ffindnz(x._obj)[:-1] # return only indices, skip values
|
|
929
|
+
indices = tuple(np.asarray(i) - 1 for i in indices)
|
|
930
|
+
sort_order = np.lexsort(indices[::-1]) # sort to row-major, C-style order
|
|
931
|
+
return tuple(Tensor(i[sort_order]) for i in indices)
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
def _reduce_core(
|
|
935
|
+
x: Tensor, fn: Callable, axis: int | tuple[int, ...] | None, keepdims: bool = False
|
|
936
|
+
):
|
|
937
|
+
if axis is None:
|
|
938
|
+
axis = tuple(range(x.ndim))
|
|
939
|
+
axis = normalize_axis_tuple(axis, x.ndim)
|
|
940
|
+
axis = tuple(i + 1 for i in axis)
|
|
941
|
+
if keepdims:
|
|
942
|
+
return fn(x._obj, dims=axis)
|
|
943
|
+
if x.is_computed():
|
|
944
|
+
return jl.compute(jl.dropdims(fn(jl.lazy(x._obj), dims=axis), dims=axis))
|
|
945
|
+
return jl.dropdims(fn(x._obj, dims=axis), dims=axis)
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
def _reduce_sum_prod(
|
|
949
|
+
x: Tensor,
|
|
950
|
+
fn: Callable,
|
|
951
|
+
axis: int | tuple[int, ...] | None,
|
|
952
|
+
dtype: DType | None,
|
|
953
|
+
keepdims: bool = False,
|
|
954
|
+
) -> Tensor:
|
|
955
|
+
result = _reduce_core(x, fn, axis, keepdims)
|
|
956
|
+
|
|
957
|
+
if np.isscalar(result):
|
|
958
|
+
tmp_dtype = jl_dtypes.int_ if jl.seval(f"{x.dtype} <: Integer") else x.dtype
|
|
959
|
+
result = jl.Tensor(
|
|
960
|
+
jl.Element(
|
|
961
|
+
jc.convert(tmp_dtype, 0),
|
|
962
|
+
np.array(result, dtype=jl_dtypes.jl_to_np_dtype[tmp_dtype]),
|
|
963
|
+
)
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
result = Tensor(result)
|
|
967
|
+
|
|
968
|
+
if jl.isa(result._obj, jl.Finch.LazyTensor):
|
|
969
|
+
if dtype is not None:
|
|
970
|
+
raise ValueError(
|
|
971
|
+
"`dtype` keyword for `sum` and `prod` in the lazy mode isn't supported"
|
|
972
|
+
)
|
|
973
|
+
# dtype casting rules
|
|
974
|
+
elif dtype is not None:
|
|
975
|
+
result = astype(result, dtype, copy=None)
|
|
976
|
+
elif jl.seval(f"{x.dtype} <: Unsigned"):
|
|
977
|
+
result = astype(result, jl_dtypes.uint, copy=None)
|
|
978
|
+
elif jl.seval(f"{x.dtype} <: Signed"):
|
|
979
|
+
result = astype(result, jl_dtypes.int_, copy=None)
|
|
980
|
+
|
|
981
|
+
return result
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def _reduce(
|
|
985
|
+
x: Tensor, fn: Callable, axis: int | tuple[int, ...] | None, keepdims: bool = False
|
|
986
|
+
) -> Tensor:
|
|
987
|
+
result = _reduce_core(x, fn, axis, keepdims)
|
|
988
|
+
if np.isscalar(result):
|
|
989
|
+
result = jl.Tensor(
|
|
990
|
+
jl.Element(
|
|
991
|
+
jc.convert(x.dtype, 0),
|
|
992
|
+
np.array(result, dtype=jl_dtypes.jl_to_np_dtype[x.dtype]),
|
|
993
|
+
)
|
|
994
|
+
)
|
|
995
|
+
return Tensor(result)
|
|
996
|
+
|
|
997
|
+
|
|
998
|
+
def sum(
|
|
999
|
+
x: Tensor,
|
|
1000
|
+
/,
|
|
1001
|
+
*,
|
|
1002
|
+
axis: int | tuple[int, ...] | None = None,
|
|
1003
|
+
dtype: DType | None = None,
|
|
1004
|
+
keepdims: bool = False,
|
|
1005
|
+
) -> Tensor:
|
|
1006
|
+
return _reduce_sum_prod(x, jl.sum, axis, dtype, keepdims)
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def prod(
|
|
1010
|
+
x: Tensor,
|
|
1011
|
+
/,
|
|
1012
|
+
*,
|
|
1013
|
+
axis: int | tuple[int, ...] | None = None,
|
|
1014
|
+
dtype: DType | None = None,
|
|
1015
|
+
keepdims: bool = False,
|
|
1016
|
+
) -> Tensor:
|
|
1017
|
+
return _reduce_sum_prod(x, jl.prod, axis, dtype, keepdims)
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
def max(
|
|
1021
|
+
x: Tensor,
|
|
1022
|
+
/,
|
|
1023
|
+
*,
|
|
1024
|
+
axis: int | tuple[int, ...] | None = None,
|
|
1025
|
+
keepdims: bool = False,
|
|
1026
|
+
) -> Tensor:
|
|
1027
|
+
return _reduce(x, jl.maximum, axis, keepdims)
|
|
1028
|
+
|
|
1029
|
+
|
|
1030
|
+
def min(
|
|
1031
|
+
x: Tensor,
|
|
1032
|
+
/,
|
|
1033
|
+
*,
|
|
1034
|
+
axis: int | tuple[int, ...] | None = None,
|
|
1035
|
+
keepdims: bool = False,
|
|
1036
|
+
) -> Tensor:
|
|
1037
|
+
return _reduce(x, jl.minimum, axis, keepdims)
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
def any(
|
|
1041
|
+
x: Tensor,
|
|
1042
|
+
/,
|
|
1043
|
+
*,
|
|
1044
|
+
axis: int | tuple[int, ...] | None = None,
|
|
1045
|
+
keepdims: bool = False,
|
|
1046
|
+
) -> Tensor:
|
|
1047
|
+
return _reduce(x != 0, jl.any, axis, keepdims)
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
def all(
|
|
1051
|
+
x: Tensor,
|
|
1052
|
+
/,
|
|
1053
|
+
*,
|
|
1054
|
+
axis: int | tuple[int, ...] | None = None,
|
|
1055
|
+
keepdims: bool = False,
|
|
1056
|
+
) -> Tensor:
|
|
1057
|
+
return _reduce(x != 0, jl.all, axis, keepdims)
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
def mean(
|
|
1061
|
+
x: Tensor,
|
|
1062
|
+
/,
|
|
1063
|
+
*,
|
|
1064
|
+
axis: int | tuple[int, ...] | None = None,
|
|
1065
|
+
keepdims: bool = False,
|
|
1066
|
+
) -> Tensor:
|
|
1067
|
+
return _reduce(x, jl.mean, axis, keepdims)
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
def std(
|
|
1071
|
+
x: Tensor,
|
|
1072
|
+
/,
|
|
1073
|
+
*,
|
|
1074
|
+
axis: int | tuple[int, ...] | None = None,
|
|
1075
|
+
correction: float = 0.0,
|
|
1076
|
+
keepdims: bool = False,
|
|
1077
|
+
) -> Tensor:
|
|
1078
|
+
def _std(x, dims):
|
|
1079
|
+
return jl.std(x, correction=correction, dims=dims)
|
|
1080
|
+
|
|
1081
|
+
return _reduce(x, _std, axis, keepdims)
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
def var(
|
|
1085
|
+
x: Tensor,
|
|
1086
|
+
/,
|
|
1087
|
+
*,
|
|
1088
|
+
axis: int | tuple[int, ...] | None = None,
|
|
1089
|
+
correction: float = 0.0,
|
|
1090
|
+
keepdims: bool = False,
|
|
1091
|
+
) -> Tensor:
|
|
1092
|
+
def _var(x, dims):
|
|
1093
|
+
return jl.var(x, correction=correction, dims=dims)
|
|
1094
|
+
|
|
1095
|
+
return _reduce(x, _var, axis, keepdims)
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
def argmin(
|
|
1099
|
+
x: Tensor,
|
|
1100
|
+
/,
|
|
1101
|
+
*,
|
|
1102
|
+
axis: int | None = None,
|
|
1103
|
+
keepdims: bool = False,
|
|
1104
|
+
) -> Tensor:
|
|
1105
|
+
return _reduce(x, jl.Finch.argmin_python, axis, keepdims)
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def argmax(
|
|
1109
|
+
x: Tensor,
|
|
1110
|
+
/,
|
|
1111
|
+
*,
|
|
1112
|
+
axis: int | None = None,
|
|
1113
|
+
keepdims: bool = False,
|
|
1114
|
+
) -> Tensor:
|
|
1115
|
+
return _reduce(x, jl.Finch.argmax_python, axis, keepdims)
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def squeeze(
|
|
1119
|
+
x: Tensor,
|
|
1120
|
+
/,
|
|
1121
|
+
axis: int | tuple[int, ...],
|
|
1122
|
+
) -> Tensor:
|
|
1123
|
+
if isinstance(axis, int):
|
|
1124
|
+
axis = (axis,)
|
|
1125
|
+
axis = normalize_axis_tuple(axis, x.ndim)
|
|
1126
|
+
axis = tuple(i + 1 for i in axis)
|
|
1127
|
+
result = jl.dropdims(x._obj, dims=axis)
|
|
1128
|
+
return Tensor(result)
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
def expand_dims(
|
|
1132
|
+
x: Tensor,
|
|
1133
|
+
/,
|
|
1134
|
+
axis: int | tuple[int, ...] = 0,
|
|
1135
|
+
) -> Tensor:
|
|
1136
|
+
if isinstance(axis, int):
|
|
1137
|
+
axis = (axis,)
|
|
1138
|
+
axis = normalize_axis_tuple(axis, x.ndim + len(axis))
|
|
1139
|
+
axis = tuple(i + 1 for i in axis)
|
|
1140
|
+
result = jl.expanddims(x._obj, dims=axis)
|
|
1141
|
+
return Tensor(result)
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
def diagonal(x: Tensor, /, *, offset: int = 0) -> Tensor:
|
|
1145
|
+
m = x.shape[-2]
|
|
1146
|
+
n = x.shape[-1]
|
|
1147
|
+
mask = eye(m, n, k=offset, format="coo", dtype=bool)
|
|
1148
|
+
res = compute(sum(where(mask, lazy(x), zeros(x.shape)), axis=-1))
|
|
1149
|
+
return res[..., 0 : builtins.min(m, n, m + offset, n - offset)]
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
def eye(
|
|
1153
|
+
n_rows: int,
|
|
1154
|
+
n_cols: int | None = None,
|
|
1155
|
+
/,
|
|
1156
|
+
*,
|
|
1157
|
+
k: int = 0,
|
|
1158
|
+
dtype: DType | None = None,
|
|
1159
|
+
format: Literal["coo", "dense"] = "coo",
|
|
1160
|
+
device: Device = None,
|
|
1161
|
+
) -> Tensor:
|
|
1162
|
+
_validate_device(device)
|
|
1163
|
+
n_cols = n_rows if n_cols is None else n_cols
|
|
1164
|
+
dtype = jl_dtypes.float64 if dtype is None else dtype
|
|
1165
|
+
tns = jl.Finch.eye_python(n_rows, n_cols, k, dtype(False))
|
|
1166
|
+
if format == "coo":
|
|
1167
|
+
return Tensor(tns)
|
|
1168
|
+
if format == "dense":
|
|
1169
|
+
return Tensor(jl.Tensor(jl.DenseFormat(2, dtype(False)), tns))
|
|
1170
|
+
raise ValueError(f"{format} not supported, only 'coo' and 'dense' is allowed.")
|
|
1171
|
+
|
|
1172
|
+
|
|
1173
|
+
def tensordot(x1: Tensor, x2: Tensor, /, *, axes=2) -> Tensor:
|
|
1174
|
+
if not isinstance(x1, Tensor):
|
|
1175
|
+
x1 = Tensor(x1)
|
|
1176
|
+
if not isinstance(x2, Tensor):
|
|
1177
|
+
x2 = Tensor(x2)
|
|
1178
|
+
if isinstance(axes, Iterable):
|
|
1179
|
+
self_axes = normalize_axis_tuple(axes[0], x1.ndim)
|
|
1180
|
+
other_axes = normalize_axis_tuple(axes[1], x2.ndim)
|
|
1181
|
+
axes = (tuple(i + 1 for i in self_axes), tuple(i + 1 for i in other_axes))
|
|
1182
|
+
|
|
1183
|
+
result = jl.tensordot(x1._obj, x2._obj, axes)
|
|
1184
|
+
return Tensor(result)
|
|
1185
|
+
|
|
1186
|
+
|
|
1187
|
+
def log(x: Tensor, /) -> Tensor:
|
|
1188
|
+
return x._elemwise_op("log")
|
|
1189
|
+
|
|
1190
|
+
|
|
1191
|
+
def log10(x: Tensor, /) -> Tensor:
|
|
1192
|
+
return x._elemwise_op("log10")
|
|
1193
|
+
|
|
1194
|
+
|
|
1195
|
+
def log1p(x: Tensor, /) -> Tensor:
|
|
1196
|
+
return x._elemwise_op("log1p")
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
def log2(x: Tensor, /) -> Tensor:
|
|
1200
|
+
return x._elemwise_op("log2")
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
def sqrt(x: Tensor, /) -> Tensor:
|
|
1204
|
+
return x._elemwise_op("sqrt")
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
def sign(x: Tensor, /) -> Tensor:
|
|
1208
|
+
return x._elemwise_op("sign")
|
|
1209
|
+
|
|
1210
|
+
|
|
1211
|
+
def round(x: Tensor, /) -> Tensor:
|
|
1212
|
+
return x._elemwise_op("round")
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
def isnan(x: Tensor, /) -> Tensor:
|
|
1216
|
+
return x._elemwise_op("isnan")
|
|
1217
|
+
|
|
1218
|
+
|
|
1219
|
+
def isinf(x: Tensor, /) -> Tensor:
|
|
1220
|
+
return x._elemwise_op("isinf")
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
def isfinite(x: Tensor, /) -> Tensor:
|
|
1224
|
+
return x._elemwise_op("isfinite")
|
|
1225
|
+
|
|
1226
|
+
|
|
1227
|
+
def exp(x: Tensor, /) -> Tensor:
|
|
1228
|
+
return x._elemwise_op("exp")
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
def expm1(x: Tensor, /) -> Tensor:
|
|
1232
|
+
return x._elemwise_op("expm1")
|
|
1233
|
+
|
|
1234
|
+
|
|
1235
|
+
def floor(x: Tensor, /) -> Tensor:
|
|
1236
|
+
return x._elemwise_op("floor")
|
|
1237
|
+
|
|
1238
|
+
|
|
1239
|
+
def ceil(x: Tensor, /) -> Tensor:
|
|
1240
|
+
return x._elemwise_op("ceil")
|
|
1241
|
+
|
|
1242
|
+
|
|
1243
|
+
def cos(x: Tensor, /) -> Tensor:
|
|
1244
|
+
return x._elemwise_op("cos")
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
def cosh(x: Tensor, /) -> Tensor:
|
|
1248
|
+
return x._elemwise_op("cosh")
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
def acos(x: Tensor, /) -> Tensor:
|
|
1252
|
+
return x._elemwise_op("acos")
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
def acosh(x: Tensor, /) -> Tensor:
|
|
1256
|
+
return x._elemwise_op("acosh")
|
|
1257
|
+
|
|
1258
|
+
|
|
1259
|
+
def sin(x: Tensor, /) -> Tensor:
|
|
1260
|
+
return x._elemwise_op("sin")
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
def sinh(x: Tensor, /) -> Tensor:
|
|
1264
|
+
return x._elemwise_op("sinh")
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
def asin(x: Tensor, /) -> Tensor:
|
|
1268
|
+
return x._elemwise_op("asin")
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
def asinh(x: Tensor, /) -> Tensor:
|
|
1272
|
+
return x._elemwise_op("asinh")
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
def tan(x: Tensor, /) -> Tensor:
|
|
1276
|
+
return x._elemwise_op("tan")
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
def tanh(x: Tensor, /) -> Tensor:
|
|
1280
|
+
return x._elemwise_op("tanh")
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
def atan(x: Tensor, /) -> Tensor:
|
|
1284
|
+
return x._elemwise_op("atan")
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
def atanh(x: Tensor, /) -> Tensor:
|
|
1288
|
+
return x._elemwise_op("atanh")
|
|
1289
|
+
|
|
1290
|
+
|
|
1291
|
+
def atan2(x1: Tensor, x2: Tensor, /) -> Tensor:
|
|
1292
|
+
return x1._elemwise_op("atand", x2)
|
|
1293
|
+
|
|
1294
|
+
|
|
1295
|
+
def trunc(x: Tensor, /) -> Tensor:
|
|
1296
|
+
return x._elemwise_op("trunc")
|
|
1297
|
+
|
|
1298
|
+
|
|
1299
|
+
def real(x: Tensor, /) -> Tensor:
|
|
1300
|
+
return x._elemwise_op("real")
|
|
1301
|
+
|
|
1302
|
+
|
|
1303
|
+
def imag(x: Tensor, /) -> Tensor:
|
|
1304
|
+
return x._elemwise_op("imag")
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
def conj(x: Tensor, /) -> Tensor:
|
|
1308
|
+
return x._elemwise_op("conj")
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
def square(x: Tensor, /) -> Tensor:
|
|
1312
|
+
return x ** Tensor(2)
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
def logaddexp(x1: Tensor, x2: Tensor, /) -> Tensor:
|
|
1316
|
+
return log(exp(x1) + exp(x2))
|
|
1317
|
+
|
|
1318
|
+
|
|
1319
|
+
def logical_and(x1: Tensor, x2: Tensor, /) -> Tensor:
|
|
1320
|
+
return x1._elemwise_op("Finch.and", x2)
|
|
1321
|
+
|
|
1322
|
+
|
|
1323
|
+
def logical_or(x1: Tensor, x2: Tensor, /) -> Tensor:
|
|
1324
|
+
return x1._elemwise_op("Finch.or", x2)
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
def logical_xor(x1: Tensor, x2: Tensor, /) -> Tensor:
|
|
1328
|
+
return x1._elemwise_op("Finch.xor", x2)
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
def power(x1: Tensor, x2: Tensor, /) -> Tensor:
|
|
1332
|
+
return x1._elemwise_op("^", x2)
|
|
1333
|
+
|
|
1334
|
+
|
|
1335
|
+
def einop(prgm, **kwargs):
|
|
1336
|
+
"""Execute an einsum-like expression
|
|
1337
|
+
|
|
1338
|
+
This function parses and executes einsum-like expressions with extended syntax
|
|
1339
|
+
that supports various operations beyond traditional Einstein summation notation.
|
|
1340
|
+
|
|
1341
|
+
Args:
|
|
1342
|
+
prgm (str): Einsum program string specifying the computation. The syntax
|
|
1343
|
+
supports:
|
|
1344
|
+
- Assignment: "C[i,j] = A[i,j] + B[j,i]"
|
|
1345
|
+
- Increment: "C[i,j] += A[i,k] * B[k,j]"
|
|
1346
|
+
- Reductions: "C[i] += A[i,j]", "C[i] max= A[i,j]", "C[i] &= A[i,j]"
|
|
1347
|
+
- Arithmetic operations: +, -, *, /, //, %, **
|
|
1348
|
+
- Comparison operations: ==, !=, <, <=, >, >=
|
|
1349
|
+
- Logical operations: and, or, not
|
|
1350
|
+
- Bitwise operations: &, |, ^, <<, >>
|
|
1351
|
+
- Function calls and complex expressions with parentheses
|
|
1352
|
+
- Mathematical functions: abs, sqrt, exp, log, sin, cos, tan, etc.
|
|
1353
|
+
- Literal values: integers, floats, booleans, and complex numbers
|
|
1354
|
+
- Python operator precedence and parentheses for grouping
|
|
1355
|
+
**kwargs: Named arrays referenced in the einsum expression. The keys
|
|
1356
|
+
should match the tensor names used in the program string.
|
|
1357
|
+
|
|
1358
|
+
Returns:
|
|
1359
|
+
The result array from executing the einsum expression.
|
|
1360
|
+
|
|
1361
|
+
Examples:
|
|
1362
|
+
>>> A = finch.random.rand(3, 4)
|
|
1363
|
+
>>> B = finch.random.rand(4, 3)
|
|
1364
|
+
>>> # Matrix addition with transpose
|
|
1365
|
+
>>> C = finch.einop("C[i,j] = A[i,j] + B[j,i]", A=A, B=B)
|
|
1366
|
+
>>> # Matrix multiplication
|
|
1367
|
+
>>> D = finch.einop("D[i,j] += A[i,k] * B[k,j]", A=A, B=B)
|
|
1368
|
+
>>> # Min-Plus multiplication with shift
|
|
1369
|
+
>>> E = finch.einop("E[i] min= A[i,k] + D[k,j] << 1", A=A, D=D)
|
|
1370
|
+
"""
|
|
1371
|
+
import galley_jl_python
|
|
1372
|
+
|
|
1373
|
+
return einop_impl(galley_jl_python, prgm, **kwargs)
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
def einsum(*args, **kwargs):
|
|
1377
|
+
"""
|
|
1378
|
+
einsum(subscripts, *operands)
|
|
1379
|
+
|
|
1380
|
+
Evaluates the Einstein summation convention on the operands.
|
|
1381
|
+
|
|
1382
|
+
Using the Einstein summation convention, many common multi-dimensional,
|
|
1383
|
+
linear algebraic array operations can be represented in a simple fashion.
|
|
1384
|
+
In *implicit* mode `einsum` computes these values.
|
|
1385
|
+
|
|
1386
|
+
In *explicit* mode, `einsum` provides further flexibility to compute
|
|
1387
|
+
other array operations that might not be considered classical Einstein
|
|
1388
|
+
summation operations, by disabling, or forcing summation over specified
|
|
1389
|
+
subscript labels.
|
|
1390
|
+
|
|
1391
|
+
See the notes and examples for clarification.
|
|
1392
|
+
|
|
1393
|
+
Parameters
|
|
1394
|
+
----------
|
|
1395
|
+
subscripts : str
|
|
1396
|
+
Specifies the subscripts for summation as comma separated list of
|
|
1397
|
+
subscript labels. An implicit (classical Einstein summation)
|
|
1398
|
+
calculation is performed unless the explicit indicator '->' is
|
|
1399
|
+
included as well as subscript labels of the precise output form.
|
|
1400
|
+
operands : list of array_like
|
|
1401
|
+
These are the arrays for the operation.
|
|
1402
|
+
|
|
1403
|
+
Returns
|
|
1404
|
+
-------
|
|
1405
|
+
output : ndarray
|
|
1406
|
+
The calculation based on the Einstein summation convention.
|
|
1407
|
+
|
|
1408
|
+
Notes
|
|
1409
|
+
-----
|
|
1410
|
+
The Einstein summation convention can be used to compute
|
|
1411
|
+
many multi-dimensional, linear algebraic array operations. `einsum`
|
|
1412
|
+
provides a succinct way of representing these.
|
|
1413
|
+
|
|
1414
|
+
A non-exhaustive list of these operations,
|
|
1415
|
+
which can be computed by `einsum`, is shown below along with examples:
|
|
1416
|
+
|
|
1417
|
+
* Trace of an array, :py:func:`numpy.trace`.
|
|
1418
|
+
* Return a diagonal, :py:func:`numpy.diag`.
|
|
1419
|
+
* Array axis summations, :py:func:`numpy.sum`.
|
|
1420
|
+
* Transpositions and permutations, :py:func:`numpy.transpose`.
|
|
1421
|
+
* Matrix multiplication and dot product, :py:func:`numpy.matmul`
|
|
1422
|
+
:py:func:`numpy.dot`.
|
|
1423
|
+
* Vector inner and outer products, :py:func:`numpy.inner`
|
|
1424
|
+
:py:func:`numpy.outer`.
|
|
1425
|
+
* Broadcasting, element-wise and scalar multiplication,
|
|
1426
|
+
:py:func:`numpy.multiply`.
|
|
1427
|
+
* Tensor contractions, :py:func:`numpy.tensordot`.
|
|
1428
|
+
* Chained array operations, in efficient calculation order,
|
|
1429
|
+
:py:func:`numpy.einsum_path`.
|
|
1430
|
+
|
|
1431
|
+
The subscripts string is a comma-separated list of subscript labels,
|
|
1432
|
+
where each label refers to a dimension of the corresponding operand.
|
|
1433
|
+
Whenever a label is repeated it is summed, so ``np.einsum('i,i', a, b)``
|
|
1434
|
+
is equivalent to :py:func:`np.inner(a,b) <numpy.inner>`. If a label
|
|
1435
|
+
appears only once, it is not summed, so ``np.einsum('i', a)``
|
|
1436
|
+
produces a view of ``a`` with no changes. A further example
|
|
1437
|
+
``np.einsum('ij,jk', a, b)`` describes traditional matrix multiplication
|
|
1438
|
+
and is equivalent to :py:func:`np.matmul(a,b) <numpy.matmul>`.
|
|
1439
|
+
Repeated subscript labels in one operand take the diagonal.
|
|
1440
|
+
For example, ``np.einsum('ii', a)`` is equivalent to
|
|
1441
|
+
:py:func:`np.trace(a) <numpy.trace>`.
|
|
1442
|
+
|
|
1443
|
+
In *implicit mode*, the chosen subscripts are important
|
|
1444
|
+
since the axes of the output are reordered alphabetically. This
|
|
1445
|
+
means that ``np.einsum('ij', a)`` doesn't affect a 2D array, while
|
|
1446
|
+
``np.einsum('ji', a)`` takes its transpose. Additionally,
|
|
1447
|
+
``np.einsum('ij,jk', a, b)`` returns a matrix multiplication, while,
|
|
1448
|
+
``np.einsum('ij,jh', a, b)`` returns the transpose of the
|
|
1449
|
+
multiplication since subscript 'h' precedes subscript 'i'.
|
|
1450
|
+
|
|
1451
|
+
In *explicit mode* the output can be directly controlled by
|
|
1452
|
+
specifying output subscript labels. This requires the
|
|
1453
|
+
identifier '->' as well as the list of output subscript labels.
|
|
1454
|
+
This feature increases the flexibility of the function since
|
|
1455
|
+
summing can be disabled or forced when required. The call
|
|
1456
|
+
``np.einsum('i->', a)`` is like :py:func:`np.sum(a) <numpy.sum>`
|
|
1457
|
+
if ``a`` is a 1-D array, and ``np.einsum('ii->i', a)``
|
|
1458
|
+
is like :py:func:`np.diag(a) <numpy.diag>` if ``a`` is a square 2-D array.
|
|
1459
|
+
The difference is that `einsum` does not allow broadcasting by default.
|
|
1460
|
+
Additionally ``np.einsum('ij,jh->ih', a, b)`` directly specifies the
|
|
1461
|
+
order of the output subscript labels and therefore returns matrix
|
|
1462
|
+
multiplication, unlike the example above in implicit mode.
|
|
1463
|
+
|
|
1464
|
+
To enable and control broadcasting, use an ellipsis. Default
|
|
1465
|
+
NumPy-style broadcasting is done by adding an ellipsis
|
|
1466
|
+
to the left of each term, like ``np.einsum('...ii->...i', a)``.
|
|
1467
|
+
``np.einsum('...i->...', a)`` is like
|
|
1468
|
+
:py:func:`np.sum(a, axis=-1) <numpy.sum>` for array ``a`` of any shape.
|
|
1469
|
+
To take the trace along the first and last axes,
|
|
1470
|
+
you can do ``np.einsum('i...i', a)``, or to do a matrix-matrix
|
|
1471
|
+
product with the left-most indices instead of rightmost, one can do
|
|
1472
|
+
``np.einsum('ij...,jk...->ik...', a, b)``.
|
|
1473
|
+
|
|
1474
|
+
`einsum` also provides an alternative way to provide the subscripts and
|
|
1475
|
+
operands as ``einsum(op0, sublist0, op1, sublist1, ..., [sublistout])``.
|
|
1476
|
+
If the output shape is not provided in this format `einsum` will be
|
|
1477
|
+
calculated in implicit mode, otherwise it will be performed explicitly.
|
|
1478
|
+
The examples below have corresponding `einsum` calls with the two
|
|
1479
|
+
parameter methods.
|
|
1480
|
+
|
|
1481
|
+
Examples
|
|
1482
|
+
--------
|
|
1483
|
+
>>> a = np.arange(25).reshape(5, 5)
|
|
1484
|
+
>>> b = np.arange(5)
|
|
1485
|
+
>>> c = np.arange(6).reshape(2, 3)
|
|
1486
|
+
|
|
1487
|
+
Trace of a matrix:
|
|
1488
|
+
|
|
1489
|
+
>>> np.einsum("ii", a)
|
|
1490
|
+
60
|
|
1491
|
+
>>> np.einsum(a, [0, 0])
|
|
1492
|
+
60
|
|
1493
|
+
>>> np.trace(a)
|
|
1494
|
+
60
|
|
1495
|
+
|
|
1496
|
+
Extract the diagonal (requires explicit form):
|
|
1497
|
+
|
|
1498
|
+
>>> np.einsum("ii->i", a)
|
|
1499
|
+
array([ 0, 6, 12, 18, 24])
|
|
1500
|
+
>>> np.einsum(a, [0, 0], [0])
|
|
1501
|
+
array([ 0, 6, 12, 18, 24])
|
|
1502
|
+
>>> np.diag(a)
|
|
1503
|
+
array([ 0, 6, 12, 18, 24])
|
|
1504
|
+
|
|
1505
|
+
Sum over an axis (requires explicit form):
|
|
1506
|
+
|
|
1507
|
+
>>> np.einsum("ij->i", a)
|
|
1508
|
+
array([ 10, 35, 60, 85, 110])
|
|
1509
|
+
>>> np.einsum(a, [0, 1], [0])
|
|
1510
|
+
array([ 10, 35, 60, 85, 110])
|
|
1511
|
+
>>> np.sum(a, axis=1)
|
|
1512
|
+
array([ 10, 35, 60, 85, 110])
|
|
1513
|
+
|
|
1514
|
+
For higher dimensional arrays summing a single axis can be done
|
|
1515
|
+
with ellipsis:
|
|
1516
|
+
|
|
1517
|
+
>>> np.einsum("...j->...", a)
|
|
1518
|
+
array([ 10, 35, 60, 85, 110])
|
|
1519
|
+
>>> np.einsum(a, [Ellipsis, 1], [Ellipsis])
|
|
1520
|
+
array([ 10, 35, 60, 85, 110])
|
|
1521
|
+
|
|
1522
|
+
Compute a matrix transpose, or reorder any number of axes:
|
|
1523
|
+
|
|
1524
|
+
>>> np.einsum("ji", c)
|
|
1525
|
+
array([[0, 3],
|
|
1526
|
+
[1, 4],
|
|
1527
|
+
[2, 5]])
|
|
1528
|
+
>>> np.einsum("ij->ji", c)
|
|
1529
|
+
array([[0, 3],
|
|
1530
|
+
[1, 4],
|
|
1531
|
+
[2, 5]])
|
|
1532
|
+
>>> np.einsum(c, [1, 0])
|
|
1533
|
+
array([[0, 3],
|
|
1534
|
+
[1, 4],
|
|
1535
|
+
[2, 5]])
|
|
1536
|
+
>>> np.transpose(c)
|
|
1537
|
+
array([[0, 3],
|
|
1538
|
+
[1, 4],
|
|
1539
|
+
[2, 5]])
|
|
1540
|
+
|
|
1541
|
+
Vector inner products:
|
|
1542
|
+
|
|
1543
|
+
>>> np.einsum("i,i", b, b)
|
|
1544
|
+
30
|
|
1545
|
+
>>> np.einsum(b, [0], b, [0])
|
|
1546
|
+
30
|
|
1547
|
+
>>> np.inner(b, b)
|
|
1548
|
+
30
|
|
1549
|
+
|
|
1550
|
+
Matrix vector multiplication:
|
|
1551
|
+
|
|
1552
|
+
>>> np.einsum("ij,j", a, b)
|
|
1553
|
+
array([ 30, 80, 130, 180, 230])
|
|
1554
|
+
>>> np.einsum(a, [0, 1], b, [1])
|
|
1555
|
+
array([ 30, 80, 130, 180, 230])
|
|
1556
|
+
>>> np.dot(a, b)
|
|
1557
|
+
array([ 30, 80, 130, 180, 230])
|
|
1558
|
+
>>> np.einsum("...j,j", a, b)
|
|
1559
|
+
array([ 30, 80, 130, 180, 230])
|
|
1560
|
+
|
|
1561
|
+
Broadcasting and scalar multiplication:
|
|
1562
|
+
|
|
1563
|
+
>>> np.einsum("..., ...", 3, c)
|
|
1564
|
+
array([[ 0, 3, 6],
|
|
1565
|
+
[ 9, 12, 15]])
|
|
1566
|
+
>>> np.einsum(",ij", 3, c)
|
|
1567
|
+
array([[ 0, 3, 6],
|
|
1568
|
+
[ 9, 12, 15]])
|
|
1569
|
+
>>> np.einsum(3, [Ellipsis], c, [Ellipsis])
|
|
1570
|
+
array([[ 0, 3, 6],
|
|
1571
|
+
[ 9, 12, 15]])
|
|
1572
|
+
>>> np.multiply(3, c)
|
|
1573
|
+
array([[ 0, 3, 6],
|
|
1574
|
+
[ 9, 12, 15]])
|
|
1575
|
+
|
|
1576
|
+
Vector outer product:
|
|
1577
|
+
|
|
1578
|
+
>>> np.einsum("i,j", np.arange(2) + 1, b)
|
|
1579
|
+
array([[0, 1, 2, 3, 4],
|
|
1580
|
+
[0, 2, 4, 6, 8]])
|
|
1581
|
+
>>> np.einsum(np.arange(2) + 1, [0], b, [1])
|
|
1582
|
+
array([[0, 1, 2, 3, 4],
|
|
1583
|
+
[0, 2, 4, 6, 8]])
|
|
1584
|
+
>>> np.outer(np.arange(2) + 1, b)
|
|
1585
|
+
array([[0, 1, 2, 3, 4],
|
|
1586
|
+
[0, 2, 4, 6, 8]])
|
|
1587
|
+
|
|
1588
|
+
Tensor contraction:
|
|
1589
|
+
|
|
1590
|
+
>>> a = np.arange(60.0).reshape(3, 4, 5)
|
|
1591
|
+
>>> b = np.arange(24.0).reshape(4, 3, 2)
|
|
1592
|
+
>>> np.einsum("ijk,jil->kl", a, b)
|
|
1593
|
+
array([[4400., 4730.],
|
|
1594
|
+
[4532., 4874.],
|
|
1595
|
+
[4664., 5018.],
|
|
1596
|
+
[4796., 5162.],
|
|
1597
|
+
[4928., 5306.]])
|
|
1598
|
+
>>> np.einsum(a, [0, 1, 2], b, [1, 0, 3], [2, 3])
|
|
1599
|
+
array([[4400., 4730.],
|
|
1600
|
+
[4532., 4874.],
|
|
1601
|
+
[4664., 5018.],
|
|
1602
|
+
[4796., 5162.],
|
|
1603
|
+
[4928., 5306.]])
|
|
1604
|
+
>>> np.tensordot(a, b, axes=([1, 0], [0, 1]))
|
|
1605
|
+
array([[4400., 4730.],
|
|
1606
|
+
[4532., 4874.],
|
|
1607
|
+
[4664., 5018.],
|
|
1608
|
+
[4796., 5162.],
|
|
1609
|
+
[4928., 5306.]])
|
|
1610
|
+
|
|
1611
|
+
Example of ellipsis use:
|
|
1612
|
+
|
|
1613
|
+
>>> a = np.arange(6).reshape((3, 2))
|
|
1614
|
+
>>> b = np.arange(12).reshape((4, 3))
|
|
1615
|
+
>>> np.einsum("ki,jk->ij", a, b)
|
|
1616
|
+
array([[10, 28, 46, 64],
|
|
1617
|
+
[13, 40, 67, 94]])
|
|
1618
|
+
>>> np.einsum("ki,...k->i...", a, b)
|
|
1619
|
+
array([[10, 28, 46, 64],
|
|
1620
|
+
[13, 40, 67, 94]])
|
|
1621
|
+
>>> np.einsum("k...,jk", a, b)
|
|
1622
|
+
array([[10, 28, 46, 64],
|
|
1623
|
+
[13, 40, 67, 94]])
|
|
1624
|
+
"""
|
|
1625
|
+
import galley_jl_python
|
|
1626
|
+
|
|
1627
|
+
return einsum_impl(galley_jl_python, *args)
|
|
1628
|
+
|
|
1629
|
+
|
|
1630
|
+
def _is_scipy_sparse_obj(x):
|
|
1631
|
+
return hasattr(x, "__module__") and x.__module__.startswith("scipy.sparse")
|
|
1632
|
+
|
|
1633
|
+
|
|
1634
|
+
def _slice_plus_one(s: slice, size: int) -> range:
|
|
1635
|
+
step = s.step if s.step is not None else 1
|
|
1636
|
+
start_default = size if step < 0 else 1
|
|
1637
|
+
stop_default = 1 if step < 0 else size
|
|
1638
|
+
|
|
1639
|
+
if s.start is not None:
|
|
1640
|
+
start = normalize_axis_index(s.start, size) + 1 if s.start < size else size
|
|
1641
|
+
else:
|
|
1642
|
+
start = start_default
|
|
1643
|
+
|
|
1644
|
+
if s.stop is not None:
|
|
1645
|
+
stop_offset = 2 if step < 0 else 0
|
|
1646
|
+
stop = (
|
|
1647
|
+
normalize_axis_index(s.stop, size) + stop_offset if s.stop < size else size
|
|
1648
|
+
)
|
|
1649
|
+
else:
|
|
1650
|
+
stop = stop_default
|
|
1651
|
+
|
|
1652
|
+
if (start, stop, step) == (1, size, 1):
|
|
1653
|
+
return jl.Colon()
|
|
1654
|
+
|
|
1655
|
+
return jl.range(start=start, step=step, stop=stop)
|
|
1656
|
+
|
|
1657
|
+
|
|
1658
|
+
def _add_plus_one(key: tuple, shape: tuple[int, ...]) -> tuple:
|
|
1659
|
+
new_key = []
|
|
1660
|
+
sizes = iter(shape)
|
|
1661
|
+
for idx in key:
|
|
1662
|
+
if idx is None:
|
|
1663
|
+
new_key.append(jl.nothing)
|
|
1664
|
+
continue
|
|
1665
|
+
|
|
1666
|
+
size = next(sizes)
|
|
1667
|
+
if isinstance(idx, int):
|
|
1668
|
+
new_key.append(normalize_axis_index(idx, size) + 1)
|
|
1669
|
+
elif isinstance(idx, slice):
|
|
1670
|
+
new_key.append(_slice_plus_one(idx, size))
|
|
1671
|
+
elif isinstance(idx, list | np.ndarray | tuple):
|
|
1672
|
+
idx = normalize_axis_tuple(idx, size)
|
|
1673
|
+
new_key.append(jl.Vector([i + 1 for i in idx]))
|
|
1674
|
+
else:
|
|
1675
|
+
new_key.append(idx)
|
|
1676
|
+
|
|
1677
|
+
return tuple(new_key)
|
|
1678
|
+
|
|
1679
|
+
|
|
1680
|
+
def _expand_ellipsis(key: tuple, shape: tuple[int, ...]) -> tuple:
|
|
1681
|
+
ellipsis_pos = None
|
|
1682
|
+
key_without_ellipsis = []
|
|
1683
|
+
# first we need to find the ellipsis and confirm it's the only one
|
|
1684
|
+
for pos, idx in enumerate(key):
|
|
1685
|
+
if idx is Ellipsis:
|
|
1686
|
+
if ellipsis_pos is None:
|
|
1687
|
+
ellipsis_pos = pos
|
|
1688
|
+
else:
|
|
1689
|
+
raise IndexError("an index can only have a single ellipsis ('...')")
|
|
1690
|
+
else:
|
|
1691
|
+
key_without_ellipsis.append(idx)
|
|
1692
|
+
key = key_without_ellipsis
|
|
1693
|
+
|
|
1694
|
+
# then we expand ellipsis with a full range
|
|
1695
|
+
if ellipsis_pos is not None:
|
|
1696
|
+
n_missing_idxs = len(shape) - builtins.sum(1 for k in key if k is not None)
|
|
1697
|
+
key = key[:ellipsis_pos] + [slice(None)] * n_missing_idxs + key[ellipsis_pos:]
|
|
1698
|
+
|
|
1699
|
+
return tuple(key)
|
|
1700
|
+
|
|
1701
|
+
|
|
1702
|
+
def _add_missing_dims(key: tuple, shape: tuple[int, ...]) -> tuple:
|
|
1703
|
+
missing_dims = len(shape) - builtins.sum(1 for k in key if k is not None)
|
|
1704
|
+
return key + (slice(None),) * missing_dims
|
|
1705
|
+
|
|
1706
|
+
|
|
1707
|
+
def _process_lazy_indexing(key: tuple, ndim: int) -> tuple:
|
|
1708
|
+
new_key = ()
|
|
1709
|
+
ellipsis_found = False
|
|
1710
|
+
for idx in key:
|
|
1711
|
+
if idx == slice(None):
|
|
1712
|
+
new_key += (jl.Colon(),)
|
|
1713
|
+
elif idx is None:
|
|
1714
|
+
new_key += (jl.nothing,)
|
|
1715
|
+
elif idx is Ellipsis:
|
|
1716
|
+
num_of_colons = ndim - builtins.sum(1 for k in key if k is not None) + 1
|
|
1717
|
+
new_key += (jl.Colon(),) * num_of_colons
|
|
1718
|
+
if ellipsis_found:
|
|
1719
|
+
raise IndexError("an index can only have a single ellipsis ('...')")
|
|
1720
|
+
|
|
1721
|
+
ellipsis_found = True
|
|
1722
|
+
else:
|
|
1723
|
+
raise ValueError(f"Invalid lazy index member: {idx}")
|
|
1724
|
+
return new_key
|
|
1725
|
+
|
|
1726
|
+
|
|
1727
|
+
def _eq_scalars(x, y):
|
|
1728
|
+
if x is None or y is None:
|
|
1729
|
+
return x == y
|
|
1730
|
+
if jl.isnan(x) or jl.isnan(y):
|
|
1731
|
+
return jl.isnan(x) and jl.isnan(y)
|
|
1732
|
+
return x == y
|
|
1733
|
+
|
|
1734
|
+
|
|
1735
|
+
def _validate_device(device: Device) -> None:
|
|
1736
|
+
if device not in {"cpu", None}:
|
|
1737
|
+
raise ValueError(
|
|
1738
|
+
f'Device not understood. Only "cpu" is allowed, but received: {device}'
|
|
1739
|
+
)
|