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.
@@ -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
+ )