jjinx 0.0.1__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,437 @@
1
+ """Methods implementing J conjunctions."""
2
+
3
+ import dataclasses
4
+ import functools
5
+
6
+ import numpy as np
7
+ from jinx.errors import DomainError, JinxNotImplementedError, ValenceError
8
+ from jinx.execution.numpy.application import (
9
+ _apply_dyad,
10
+ _apply_monad,
11
+ fill_and_assemble,
12
+ get_rank,
13
+ split_into_cells,
14
+ )
15
+ from jinx.execution.numpy.conversion import box_dtype, ndarray_or_scalar_to_noun
16
+ from jinx.execution.numpy.helpers import (
17
+ is_box,
18
+ maybe_pad_with_fill_value,
19
+ maybe_parenthesise_verb_spelling,
20
+ )
21
+ from jinx.vocabulary import Dyad, Monad, Noun, Verb
22
+
23
+ INFINITY = float("inf")
24
+
25
+
26
+ def _modify_rank(
27
+ verb: Verb[np.ndarray], rank: np.ndarray | int | float
28
+ ) -> Verb[np.ndarray]:
29
+ rank = np.atleast_1d(rank)
30
+ if np.issubdtype(rank.dtype, np.floating):
31
+ if not np.isinf(rank).any():
32
+ raise DomainError(f"Rank must be an integer or infinity, got {rank.dtype}")
33
+
34
+ elif not np.issubdtype(rank.dtype, np.integer):
35
+ raise DomainError(f"Rank must be an integer or infinity, got {rank.dtype}")
36
+
37
+ if rank.size > 3 or rank.ndim > 1:
38
+ raise DomainError(
39
+ f"Rank must be a scalar or 1D array of length <= 3, got {rank.ndim}D array with shape {rank.shape}"
40
+ )
41
+
42
+ rank_list = [int(r) if not np.isinf(r) else INFINITY for r in rank.tolist()]
43
+ verb_spelling = spelling = maybe_parenthesise_verb_spelling(verb.spelling)
44
+
45
+ if len(rank_list) == 1:
46
+ monad_rank = left_rank = right_rank = rank_list[0]
47
+ spelling = f'{verb_spelling}"{rank_list[0]}'
48
+
49
+ elif len(rank_list) == 2:
50
+ left_rank, right_rank = rank_list
51
+ monad_rank = right_rank
52
+ spelling = f'{verb_spelling}"{left_rank} {right_rank}'
53
+
54
+ else:
55
+ monad_rank, left_rank, right_rank = rank_list
56
+ spelling = f'{verb_spelling}"{monad_rank} {left_rank} {right_rank}'
57
+
58
+ if verb.monad:
59
+ monad = dataclasses.replace(verb.monad, rank=monad_rank, function=verb)
60
+ else:
61
+ monad = None
62
+
63
+ if verb.dyad:
64
+ dyad = dataclasses.replace(
65
+ verb.dyad,
66
+ left_rank=left_rank,
67
+ right_rank=right_rank,
68
+ function=verb,
69
+ )
70
+ else:
71
+ dyad = None
72
+
73
+ return dataclasses.replace(
74
+ verb,
75
+ spelling=spelling,
76
+ name=spelling,
77
+ monad=monad,
78
+ dyad=dyad,
79
+ )
80
+
81
+
82
+ def rank_conjunction(
83
+ verb: Verb[np.ndarray], noun: Noun[np.ndarray]
84
+ ) -> Verb[np.ndarray]:
85
+ rank = np.atleast_1d(noun.implementation).tolist()
86
+ return _modify_rank(verb, rank)
87
+
88
+
89
+ def at_conjunction(u: Verb[np.ndarray], v: Verb[np.ndarray]) -> Verb[np.ndarray]:
90
+ """@ conjunction: compose verbs u and v, with u applied using the rank of v.
91
+
92
+ This means v is applied first, then u is applied to each cell of the result of v
93
+ *before* any padding or assembly is done.
94
+
95
+ Once u has been applied to each v-cell, the results are padded and assembled.
96
+ """
97
+
98
+ def _monad(y: np.ndarray) -> np.ndarray:
99
+ rank = get_rank(v.monad.rank, y.ndim) # type: ignore[union-attr]
100
+ v_cell_array = split_into_cells(y, rank)
101
+ v_cells = [_apply_monad(v, cell) for cell in v_cell_array.cells]
102
+ u_cells = [_apply_monad(u, cell) for cell in v_cells]
103
+ padded_cells = maybe_pad_with_fill_value(u_cells)
104
+ return fill_and_assemble(padded_cells, v_cell_array.frame_shape)
105
+
106
+ def _dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
107
+ left_rank = get_rank(v.dyad.left_rank, x.ndim) # type: ignore[union-attr]
108
+ right_rank = get_rank(v.dyad.right_rank, y.ndim) # type: ignore[union-attr]
109
+
110
+ v_cell_left_array = split_into_cells(x, left_rank)
111
+ v_cell_right_array = split_into_cells(y, right_rank)
112
+
113
+ v_cells = [
114
+ _apply_dyad(v, lx, ry)
115
+ for lx, ry in zip(
116
+ v_cell_left_array.cells, v_cell_right_array.cells, strict=True
117
+ )
118
+ ]
119
+ u_cells = [_apply_monad(u, cell) for cell in v_cells]
120
+
121
+ padded_cells = maybe_pad_with_fill_value(u_cells)
122
+ return fill_and_assemble(padded_cells, v_cell_right_array.frame_shape)
123
+
124
+ u_spelling = maybe_parenthesise_verb_spelling(u.spelling)
125
+ v_spelling = maybe_parenthesise_verb_spelling(v.spelling)
126
+
127
+ if v.dyad is None:
128
+ dyad = None
129
+ else:
130
+ dyad = Dyad(
131
+ name=f"{u_spelling}@{v_spelling}",
132
+ left_rank=v.dyad.left_rank,
133
+ right_rank=v.dyad.right_rank,
134
+ function=_dyad,
135
+ )
136
+
137
+ return Verb[np.ndarray](
138
+ name=f"{u_spelling}@{v_spelling}",
139
+ spelling=f"{u_spelling}@{v_spelling}",
140
+ monad=Monad(
141
+ name=f"{u_spelling}@{v_spelling}",
142
+ rank=v.monad.rank, # type: ignore[union-attr]
143
+ function=_monad,
144
+ ),
145
+ dyad=dyad,
146
+ )
147
+
148
+
149
+ def atco_conjunction(u: Verb[np.ndarray], v: Verb[np.ndarray]) -> Verb[np.ndarray]:
150
+ """@: conjunction: compose verbs u and v, with the rank of the new verb as infinity."""
151
+
152
+ def monad(y: np.ndarray) -> np.ndarray:
153
+ a = _apply_monad(v, y)
154
+ b = _apply_monad(u, a)
155
+ return b
156
+
157
+ def dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
158
+ a = _apply_dyad(v, x, y)
159
+ b = _apply_monad(u, a)
160
+ return b
161
+
162
+ u_spelling = maybe_parenthesise_verb_spelling(u.spelling)
163
+ v_spelling = maybe_parenthesise_verb_spelling(v.spelling)
164
+
165
+ return Verb[np.ndarray](
166
+ name=f"{u_spelling}@:{v_spelling}",
167
+ spelling=f"{u_spelling}@:{v_spelling}",
168
+ monad=Monad(
169
+ name=f"{u_spelling}@:{v_spelling}",
170
+ rank=INFINITY,
171
+ function=monad,
172
+ ),
173
+ dyad=Dyad(
174
+ name=f"{u_spelling}@:{v_spelling}",
175
+ left_rank=INFINITY,
176
+ right_rank=INFINITY,
177
+ function=dyad,
178
+ ),
179
+ )
180
+
181
+
182
+ def ampm_conjunction(
183
+ left: Verb[np.ndarray] | Noun[np.ndarray],
184
+ right: Verb[np.ndarray] | Noun[np.ndarray],
185
+ ) -> Verb[np.ndarray]:
186
+ """& conjunction: make a monad from a dyad by providing the left or right noun argument,
187
+ or compose two verbs."""
188
+ if isinstance(left, Noun) and isinstance(right, Verb):
189
+ if isinstance(right.dyad.function, Verb): # type: ignore[union-attr]
190
+ function = functools.partial(_apply_dyad, right, left.implementation)
191
+ else:
192
+ function = functools.partial(right.dyad.function, left.implementation) # type: ignore[union-attr]
193
+ verb_spelling = maybe_parenthesise_verb_spelling(right.spelling)
194
+ spelling = f"{left.implementation}&{verb_spelling}"
195
+ monad = Monad(name=spelling, rank=INFINITY, function=function)
196
+ dyad = None
197
+
198
+ elif isinstance(left, Verb) and isinstance(right, Noun):
199
+ # functools.partial cannot be used to apply to right argument of ufuncs
200
+ # as they do not accept kwargs, so we need to wrap the function.
201
+ def _wrapper(x: np.ndarray, y: np.ndarray) -> np.ndarray:
202
+ return _apply_dyad(left, x, y)
203
+
204
+ function = functools.partial(_wrapper, y=right.implementation)
205
+ verb_spelling = maybe_parenthesise_verb_spelling(left.spelling)
206
+ spelling = f"{verb_spelling}&{right.implementation}"
207
+ monad = Monad(name=spelling, rank=INFINITY, function=function)
208
+ dyad = None
209
+
210
+ elif isinstance(left, Verb) and isinstance(right, Verb):
211
+ # Compose u&v, with the new verb having the right verb's monadic rank.
212
+ def monad_(y: np.ndarray) -> np.ndarray:
213
+ a = _apply_monad(right, y)
214
+ b = _apply_monad(left, a)
215
+ return b
216
+
217
+ def dyad_(x: np.ndarray, y: np.ndarray) -> np.ndarray:
218
+ ry = _apply_monad(right, y)
219
+ rx = _apply_monad(right, x)
220
+ return _apply_dyad(left, rx, ry)
221
+
222
+ left_spelling = maybe_parenthesise_verb_spelling(left.spelling)
223
+ right_spelling = maybe_parenthesise_verb_spelling(right.spelling)
224
+ spelling = f"{left_spelling}&{right_spelling}"
225
+
226
+ monad = Monad(name=spelling, rank=right.monad.rank, function=monad_) # type: ignore[union-attr]
227
+ dyad = Dyad(
228
+ name=spelling,
229
+ left_rank=right.monad.rank, # type: ignore[union-attr]
230
+ right_rank=right.monad.rank, # type: ignore[union-attr]
231
+ function=dyad_,
232
+ )
233
+
234
+ return Verb(name=spelling, spelling=spelling, monad=monad, dyad=dyad)
235
+
236
+
237
+ def ampdotco_conjunction(u: Verb[np.ndarray], v: Verb[np.ndarray]) -> Verb[np.ndarray]:
238
+ """&.: conjunction: execute v on the arguments, then u on the result, then
239
+ the inverse v of on that result."""
240
+
241
+ if v.obverse is None:
242
+ raise DomainError(f"{v.spelling} has no obverse")
243
+
244
+ def _monad(y: np.ndarray) -> np.ndarray:
245
+ vy = _apply_monad(v, y)
246
+ uvy = _apply_monad(u, vy)
247
+ return _apply_monad(v.obverse, uvy) # type: ignore[arg-type]
248
+
249
+ def _dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
250
+ vy = _apply_monad(v, y)
251
+ vx = _apply_monad(v, x)
252
+ uvy = _apply_dyad(u, vx, vy)
253
+ return _apply_monad(v.obverse, uvy) # type: ignore[arg-type]
254
+
255
+ v_spelling = maybe_parenthesise_verb_spelling(v.spelling)
256
+ u_spelling = maybe_parenthesise_verb_spelling(u.spelling)
257
+
258
+ return Verb[np.ndarray](
259
+ name=f"{u_spelling}&.:{v_spelling}",
260
+ spelling=f"{u_spelling}&.:{v_spelling}",
261
+ monad=Monad(
262
+ name=f"{u_spelling}&.:{v_spelling}",
263
+ rank=INFINITY,
264
+ function=_monad,
265
+ ),
266
+ dyad=Dyad(
267
+ name=f"{u_spelling}&.:{v_spelling}",
268
+ left_rank=INFINITY,
269
+ right_rank=INFINITY,
270
+ function=_dyad,
271
+ ),
272
+ )
273
+
274
+
275
+ def ampdot_conjunction(u: Verb[np.ndarray], v: Verb[np.ndarray]) -> Verb[np.ndarray]:
276
+ """&. conjunction: u&.v is equivalent to (u&.:v)"mv , where mv is the monadic rank of v."""
277
+ if v.monad is None:
278
+ raise ValenceError(f"{v.spelling} has no monadic form")
279
+ verb = ampdotco_conjunction(u, v)
280
+ return _modify_rank(verb, v.monad.rank)
281
+
282
+
283
+ def hatco_conjunction(
284
+ u: Verb[np.ndarray], noun_or_verb: Noun[np.ndarray] | Verb[np.ndarray]
285
+ ) -> Verb[np.ndarray]:
286
+ """^: conjunction: power of verb."""
287
+
288
+ if isinstance(noun_or_verb, Verb):
289
+ raise JinxNotImplementedError("^: conjunction with verb is not yet implemented")
290
+
291
+ if isinstance(noun_or_verb, Noun):
292
+ exponent: Noun = noun_or_verb
293
+
294
+ if exponent.implementation.size == 0:
295
+ raise DomainError(
296
+ f"^: requires non-empty exponent, got {exponent.implementation}"
297
+ )
298
+
299
+ # Special case (^:0) is ]
300
+ if (
301
+ np.isscalar(exponent.implementation) or exponent.implementation.shape == ()
302
+ ) and exponent == 0:
303
+ return Verb(
304
+ name="SQUARELF",
305
+ spelling="]",
306
+ monad=Monad(
307
+ name="SQUARELF",
308
+ rank=INFINITY,
309
+ function=lambda y: y,
310
+ ),
311
+ dyad=Dyad(
312
+ name="SQUARELF",
313
+ left_rank=INFINITY,
314
+ right_rank=INFINITY,
315
+ function=lambda x, y: y,
316
+ ),
317
+ obverse="]",
318
+ )
319
+
320
+ # Special case (^:1) is u
321
+ if (
322
+ np.isscalar(exponent.implementation) or exponent.implementation.shape == ()
323
+ ) and exponent == 1:
324
+ return u
325
+
326
+ if np.isinf(exponent.implementation).any():
327
+ raise JinxNotImplementedError(
328
+ "^: with infinite exponent is not yet implemented"
329
+ )
330
+
331
+ if not np.issubdtype(exponent.implementation.dtype, np.integer):
332
+ raise DomainError(
333
+ f"^: requires integer exponent, got {exponent.implementation}"
334
+ )
335
+
336
+ def monad(y: np.ndarray) -> np.ndarray:
337
+ result = []
338
+ for atom in exponent.implementation.ravel().tolist():
339
+ if atom == 0:
340
+ result.append(y)
341
+ continue
342
+ elif atom > 0:
343
+ verb = u
344
+ exp = atom
345
+ else: # atom < 0:
346
+ if not isinstance(u.obverse, Verb):
347
+ raise DomainError(f"{u.spelling} has no obverse")
348
+ verb = u.obverse
349
+ exp = -atom
350
+
351
+ atom_result = y
352
+ for _ in range(exp):
353
+ atom_result = _apply_monad(verb, atom_result)
354
+
355
+ result.append(atom_result)
356
+
357
+ result = maybe_pad_with_fill_value(result, fill_value=0)
358
+ array = np.asarray(result)
359
+ return array.reshape(exponent.implementation.shape + result[0].shape)
360
+
361
+ def dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
362
+ result = []
363
+ for atom in exponent.implementation.ravel().tolist():
364
+ if atom == 0:
365
+ result.append(y)
366
+ continue
367
+ elif atom > 0:
368
+ verb = u
369
+ exp = atom
370
+ else: # atom < 0:
371
+ if not isinstance(u.obverse, Verb):
372
+ raise DomainError(f"{u.spelling} has no obverse")
373
+ verb = u.obverse
374
+ exp = -atom
375
+
376
+ atom_result = y
377
+ for _ in range(exp):
378
+ atom_result = _apply_dyad(verb, x, atom_result)
379
+
380
+ result.append(atom_result)
381
+
382
+ result = maybe_pad_with_fill_value(result, fill_value=0)
383
+ array = np.asarray(result)
384
+ return array.reshape(exponent.implementation.shape + result[0].shape)
385
+
386
+ u_spelling = maybe_parenthesise_verb_spelling(u.spelling)
387
+
388
+ return Verb(
389
+ name=f"{u_spelling}^:{exponent.implementation}",
390
+ spelling=f"{u_spelling}^:{exponent.implementation}",
391
+ monad=Monad(
392
+ name=f"{u_spelling}^:{exponent.implementation}",
393
+ rank=INFINITY,
394
+ function=monad,
395
+ ),
396
+ dyad=Dyad(
397
+ name=f"{u_spelling}^:{exponent.implementation}",
398
+ left_rank=INFINITY,
399
+ right_rank=INFINITY,
400
+ function=dyad,
401
+ ),
402
+ )
403
+
404
+
405
+ def grave_conjunction(
406
+ left: Verb[np.ndarray] | Noun[np.ndarray],
407
+ right: Verb[np.ndarray] | Noun[np.ndarray],
408
+ ) -> Noun[np.ndarray]:
409
+ """` conjunction: tie."""
410
+ if isinstance(left, Verb):
411
+ left_boxed = np.array([(left,)], dtype=box_dtype)
412
+ elif isinstance(left, Noun) and is_box(left.implementation):
413
+ left_boxed = np.atleast_1d(left.implementation)
414
+ else:
415
+ raise DomainError("executing conj ` (left argument not boxed or verb)")
416
+
417
+ if isinstance(right, Verb):
418
+ right_boxed = np.array([(right,)], dtype=box_dtype)
419
+ elif isinstance(right, Noun) and is_box(right.implementation):
420
+ right_boxed = np.atleast_1d(right.implementation)
421
+ else:
422
+ raise DomainError("executing conj ` (right argument not boxed or verb)")
423
+
424
+ array = np.concatenate([left_boxed, right_boxed], axis=0, dtype=box_dtype)
425
+ return ndarray_or_scalar_to_noun(array)
426
+
427
+
428
+ CONJUNCTION_MAP = {
429
+ "RANK": rank_conjunction,
430
+ "AT": at_conjunction,
431
+ "ATCO": atco_conjunction,
432
+ "AMPM": ampm_conjunction,
433
+ "AMPDOT": ampdot_conjunction,
434
+ "AMPDOTCO": ampdotco_conjunction,
435
+ "HATCO": hatco_conjunction,
436
+ "GRAVE": grave_conjunction,
437
+ }
@@ -0,0 +1,62 @@
1
+ """Methods for converting between J Nouns and NumPy arrays."""
2
+
3
+ import numpy as np
4
+ from jinx.vocabulary import DataType, Noun
5
+
6
+ # Define a structured dtype for boxes, which can hold any object.
7
+ #
8
+ # The alternative of using np.object directly (the 'O' dtype) is problematic for a
9
+ # couple of reasons.
10
+ #
11
+ # Firstly we need to add metadata to the dtype to indicate that it is a box
12
+ # because we may also want to use np.object for other purposes (e.g. a rational
13
+ # number dtype). Not all NumPy operations preserve the dtype metadata however
14
+ # (e.g. np.concatenate), so we would need to patch the metadata back in.
15
+ #
16
+ # Secondly, np.object presents issues when detecting array sizes and concatenating
17
+ # boxed arrays. E.g. with the comma_dyad implementation that works correct for non-boxed
18
+ # arrays, '(<1),(<2 3),(<4)' created a 2D array not a 1D array.
19
+ #
20
+ # Using a structured dtype allows us to side-step these issues at the small expense
21
+ # of making it more difficult to insert and extract data from the box.
22
+ box_dtype = np.dtype([("content", "O")])
23
+
24
+
25
+ DATATYPE_TO_NP_MAP = {
26
+ DataType.Integer: np.int64,
27
+ DataType.Float: np.float64,
28
+ DataType.Byte: np.str_,
29
+ DataType.Box: box_dtype,
30
+ }
31
+
32
+
33
+ def convert_noun_to_numpy_array(noun: Noun[np.ndarray]) -> np.ndarray:
34
+ dtype = DATATYPE_TO_NP_MAP[noun.data_type]
35
+ if len(noun.data) == 1:
36
+ # A scalar (ndim == 0) is returned for single element arrays.
37
+ return np.array(noun.data[0], dtype=dtype) # type: ignore[call-overload]
38
+ return np.array(noun.data, dtype=dtype) # type: ignore[call-overload]
39
+
40
+
41
+ def ensure_noun_implementation(noun: Noun[np.ndarray]) -> None:
42
+ if noun.implementation is None:
43
+ noun.implementation = convert_noun_to_numpy_array(noun)
44
+
45
+
46
+ def infer_data_type(data: np.ndarray) -> DataType:
47
+ dtype = data.dtype
48
+ if np.issubdtype(dtype, np.integer) or np.issubdtype(dtype, np.bool_):
49
+ return DataType.Integer
50
+ if np.issubdtype(dtype, np.floating):
51
+ return DataType.Float
52
+ if np.issubdtype(dtype, np.character):
53
+ return DataType.Byte
54
+ if dtype == box_dtype:
55
+ return DataType.Box
56
+
57
+ raise NotImplementedError(f"Cannot handle NumPy dtype: {dtype}")
58
+
59
+
60
+ def ndarray_or_scalar_to_noun(data: np.ndarray) -> Noun[np.ndarray]:
61
+ data_type = infer_data_type(data)
62
+ return Noun[np.ndarray](data_type=data_type, implementation=data)
@@ -0,0 +1,158 @@
1
+ """Helper methods for manipulating arrays."""
2
+
3
+ import itertools
4
+ from typing import Any
5
+
6
+ import numpy as np
7
+ from jinx.execution.numpy.conversion import box_dtype
8
+
9
+
10
+ def get_fill_value(array: np.ndarray) -> int | str | np.ndarray:
11
+ """Get the fill value for an array."""
12
+ if np.issubdtype(array.dtype, np.number):
13
+ return 0
14
+ elif np.issubdtype(array.dtype, np.str_):
15
+ return " "
16
+ elif is_box(array):
17
+ return np.array([], dtype=box_dtype).squeeze()
18
+ raise NotImplementedError(f"Fill value for dtype {array.dtype} is not known.")
19
+
20
+
21
+ def maybe_pad_with_fill_value(
22
+ arrays: list[np.ndarray],
23
+ fill_value: Any = None,
24
+ ) -> list[np.ndarray]:
25
+ """Pad arrays to the same shape with a fill value."""
26
+ shapes = [arr.shape for arr in arrays]
27
+ if len(set(shapes)) == 1:
28
+ return arrays
29
+
30
+ dims = [max(dim) for dim in itertools.zip_longest(*shapes, fillvalue=1)]
31
+ padded_arrays = []
32
+
33
+ for arr in arrays:
34
+ if arr.shape == () or np.isscalar(arr):
35
+ arr = np.atleast_1d(arr)
36
+
37
+ pad_widths = [(0, dim - shape) for shape, dim in zip(arr.shape, dims)]
38
+ fill_value = fill_value if fill_value is not None else get_fill_value(arr)
39
+ padded_array = np.pad(
40
+ arr, pad_widths, mode="constant", constant_values=fill_value
41
+ )
42
+ padded_arrays.append(padded_array)
43
+
44
+ return padded_arrays
45
+
46
+
47
+ def maybe_pad_by_duplicating_atoms(
48
+ arrays: list[np.ndarray],
49
+ fill_value: Any = None,
50
+ ignore_first_dim: bool = True,
51
+ ) -> list[np.ndarray]:
52
+ """Pad arrays to the same shape, duplicating atoms to fill the required shape.
53
+
54
+ Fill values are used to pad arrays of larger shapes.
55
+ """
56
+ is_atom = [np.isscalar(arr) or arr.ndim == 0 for arr in arrays]
57
+ arrays = [np.atleast_1d(arr) for arr in arrays]
58
+
59
+ ndim = max(arr.ndim for arr in arrays)
60
+ if ndim == 1:
61
+ ignore_first_dim = False
62
+
63
+ reversed_shapes = [arr.shape[::-1] for arr in arrays]
64
+
65
+ trailing_dims = [
66
+ max(shape) for shape in itertools.zip_longest(*reversed_shapes, fillvalue=1)
67
+ ]
68
+ trailing_dims.reverse()
69
+
70
+ if ignore_first_dim:
71
+ trailing_dims = trailing_dims[1:] or trailing_dims
72
+
73
+ padded_arrays = []
74
+
75
+ for arr, is_atom_ in zip(arrays, is_atom):
76
+ if is_atom_:
77
+ padded = np.full(trailing_dims, arr, dtype=arr.dtype)
78
+
79
+ else:
80
+ arr = increase_ndim(arr, ndim)
81
+
82
+ if ignore_first_dim:
83
+ padding = [(0, 0)] + [
84
+ (0, d - s) for s, d in zip(arr.shape[1:], trailing_dims)
85
+ ]
86
+ else:
87
+ padding = [(0, d - s) for s, d in zip(arr.shape, trailing_dims)]
88
+
89
+ fill_value = fill_value if fill_value is not None else get_fill_value(arr)
90
+ padded = np.pad(arr, padding, constant_values=fill_value)
91
+
92
+ padded_arrays.append(padded)
93
+
94
+ padded_arrays = [increase_ndim(arr, ndim) for arr in padded_arrays]
95
+ return padded_arrays
96
+
97
+
98
+ def maybe_parenthesise_verb_spelling(spelling: str) -> str:
99
+ if spelling.startswith("(") and spelling.endswith(")"):
100
+ return spelling
101
+ return f"({spelling})" if " " in spelling else spelling
102
+
103
+
104
+ def increase_ndim(y: np.ndarray, ndim: int) -> np.ndarray:
105
+ idx = (np.newaxis,) * (ndim - y.ndim) + (slice(None),)
106
+ return y[idx]
107
+
108
+
109
+ def is_box(obj: Any) -> bool:
110
+ return getattr(obj, "dtype", None) == box_dtype
111
+
112
+
113
+ def hash_box(array: np.ndarray, level: int = 0) -> int:
114
+ """Compute a hash value for a box array."""
115
+ if not is_box(array):
116
+ raise ValueError("Array must be of box dtype.")
117
+
118
+ val = 3331
119
+ for item in array:
120
+ if is_box(item):
121
+ val = (val * 31 + level) % (2**64)
122
+ val ^= hash_box(item, level + 1)
123
+ elif isinstance(item, np.ndarray):
124
+ val ^= hash(item.tobytes())
125
+ else:
126
+ val ^= hash(item)
127
+ return val
128
+
129
+
130
+ def is_ufunc(func: Any) -> bool:
131
+ return isinstance(func, np.ufunc) or hasattr(func, "ufunc")
132
+
133
+
134
+ def mark_ufunc_based[T](function: T) -> T:
135
+ """Mark a function as a ufunc-based function.
136
+
137
+ This is used to identify functions that are typically composed of ufuncs
138
+ and can be applied directly to NumPy arrays by the verb-application methods.
139
+
140
+ This greatly speeds up application of some verbs.
141
+ """
142
+ function._is_ufunc_based = True # type: ignore[attr-defined]
143
+ return function
144
+
145
+
146
+ def is_ufunc_based(function: Any) -> bool:
147
+ """Check if a function is a ufunc-based function."""
148
+ return getattr(function, "_is_ufunc_based", False)
149
+
150
+
151
+ def is_same_array(x: np.ndarray, y: np.ndarray) -> bool:
152
+ """Check if two arrays are the same, even if `x is y` is `False` avoiding
153
+ comparison of the array values.
154
+
155
+ The arrays are the same if they have the same memory address, shape, strides
156
+ and dtype.
157
+ """
158
+ return x.__array_interface__ == y.__array_interface__