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.
- jinx/__init__.py +0 -0
- jinx/errors.py +41 -0
- jinx/execution/executor.py +60 -0
- jinx/execution/jax/__init__.py +45 -0
- jinx/execution/jax/adverbs.py +91 -0
- jinx/execution/jax/application.py +231 -0
- jinx/execution/jax/verbs.py +90 -0
- jinx/execution/numpy/__init__.py +29 -0
- jinx/execution/numpy/adverbs.py +334 -0
- jinx/execution/numpy/application.py +343 -0
- jinx/execution/numpy/conjunctions.py +437 -0
- jinx/execution/numpy/conversion.py +62 -0
- jinx/execution/numpy/helpers.py +158 -0
- jinx/execution/numpy/printing.py +179 -0
- jinx/execution/numpy/verbs.py +850 -0
- jinx/primitives.py +490 -0
- jinx/shell.py +68 -0
- jinx/vocabulary.py +181 -0
- jinx/word_evaluation.py +375 -0
- jinx/word_formation.py +229 -0
- jinx/word_spelling.py +118 -0
- jjinx-0.0.1.dist-info/METADATA +148 -0
- jjinx-0.0.1.dist-info/RECORD +27 -0
- jjinx-0.0.1.dist-info/WHEEL +5 -0
- jjinx-0.0.1.dist-info/entry_points.txt +2 -0
- jjinx-0.0.1.dist-info/licenses/LICENSE +21 -0
- jjinx-0.0.1.dist-info/top_level.txt +1 -0
@@ -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
|
+
}
|