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,850 @@
1
+ """Methods implementing J verbs.
2
+
3
+ Where possible, dyads are implemented as ufuncs. This equips the dyads with
4
+ efficient reduce, outer and accumulate methods over arrays.
5
+
6
+ Specifically where a dyadic application of a verb has left and right rank both 0,
7
+ this is equivalent to elementwise application of the verb to the arrays. This is
8
+ what ufuncs capture. For example, dyadic `+` is equivalent to `np.add` and dyadic
9
+ `*` is equivalent to `np.multiply`.
10
+
11
+ It is important that all implementations here share the same "rank" characteristics
12
+ as their J counterparts.
13
+ """
14
+
15
+ import itertools
16
+ import math
17
+ import random
18
+ from typing import Callable
19
+
20
+ import numpy as np
21
+ from jinx.errors import (
22
+ DomainError,
23
+ JIndexError,
24
+ JinxNotImplementedError,
25
+ LengthError,
26
+ ValenceError,
27
+ )
28
+ from jinx.execution.numpy.conversion import box_dtype
29
+ from jinx.execution.numpy.helpers import (
30
+ get_fill_value,
31
+ hash_box,
32
+ increase_ndim,
33
+ is_box,
34
+ is_same_array,
35
+ mark_ufunc_based,
36
+ maybe_pad_by_duplicating_atoms,
37
+ maybe_pad_with_fill_value,
38
+ )
39
+ from jinx.word_formation import form_words
40
+
41
+ np.seterr(divide="ignore")
42
+
43
+
44
+ def eq_monad(y: np.ndarray) -> np.ndarray:
45
+ nub = tildedot_monad(y)
46
+ result = []
47
+ for item in nub:
48
+ value = np.all(item == y, axis=tuple(range(1, y.ndim)))
49
+ result.append(value)
50
+ return np.asarray(result).astype(np.int64)
51
+
52
+
53
+ @mark_ufunc_based
54
+ def percent_monad(y: np.ndarray) -> np.ndarray:
55
+ """% monad: returns the reciprocal of the array."""
56
+ # N.B. np.reciprocal does not support integer types, use division instead.
57
+ return 1 / y
58
+
59
+
60
+ @mark_ufunc_based
61
+ def percentco_dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
62
+ return np.power(y, 1 / x)
63
+
64
+
65
+ def plusdot_monad(y: np.ndarray) -> np.ndarray:
66
+ """+. monad: returns real and imaginary parts of numbers."""
67
+ y = np.atleast_1d(y)
68
+ return np.concatenate([np.real(y), np.imag(y)], axis=-1)
69
+
70
+
71
+ @mark_ufunc_based
72
+ def plusco_monad(y: np.ndarray) -> np.ndarray:
73
+ """+: monad: double the values in the array."""
74
+ return 2 * y
75
+
76
+
77
+ @mark_ufunc_based
78
+ def minusdot_monad(y: np.ndarray) -> np.ndarray:
79
+ """-.: monad: returns 1 - y."""
80
+ return 1 - y
81
+
82
+
83
+ @mark_ufunc_based
84
+ def minusco_monad(y: np.ndarray) -> np.ndarray:
85
+ """-: monad: halve the values in the array."""
86
+ return y / 2
87
+
88
+
89
+ def minusco_dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
90
+ """-: dyad: match, returns true if x and y have same shape and values."""
91
+ is_equal = np.array_equal(x, y, equal_nan=True)
92
+ return np.asarray(is_equal)
93
+
94
+
95
+ @mark_ufunc_based
96
+ def plusco_dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
97
+ """+: dyad: not-or operation."""
98
+ # N.B. This is not the same as the J implementation which forbids values
99
+ # outside of 0 and 1.
100
+ return ~np.logical_or(x, y).astype(np.int64)
101
+
102
+
103
+ def stardot_monad(y: np.ndarray) -> np.ndarray:
104
+ """*. monad: convert x-y coordinates to r-theta coordinates."""
105
+ y = np.atleast_1d(y)
106
+ r = np.abs(y)
107
+ theta = np.angle(y)
108
+ return np.concatenate([r, theta])
109
+
110
+
111
+ @mark_ufunc_based
112
+ def starco_dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
113
+ """*: dyad: not-and operation."""
114
+ # N.B. This is not the same as the J implementation which forbids values
115
+ # outside of 0 and 1.
116
+ return ~np.logical_and(x, y).astype(np.int64)
117
+
118
+
119
+ @mark_ufunc_based
120
+ def hatdot_dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
121
+ """^. dyad: logarithm of y to the base x."""
122
+ return np.log(y) / np.log(x)
123
+
124
+
125
+ def lt_monad(y: np.ndarray) -> np.ndarray:
126
+ """< monad: box a noun."""
127
+ return np.array([(y,)], dtype=box_dtype).squeeze()
128
+
129
+
130
+ def gt_monad(y: np.ndarray) -> np.ndarray:
131
+ """> monad: open a boxed element or array of boxed elements."""
132
+ if not is_box(y):
133
+ return y
134
+ elements = [np.asarray(item[0]) for item in y.ravel().tolist()]
135
+ elements_padded = maybe_pad_with_fill_value(elements)
136
+ return np.asarray(elements_padded).squeeze()
137
+
138
+
139
+ @mark_ufunc_based
140
+ def ltco_monad(y: np.ndarray) -> np.ndarray:
141
+ """<: monad: decrements the array."""
142
+ return y - 1
143
+
144
+
145
+ @mark_ufunc_based
146
+ def gtco_monad(y: np.ndarray) -> np.ndarray:
147
+ """>: monad: increments the array."""
148
+ return y + 1
149
+
150
+
151
+ def comma_monad(y: np.ndarray) -> np.ndarray:
152
+ """, monad: returns the flattened array."""
153
+ y = np.atleast_1d(y)
154
+ return np.ravel(y)
155
+
156
+
157
+ def comma_dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
158
+ """, dyad: returns array containing the items of x followed by the items of y."""
159
+
160
+ x = np.atleast_1d(x)
161
+ y = np.atleast_1d(y)
162
+
163
+ dtype = np.promote_types(x.dtype, y.dtype)
164
+
165
+ if x.shape == (1,):
166
+ x = np.full_like(y[:1], x[0], dtype=dtype)
167
+ elif y.shape == (1,):
168
+ y = np.full_like(x[:1], y[0], dtype=dtype)
169
+ else:
170
+ trailing_dims = [
171
+ max(xs, ys)
172
+ for xs, ys in itertools.zip_longest(
173
+ reversed(x.shape), reversed(y.shape), fillvalue=1
174
+ )
175
+ ]
176
+ trailing_dims.reverse()
177
+ trailing_dims = trailing_dims[1:] # ignore dimension that we concatenate along
178
+
179
+ ndmin = max(x.ndim, y.ndim)
180
+ x = increase_ndim(x, ndmin)
181
+ y = increase_ndim(y, ndmin)
182
+
183
+ x = np.pad(
184
+ x,
185
+ [(0, 0)] + [(0, d - s) for s, d in zip(x.shape[1:], trailing_dims)],
186
+ constant_values=get_fill_value(x),
187
+ )
188
+ y = np.pad(
189
+ y,
190
+ [(0, 0)] + [(0, d - s) for s, d in zip(y.shape[1:], trailing_dims)],
191
+ constant_values=get_fill_value(y),
192
+ )
193
+
194
+ return np.concatenate([x, y], axis=0)
195
+
196
+
197
+ def commadot_monad(y: np.ndarray) -> np.ndarray:
198
+ """,. monad: ravel items."""
199
+ y = np.atleast_1d(y)
200
+ return y.reshape(y.shape[0], -1)
201
+
202
+
203
+ def commadot_dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
204
+ """,. dyad: join each item of x to each item of y."""
205
+ x = np.atleast_1d(x)
206
+ y = np.atleast_1d(y)
207
+
208
+ if x.shape == (1,):
209
+ x = np.repeat(x, y.shape[0], axis=0)
210
+
211
+ if y.shape == (1,):
212
+ y = np.repeat(y, x.shape[0], axis=0)
213
+
214
+ if len(x) != len(y):
215
+ raise LengthError(
216
+ f"executing dyad ,. shapes {x.shape} and {y.shape} have different numbers of items"
217
+ )
218
+
219
+ items = []
220
+ for x_item, y_item in zip(x, y, strict=True):
221
+ items.append(comma_dyad(x_item, y_item))
222
+
223
+ if len(items) == 1:
224
+ return np.asarray(items[0])
225
+
226
+ result = maybe_pad_with_fill_value(items)
227
+ return np.asarray(result)
228
+
229
+
230
+ def commaco_monad(y: np.ndarray) -> np.ndarray:
231
+ """,: monad: create array with rank 1 more than rank of y."""
232
+ if np.isscalar(y) or y.shape == ():
233
+ return np.array([y])
234
+ return y[np.newaxis, :]
235
+
236
+
237
+ def commaco_dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
238
+ """,: dyad: create a two item array from x and y."""
239
+ items = maybe_pad_by_duplicating_atoms([x, y], ignore_first_dim=False)
240
+ return np.asarray(items)
241
+
242
+
243
+ @mark_ufunc_based
244
+ def bar_dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
245
+ """| dyad: remainder when dividing y by x."""
246
+ x = np.atleast_1d(x)
247
+ y = np.atleast_1d(y)
248
+ # In J, '0 | y' is y, not 0.
249
+ result = np.where(x, np.mod(y, x), y)
250
+ if result.ndim == 1 and result.shape[0] == 1:
251
+ return result[0]
252
+ return result
253
+
254
+
255
+ def bardot_dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
256
+ """|. dyad: rotate the array."""
257
+ y = np.atleast_1d(y)
258
+ x = np.atleast_1d(x)
259
+ if x.shape[-1] > y.ndim:
260
+ raise ValueError(
261
+ f"length error, executing dyad |. (x has {x.shape[-1]} atoms but y only has {y.ndim} axes)"
262
+ )
263
+ return np.roll(y, -x, axis=tuple(range(x.shape[-1])))
264
+
265
+
266
+ def barco_dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
267
+ """|: dyad: rearrange the axes of the array."""
268
+ x = np.atleast_1d(x)
269
+ if len(x) > y.ndim:
270
+ raise JIndexError("|: x has more items than y has dimensions")
271
+ if any(item > y.ndim for item in x):
272
+ raise JIndexError("|: x has items greater than y has dimensions")
273
+ if len(set(x)) != len(x):
274
+ raise JIndexError("|: x contains a duplicate axis number")
275
+ first = []
276
+ for i in range(y.ndim):
277
+ if i not in x:
278
+ first.append(i)
279
+ return np.transpose(y, axes=first + x.tolist())
280
+
281
+
282
+ def tildedot_monad(y: np.ndarray) -> np.ndarray:
283
+ """~. monad: remove duplicates from a list."""
284
+ y = np.atleast_1d(y)
285
+
286
+ if is_box(y):
287
+ seen = set()
288
+ result = []
289
+ for item in y:
290
+ h = hash_box(item)
291
+ if h not in seen:
292
+ result.append(item if is_box(item) else (item[0],))
293
+ seen.add(h)
294
+ return np.array(result, dtype=box_dtype).squeeze()
295
+
296
+ uniq, idx = np.unique(y, return_index=True, axis=0)
297
+ return uniq[np.argsort(idx)]
298
+
299
+
300
+ def tildeco_monad(y: np.ndarray) -> np.ndarray:
301
+ """~: monad: nub sieve."""
302
+ y = np.atleast_1d(y)
303
+ _, idx = np.unique(y, return_index=True, axis=0)
304
+ result = np.zeros(y.shape[0], dtype=np.int64)
305
+ result[idx] = 1
306
+ return result
307
+
308
+
309
+ def dollar_monad(y: np.ndarray) -> np.ndarray:
310
+ """$ monad: returns the shape of the array."""
311
+ if isinstance(y, str):
312
+ return np.array([len(y)])
313
+ if np.isscalar(y) or y.shape == ():
314
+ # Differs from the J implementation which returns a missing value for shape of scalar.
315
+ return np.array(0)
316
+ return np.array(y.shape)
317
+
318
+
319
+ def dollar_dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
320
+ """$ dyad: create an array with a particular shape.
321
+
322
+ Does not support custom fill values at the moment.
323
+ Does not support INFINITY as an atom of x.
324
+ """
325
+ if np.isscalar(x) or x.shape == ():
326
+ if x < 0 or not np.issubdtype(x.dtype, np.integer):
327
+ raise DomainError(f"Invalid shape: {x}")
328
+
329
+ if np.isscalar(x) or x.shape == ():
330
+ x_shape = (np.squeeze(x),)
331
+ else:
332
+ x_shape = tuple(x)
333
+
334
+ if np.isscalar(y) or y.shape == ():
335
+ if is_box(y):
336
+ result = np.array([y] * np.prod(x_shape), dtype=box_dtype).reshape(x_shape)
337
+ else:
338
+ result = np.empty(x_shape, dtype=y.dtype)
339
+ result[:] = y
340
+ return result
341
+
342
+ output_shape = x_shape + y.shape[1:]
343
+ data = y.ravel()
344
+ repeat, fill = divmod(np.prod(output_shape), data.size)
345
+ result = np.concatenate([np.tile(data, repeat), data[:fill]]).reshape(output_shape)
346
+ return result
347
+
348
+
349
+ def idot_monad(y: np.ndarray) -> np.ndarray:
350
+ """i. monad: returns increasing/decreasing sequence of integer wrapperd to shape y."""
351
+ arr = np.atleast_1d(y)
352
+ if not np.issubdtype(y.dtype, np.integer):
353
+ raise DomainError("y has nonintegral value")
354
+ shape = abs(arr)
355
+ n = np.prod(shape)
356
+ axes_to_flip = np.where(arr < 0)[0]
357
+ result = np.arange(n).reshape(shape)
358
+ return np.flip(result, axes_to_flip)
359
+
360
+
361
+ def icapdot_monad(y: np.ndarray) -> np.ndarray:
362
+ """I. monad: return indexes of every 1 in the Boolean list y."""
363
+ arr = np.atleast_1d(y)
364
+ if not (np.issubdtype(y.dtype, np.integer) or np.issubdtype(y.dtype, np.bool_)):
365
+ raise DomainError("y has nonintegral value")
366
+
367
+ if np.any(arr < 0):
368
+ raise DomainError("y has negative values")
369
+
370
+ indexes = np.where(arr)[0]
371
+ nonzero = arr[indexes]
372
+ return np.repeat(indexes, nonzero)
373
+
374
+
375
+ def number_monad(y: np.ndarray) -> np.ndarray:
376
+ """# monad: count number of items in y."""
377
+ if isinstance(y, str):
378
+ return np.array(len(y))
379
+ if np.isscalar(y) or y.shape == ():
380
+ return np.array(1)
381
+ return np.array(y.shape[0])
382
+
383
+
384
+ def number_dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
385
+ """# dyad: copy items in y exactly x times."""
386
+ return np.repeat(y, x, axis=0)
387
+
388
+
389
+ def numberdot_monad(y: np.ndarray) -> np.ndarray:
390
+ """#. monad: return corresponding number of a binary numeral."""
391
+ y = np.atleast_1d(y)
392
+ weights = 2 ** np.arange(y.size, dtype=np.int64)[::-1]
393
+ return np.dot(y, weights)
394
+
395
+
396
+ def numberdot_dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
397
+ """#. dyad: generalizes #.y to bases other than 2 (including mixed bases)."""
398
+ x = np.atleast_1d(x)
399
+ y = np.atleast_1d(y)
400
+
401
+ if 1 < len(x) != len(y):
402
+ raise LengthError(
403
+ f"Error executing dyad #. shapes {len(x)} and {len(y)} do not conform"
404
+ )
405
+
406
+ if len(x) == 1:
407
+ x = np.full_like(y, x[0], dtype=np.int64)
408
+
409
+ weights = np.multiply.accumulate(x[1:][::-1])[::-1]
410
+ return np.dot(y[:-1], weights) + y[-1]
411
+
412
+
413
+ def numberco_monad(y: np.ndarray) -> np.ndarray:
414
+ """#: monad: return the binary expansion of y as a boolean list."""
415
+ y = np.atleast_1d(y)
416
+
417
+ if np.issubdtype(y.dtype, np.floating):
418
+ is_y_floating = True
419
+ floor_y = np.floor(y)
420
+ fractional_part = y - floor_y
421
+ y = floor_y.astype(np.int64)
422
+ else:
423
+ is_y_floating = False
424
+
425
+ if np.all(y == 0):
426
+ max_bits = 1
427
+ else:
428
+ max_bits = np.floor(np.log2(np.max(np.abs(y)))).astype(int) + 1
429
+
430
+ # Convert negative numbers to two's complement form.
431
+ # They become positive, and then the bits are inverted.
432
+ is_negative = y < 0
433
+ y[is_negative] = ~y[is_negative]
434
+
435
+ remainders = []
436
+
437
+ for _ in range(max_bits):
438
+ bits = y % 2
439
+ y >>= 1
440
+ remainders.append(bits)
441
+
442
+ result = np.stack(remainders[::-1], axis=-1)
443
+ result[is_negative] = 1 - result[is_negative]
444
+
445
+ if is_y_floating:
446
+ result = result.astype(np.float64)
447
+ result[..., -1] += fractional_part
448
+
449
+ if result.ndim > 1 and result.shape[0] == 1:
450
+ result = result.reshape(result.shape[1:])
451
+
452
+ return result
453
+
454
+
455
+ @mark_ufunc_based
456
+ def squarelf_monad(y: np.ndarray) -> np.ndarray:
457
+ """[ monad: returns the whole array."""
458
+ return y
459
+
460
+
461
+ @mark_ufunc_based
462
+ def squarelf_dyad(x: np.ndarray, _: np.ndarray) -> np.ndarray:
463
+ """[ dyad: returns x."""
464
+ return x
465
+
466
+
467
+ squarerf_monad = squarelf_monad
468
+
469
+
470
+ def squarerfco_monad(y: np.ndarray) -> np.ndarray:
471
+ """[: monad: raise a ValenceError."""
472
+ raise ValenceError("[: must be part of a capped fork.")
473
+
474
+
475
+ def squarerfco_dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
476
+ """[: dyad: raise a ValenceError."""
477
+ raise ValenceError("[: must be part of a capped fork.")
478
+
479
+
480
+ def squarerf_dyad(_: np.ndarray, y: np.ndarray) -> np.ndarray:
481
+ """] dyad: returns y."""
482
+ return y
483
+
484
+
485
+ def slashco_monad(y: np.ndarray) -> np.ndarray:
486
+ """/: monad: permutation that sorts y in increasing order."""
487
+ y = np.atleast_1d(y)
488
+ if y.ndim == 1:
489
+ return np.argsort(y, stable=True)
490
+
491
+ # Ravelled items of y are sorted lexicographically.
492
+ y = y.reshape(len(y), -1)
493
+ return np.lexsort(np.rot90(y))
494
+
495
+
496
+ def slashco_dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
497
+ """/: monad: sort y in increasing order."""
498
+ y = np.atleast_1d(y)
499
+
500
+ if is_same_array(x, y):
501
+ # This handles /:~
502
+ if x.ndim == 1:
503
+ return np.sort(y, kind="stable")
504
+ idx = slashco_monad(y)
505
+ return y[idx]
506
+
507
+ idx = slashco_monad(y)
508
+ return x[idx]
509
+
510
+
511
+ def bslashco_monad(y: np.ndarray) -> np.ndarray:
512
+ r"""\: monad: permutation that sorts y in decreasing order."""
513
+ y = np.atleast_1d(y)
514
+ if y.ndim == 1:
515
+ # Stable sort in decreasing order.
516
+ # np.argsort(a)[::-1] on its own does not work as the indices of
517
+ # equal elements will appear reversed in the result.
518
+ return len(y) - 1 - np.argsort(y[::-1], kind="stable")[::-1]
519
+
520
+ y = y.reshape(len(y), -1)
521
+ return len(y) - 1 - np.lexsort(np.rot90(y[::-1]))[::-1]
522
+
523
+
524
+ def bslashco_dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
525
+ r"""\: dyad: sort y in decreasing order."""
526
+ y = np.atleast_1d(y)
527
+
528
+ if is_same_array(x, y):
529
+ # This handles \:~
530
+ if x.ndim == 1:
531
+ # Not technically correct (see comment on monad above), but
532
+ # good enough for now.
533
+ return np.flip(np.sort(y, kind="stable"))
534
+ idx = bslashco_monad(y)
535
+ return y[idx]
536
+
537
+ idx = bslashco_monad(y)
538
+ return x[idx]
539
+
540
+
541
+ def curlylf_dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
542
+ """{ dyad: select item with index x from array y."""
543
+ y = np.atleast_1d(y)
544
+
545
+ if not is_box(x):
546
+ if not np.issubdtype(x.dtype, np.integer):
547
+ raise DomainError("{ dyad: x must be an integer")
548
+ try:
549
+ return y[x]
550
+ except IndexError:
551
+ raise JIndexError(
552
+ f"{{ dyad: x {x} is out of bounds for y with shape {y.shape}"
553
+ ) from None
554
+
555
+ x_inner = gt_monad(x)
556
+
557
+ if len(x_inner) > y.ndim:
558
+ raise LengthError(
559
+ f"{{ dyad: selector is overlong x has length {len(x_inner)} but rank of y is only {y.ndim}"
560
+ )
561
+
562
+ if not is_box(x_inner):
563
+ if not np.issubdtype(x_inner.dtype, np.integer):
564
+ raise DomainError("{ dyad: indices must be integers")
565
+ try:
566
+ return y[tuple(x_inner)]
567
+ except IndexError:
568
+ raise JIndexError(
569
+ f"{{ dyad: x {x_inner} is out of bounds for y with shape {y.shape}"
570
+ ) from None
571
+
572
+ x_inner_inner = gt_monad(x_inner)
573
+
574
+ if len(x_inner_inner) > y.ndim:
575
+ raise LengthError(
576
+ f"{{ dyad: selector is overlong x has length {len(x_inner_inner)} but rank of y is only {y.ndim}"
577
+ )
578
+
579
+ if not all(np.issubdtype(item.dtype, np.integer) for item in x_inner_inner):
580
+ raise DomainError("{ dyad: indices must be integers")
581
+
582
+ try:
583
+ return y[np.ix_(*x_inner_inner)]
584
+ except IndexError:
585
+ raise JIndexError(
586
+ f"{{ dyad: x {x_inner} is out of bounds for y with shape {y.shape}"
587
+ ) from None
588
+
589
+
590
+ def curlylfdot_monad(y: np.ndarray) -> np.ndarray:
591
+ """{. monad: returns the first item of y."""
592
+ y = np.atleast_1d(y)
593
+ if y.size == 0:
594
+ return np.array([])
595
+ return y[0]
596
+
597
+
598
+ def curlylfdot_dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
599
+ """{. dyad: the leading x items of y."""
600
+ x = np.atleast_1d(x)
601
+ y = np.atleast_1d(y)
602
+
603
+ if len(x) > y.ndim:
604
+ raise LengthError(f"x has {len(x)} atoms but y has only {y.ndim} axes")
605
+
606
+ padding = []
607
+ slices = []
608
+
609
+ for dim, take in enumerate(x):
610
+ if take == 0:
611
+ raise JinxNotImplementedError(
612
+ "{. dyad: Dimension with 0 items is not supported"
613
+ )
614
+ elif take > y.shape[dim]:
615
+ padding.append((0, take - y.shape[dim]))
616
+ slices.append(slice(None))
617
+ elif take < -y.shape[dim]:
618
+ padding.append((-take - y.shape[dim], 0))
619
+ slices.append(slice(None))
620
+ elif take < 0:
621
+ padding.append((0, 0))
622
+ slices.append(slice(y.shape[dim] + take, None))
623
+ else:
624
+ padding.append((0, 0))
625
+ slices.append(slice(0, take))
626
+
627
+ if len(x) < y.ndim:
628
+ padding += [(0, 0)] * (y.ndim - len(x))
629
+
630
+ result = y[tuple(slices)]
631
+ result = np.pad(result, padding, mode="constant", constant_values=get_fill_value(y))
632
+ return result
633
+
634
+
635
+ def curlyrtdot_monad(y: np.ndarray) -> np.ndarray:
636
+ """}. monad: drop leading item from y."""
637
+ y = np.atleast_1d(y)
638
+ if y.size == 0:
639
+ return np.array([])
640
+ return y[1:]
641
+
642
+
643
+ def curlyrtdot_dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
644
+ """}. monad: drop leading x items from y."""
645
+ x = np.atleast_1d(x)
646
+ y = np.atleast_1d(y)
647
+
648
+ if len(x) > y.ndim:
649
+ raise LengthError(f"x has {len(x)} atoms but y has only {y.ndim} axes")
650
+
651
+ if y.size == 0:
652
+ return np.array([])
653
+
654
+ padding = []
655
+ slices = []
656
+
657
+ for dim, drop in enumerate(x):
658
+ if drop == 0:
659
+ padding.append((0, 0))
660
+ slices.append(slice(None))
661
+ elif drop > y.shape[dim]:
662
+ raise JinxNotImplementedError("}. dyad: empty dimension is not supported")
663
+ elif drop < -y.shape[dim]:
664
+ raise JinxNotImplementedError("}. dyad: empty dimension is not supported")
665
+ elif drop < 0:
666
+ padding.append((0, 0))
667
+ slices.append(slice(None, y.shape[dim] + drop))
668
+ else:
669
+ padding.append((0, 0))
670
+ slices.append(slice(drop, None))
671
+
672
+ if len(x) < y.ndim:
673
+ padding += [(0, 0)] * (y.ndim - len(x))
674
+
675
+ result = y[tuple(slices)]
676
+ result = np.pad(result, padding, mode="constant", constant_values=get_fill_value(y))
677
+ return result
678
+
679
+
680
+ def curlylfco_monad(y: np.ndarray) -> np.ndarray:
681
+ """{: monad: return last item of y."""
682
+ if np.isscalar(y) or y.shape == ():
683
+ return np.asarray(y)
684
+ return y[-1]
685
+
686
+
687
+ def curlyrtco_monad(y: np.ndarray) -> np.ndarray:
688
+ """}: monad: drop last item of y."""
689
+ y = np.atleast_1d(y)
690
+ return y[:-1] if y.size > 0 else np.array([], dtype=y.dtype)
691
+
692
+
693
+ def bang_monad(y: np.ndarray) -> np.ndarray:
694
+ """! monad: returns y factorial (and more generally the gamma function of 1+y)."""
695
+ if isinstance(y, int) or np.issubdtype(y.dtype, np.integer) and y >= 0:
696
+ return np.asarray(math.factorial(y))
697
+ return np.asarray(math.gamma(1 + y))
698
+
699
+
700
+ def bang_dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
701
+ """! dyad: returns y-Combinations-x."""
702
+ if (isinstance(y, int) or np.issubdtype(y.dtype, np.integer) and y >= 0) and (
703
+ isinstance(x, int) or np.issubdtype(x.dtype, np.integer) and x >= 0
704
+ ):
705
+ return np.asarray(math.comb(y, x))
706
+ x_ = bang_monad(x)
707
+ y_ = bang_monad(y)
708
+ x_y = bang_monad(y - x)
709
+ return np.asarray(y_ / x_ / x_y)
710
+
711
+
712
+ def semi_monad(y: np.ndarray) -> np.ndarray:
713
+ """; monad: remove one level of boxing from a noun."""
714
+ if not is_box(y):
715
+ return y
716
+
717
+ y = y.ravel()
718
+ items = [item[0] for item in y.tolist()]
719
+
720
+ is_all_boxed = all(is_box(item) for item in items)
721
+ is_all_not_boxed = all(not is_box(item) for item in items)
722
+ if not is_all_boxed and not is_all_not_boxed:
723
+ raise DomainError("Contents are incompatible: numeric and boxed")
724
+
725
+ items = maybe_pad_by_duplicating_atoms(items, ignore_first_dim=True)
726
+ return np.concatenate(items, axis=0)
727
+
728
+
729
+ def semi_dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
730
+ """; dyad: link two nouns into a box."""
731
+ x = lt_monad(x)
732
+ if not is_box(y):
733
+ y = lt_monad(y)
734
+
735
+ x = np.atleast_1d(x)
736
+ y = np.atleast_1d(y)
737
+ return np.concatenate([x, y], axis=0)
738
+
739
+
740
+ def semico_monad(y: np.ndarray) -> np.ndarray:
741
+ """;: monad: partition string into boxed words according to J's rules for word formation."""
742
+ if not np.issubdtype(y.dtype, np.str_):
743
+ raise DomainError(";: monad: y must be a string")
744
+ string = "".join(y)
745
+ words = [word.value for word in form_words(string)]
746
+ return np.array(words, dtype=box_dtype)
747
+
748
+
749
+ def query_monad(y: np.ndarray) -> np.ndarray:
750
+ """? monad: generates a random number uniformly distributed in a range determined by integer y."""
751
+ if not np.issubdtype(y.dtype, np.integer) or y < 0:
752
+ raise DomainError("y must be a positive integer")
753
+
754
+ if y == 0:
755
+ result = random.random()
756
+
757
+ elif y == 1:
758
+ result = 0
759
+
760
+ elif y == 2:
761
+ result = random.choice([0, 1])
762
+
763
+ else:
764
+ result = random.randint(0, int(y))
765
+
766
+ return np.asarray(result)
767
+
768
+
769
+ def query_dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
770
+ """? dyad: select x items at random from list i.y."""
771
+ if not np.issubdtype(y.dtype, np.integer) or y < 0:
772
+ raise DomainError("y must be a positive integer")
773
+
774
+ if not np.issubdtype(x.dtype, np.integer) or x < 0:
775
+ raise DomainError("x must be a positive integer")
776
+
777
+ if x == 0:
778
+ # This should return "empty" but Jinx does not have a concept of empty.
779
+ return np.asarray(0)
780
+
781
+ rng = np.random.default_rng()
782
+ return rng.choice(y, size=x, replace=False)
783
+
784
+
785
+ MonadT = Callable[[np.ndarray], np.ndarray]
786
+ DyadT = Callable[[np.ndarray, np.ndarray], np.ndarray]
787
+
788
+
789
+ def cast_bool_to_int(func: np.ufunc) -> DyadT:
790
+ @mark_ufunc_based
791
+ def func_(x: np.ndarray, y: np.ndarray) -> np.ndarray:
792
+ result = func(x, y)
793
+ return result.view(np.int8)
794
+
795
+ return func_
796
+
797
+
798
+ # Use NotImplemented for monads or dyads that have not yet been implemented in Jinx.
799
+ # Use None for monadic or dyadic valences of the verb do not exist in J.
800
+ VERB_MAP: dict[str, tuple[MonadT | None, DyadT | None]] = {
801
+ # VERB: (MONAD, DYAD)
802
+ "EQ": (eq_monad, cast_bool_to_int(np.equal)),
803
+ "MINUS": (np.negative, np.subtract),
804
+ "MINUSDOT": (minusdot_monad, NotImplemented),
805
+ "MINUSCO": (minusco_monad, minusco_dyad),
806
+ "PLUS": (np.conj, np.add),
807
+ "PLUSDOT": (plusdot_monad, np.gcd),
808
+ "PLUSCO": (plusco_monad, plusco_dyad),
809
+ "STAR": (np.sign, np.multiply),
810
+ "STARDOT": (stardot_monad, np.lcm),
811
+ "STARCO": (np.square, starco_dyad),
812
+ "PERCENT": (percent_monad, np.divide),
813
+ "PERCENTCO": (np.sqrt, percentco_dyad),
814
+ "HAT": (np.exp, np.power),
815
+ "HATDOT": (np.log, hatdot_dyad),
816
+ "DOLLAR": (dollar_monad, dollar_dyad),
817
+ "LT": (lt_monad, cast_bool_to_int(np.less)),
818
+ "LTDOT": (np.floor, np.minimum),
819
+ "LTCO": (ltco_monad, cast_bool_to_int(np.less_equal)),
820
+ "GT": (gt_monad, cast_bool_to_int(np.greater)),
821
+ "GTDOT": (np.ceil, np.maximum),
822
+ "GTCO": (gtco_monad, cast_bool_to_int(np.greater_equal)),
823
+ "IDOT": (idot_monad, NotImplemented),
824
+ "ICAPDOT": (icapdot_monad, NotImplemented),
825
+ "TILDEDOT": (tildedot_monad, None),
826
+ "TILDECO": (tildeco_monad, cast_bool_to_int(np.not_equal)),
827
+ "COMMA": (comma_monad, comma_dyad),
828
+ "COMMADOT": (commadot_monad, commadot_dyad),
829
+ "COMMACO": (commaco_monad, commaco_dyad),
830
+ "BAR": (np.abs, bar_dyad),
831
+ "BARDOT": (np.flipud, bardot_dyad),
832
+ "BARCO": (np.transpose, barco_dyad),
833
+ "NUMBER": (number_monad, number_dyad),
834
+ "NUMBERDOT": (numberdot_monad, numberdot_dyad),
835
+ "NUMBERCO": (numberco_monad, NotImplemented),
836
+ "SQUARELF": (squarelf_monad, squarelf_dyad),
837
+ "SQUARERF": (squarerf_monad, squarerf_dyad),
838
+ "SQUARERFCO": (squarerfco_monad, squarerfco_dyad),
839
+ "SLASHCO": (slashco_monad, slashco_dyad),
840
+ "BSLASHCO": (bslashco_monad, bslashco_dyad),
841
+ "BANG": (bang_monad, bang_dyad),
842
+ "CURLYLF": (NotImplemented, curlylf_dyad),
843
+ "CURLYLFDOT": (curlylfdot_monad, curlylfdot_dyad),
844
+ "CURLYRTDOT": (curlyrtdot_monad, curlyrtdot_dyad),
845
+ "CURLYLFCO": (curlylfco_monad, None),
846
+ "CURLYRTCO": (curlyrtco_monad, None),
847
+ "SEMI": (semi_monad, semi_dyad),
848
+ "SEMICO": (semico_monad, NotImplemented),
849
+ "QUERY": (query_monad, query_dyad),
850
+ }