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,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__
|