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,334 @@
|
|
1
|
+
"""Methods implementing J adverbs."""
|
2
|
+
|
3
|
+
import dataclasses
|
4
|
+
import functools
|
5
|
+
from typing import Callable
|
6
|
+
|
7
|
+
import numpy as np
|
8
|
+
from jinx.errors import DomainError, JinxNotImplementedError, LengthError, ValenceError
|
9
|
+
from jinx.execution.numpy.application import _apply_dyad, _apply_monad
|
10
|
+
from jinx.execution.numpy.helpers import (
|
11
|
+
get_fill_value,
|
12
|
+
is_box,
|
13
|
+
is_ufunc,
|
14
|
+
maybe_pad_with_fill_value,
|
15
|
+
maybe_parenthesise_verb_spelling,
|
16
|
+
)
|
17
|
+
from jinx.vocabulary import Dyad, Monad, Verb
|
18
|
+
|
19
|
+
INFINITY = float("inf")
|
20
|
+
|
21
|
+
|
22
|
+
def slash_adverb(verb: Verb[np.ndarray]) -> Verb[np.ndarray]:
|
23
|
+
if verb.dyad is None or verb.dyad.function is None:
|
24
|
+
# Note: this differs from J which still allows the adverb to be applied
|
25
|
+
# to a verb, but may raise an error when the new verb is applied to a noun
|
26
|
+
# and the verb has no dyadic valence.
|
27
|
+
raise ValenceError(f"Verb {verb.spelling} has no dyadic valence.")
|
28
|
+
|
29
|
+
if is_ufunc(verb.dyad.function) and verb.dyad.is_commutative:
|
30
|
+
f: np.ufunc = verb.dyad.function # type: ignore[assignment]
|
31
|
+
monad = f.reduce
|
32
|
+
dyad = f.outer
|
33
|
+
|
34
|
+
else:
|
35
|
+
# Slow path: dyad is not a ufunc.
|
36
|
+
# The function is either callable, in which cases it is applied directly,
|
37
|
+
# or a Verb object that needs to be applied indirectly with _apply_dyad().
|
38
|
+
if isinstance(verb.dyad.function, Verb):
|
39
|
+
func = functools.partial(_apply_dyad, verb) # type: ignore[assignment]
|
40
|
+
else:
|
41
|
+
func = verb.dyad.function # type: ignore[assignment]
|
42
|
+
|
43
|
+
def _dyad_arg_swap(x: np.ndarray, y: np.ndarray) -> np.ndarray:
|
44
|
+
return func(y, x)
|
45
|
+
|
46
|
+
def _reduce(y: np.ndarray) -> np.ndarray:
|
47
|
+
y = np.atleast_1d(y)
|
48
|
+
y = np.flip(y, axis=0)
|
49
|
+
return functools.reduce(_dyad_arg_swap, y)
|
50
|
+
|
51
|
+
def _outer(x: np.ndarray, y: np.ndarray) -> np.ndarray:
|
52
|
+
# We have already checked that verb.dyad is not None, so this is safe.
|
53
|
+
verb_slash = _modify_rank(verb, np.array([verb.dyad.left_rank, INFINITY])) # type: ignore[union-attr]
|
54
|
+
return _apply_dyad(verb_slash, x, y)
|
55
|
+
|
56
|
+
monad = _reduce
|
57
|
+
dyad = _outer
|
58
|
+
|
59
|
+
spelling = maybe_parenthesise_verb_spelling(verb.spelling)
|
60
|
+
spelling = f"{verb.spelling}/"
|
61
|
+
|
62
|
+
return Verb[np.ndarray](
|
63
|
+
name=spelling,
|
64
|
+
spelling=spelling,
|
65
|
+
monad=Monad(name=spelling, rank=INFINITY, function=monad),
|
66
|
+
dyad=Dyad(
|
67
|
+
name=spelling, left_rank=INFINITY, right_rank=INFINITY, function=dyad
|
68
|
+
),
|
69
|
+
)
|
70
|
+
|
71
|
+
|
72
|
+
def bslash_adverb(verb: Verb[np.ndarray]) -> Verb[np.ndarray]:
|
73
|
+
# Common cases that have a straightforward optimisation.
|
74
|
+
SPECIAL_MONAD = {
|
75
|
+
"+/": np.add.accumulate,
|
76
|
+
"*/": np.multiply.accumulate,
|
77
|
+
"<./": np.minimum.accumulate,
|
78
|
+
">./": np.maximum.accumulate,
|
79
|
+
}
|
80
|
+
|
81
|
+
if verb.spelling in SPECIAL_MONAD:
|
82
|
+
monad_ = SPECIAL_MONAD[verb.spelling]
|
83
|
+
|
84
|
+
else:
|
85
|
+
|
86
|
+
def monad_(y: np.ndarray) -> np.ndarray: # type: ignore[misc]
|
87
|
+
y = np.atleast_1d(y)
|
88
|
+
result = []
|
89
|
+
for i in range(1, len(y) + 1):
|
90
|
+
result.append(_apply_monad(verb, y[:i]))
|
91
|
+
result = maybe_pad_with_fill_value(result, fill_value=get_fill_value(y))
|
92
|
+
return np.asarray(result)
|
93
|
+
|
94
|
+
def dyad_(x: np.ndarray, y: np.ndarray) -> np.ndarray:
|
95
|
+
if not np.issubdtype(x.dtype, np.integer):
|
96
|
+
raise DomainError(f"x has nonintegral value ({x})")
|
97
|
+
y = np.atleast_1d(y)
|
98
|
+
if x == 0:
|
99
|
+
return np.zeros(len(y) + 1, dtype=np.int64)
|
100
|
+
if x == 1 or x == -1:
|
101
|
+
windows = y
|
102
|
+
elif x > 0:
|
103
|
+
# Overlapping windows
|
104
|
+
windows = np.array([y[i : i + x] for i in range(len(y) - x + 1)])
|
105
|
+
else:
|
106
|
+
# Non-overlapping windows
|
107
|
+
windows = np.array([y[i : i - x] for i in range(0, len(y), -x)])
|
108
|
+
|
109
|
+
result = []
|
110
|
+
for window in windows:
|
111
|
+
result.append(_apply_monad(verb, window))
|
112
|
+
result = maybe_pad_with_fill_value(result, fill_value=get_fill_value(y))
|
113
|
+
return np.asarray(result)
|
114
|
+
|
115
|
+
spelling = maybe_parenthesise_verb_spelling(verb.spelling)
|
116
|
+
spelling = f"{spelling}\\"
|
117
|
+
|
118
|
+
return Verb(
|
119
|
+
name=spelling,
|
120
|
+
spelling=spelling,
|
121
|
+
monad=Monad(name=spelling, rank=INFINITY, function=monad_),
|
122
|
+
dyad=Dyad(name=spelling, left_rank=0, right_rank=INFINITY, function=dyad_),
|
123
|
+
)
|
124
|
+
|
125
|
+
|
126
|
+
def bslashdot_adverb(verb: Verb[np.ndarray]) -> Verb[np.ndarray]:
|
127
|
+
SPECIAL_MONAD = {
|
128
|
+
"+/": lambda x: np.add.accumulate(x[::-1])[::-1],
|
129
|
+
"*/": lambda x: np.multiply.accumulate(x[::-1])[::-1],
|
130
|
+
"<./": lambda x: np.minimum.accumulate(x[::-1])[::-1],
|
131
|
+
">./": lambda x: np.maximum.accumulate(x[::-1])[::-1],
|
132
|
+
}
|
133
|
+
|
134
|
+
if verb.spelling in SPECIAL_MONAD:
|
135
|
+
monad_ = SPECIAL_MONAD[verb.spelling]
|
136
|
+
else:
|
137
|
+
|
138
|
+
def monad_(y: np.ndarray) -> np.ndarray:
|
139
|
+
y = np.atleast_1d(y)
|
140
|
+
result = []
|
141
|
+
for i in range(len(y)):
|
142
|
+
result.append(_apply_monad(verb, y[i:]))
|
143
|
+
result = maybe_pad_with_fill_value(result, fill_value=get_fill_value(y))
|
144
|
+
return np.asarray(result)
|
145
|
+
|
146
|
+
def dyad_(x: np.ndarray, y: np.ndarray) -> np.ndarray:
|
147
|
+
if not np.issubdtype(x.dtype, np.integer):
|
148
|
+
raise DomainError(f"x has nonintegral value ({x})")
|
149
|
+
y = np.atleast_1d(y)
|
150
|
+
if x == 0:
|
151
|
+
return y
|
152
|
+
elif x > 0:
|
153
|
+
# Overlapping windows
|
154
|
+
windows = [
|
155
|
+
np.concatenate([y[:i], y[i + x :]], axis=0)
|
156
|
+
for i in range(len(y) - x + 1)
|
157
|
+
]
|
158
|
+
else:
|
159
|
+
# Non-overlapping windows
|
160
|
+
windows = [
|
161
|
+
np.concatenate([y[:i], y[i - x :]], axis=0)
|
162
|
+
for i in range(0, len(y), -x)
|
163
|
+
]
|
164
|
+
|
165
|
+
result = []
|
166
|
+
for window in windows:
|
167
|
+
result.append(_apply_monad(verb, window))
|
168
|
+
result = maybe_pad_with_fill_value(result, fill_value=get_fill_value(y))
|
169
|
+
return np.asarray(result)
|
170
|
+
|
171
|
+
spelling = maybe_parenthesise_verb_spelling(verb.spelling)
|
172
|
+
spelling = f"{verb.spelling}\\."
|
173
|
+
|
174
|
+
return Verb(
|
175
|
+
name=spelling,
|
176
|
+
spelling=spelling,
|
177
|
+
monad=Monad(name=spelling, rank=INFINITY, function=monad_),
|
178
|
+
dyad=Dyad(name=spelling, left_rank=0, right_rank=INFINITY, function=dyad_),
|
179
|
+
)
|
180
|
+
|
181
|
+
|
182
|
+
def tilde_adverb(verb: Verb[np.ndarray]) -> Verb[np.ndarray]:
|
183
|
+
if verb.dyad is None or verb.dyad.function is None:
|
184
|
+
# Note: this differs from J which still allows the adverb to be applied
|
185
|
+
# to a verb, but may raise an error when the new verb is applied to a noun
|
186
|
+
# and the verb has no dyadic valence.
|
187
|
+
raise ValenceError(f"Verb {verb.spelling} has no dyadic valence.")
|
188
|
+
|
189
|
+
def monad(y: np.ndarray) -> np.ndarray:
|
190
|
+
# replicate argument and apply verb dyadically
|
191
|
+
return _apply_dyad(verb, y, y)
|
192
|
+
|
193
|
+
def dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
|
194
|
+
# swap the arguments and apply verb dyadically
|
195
|
+
return _apply_dyad(verb, y, x)
|
196
|
+
|
197
|
+
spelling = maybe_parenthesise_verb_spelling(verb.spelling)
|
198
|
+
spelling = f"{verb.spelling}~"
|
199
|
+
|
200
|
+
return Verb[np.ndarray](
|
201
|
+
name=spelling,
|
202
|
+
spelling=spelling,
|
203
|
+
monad=Monad(name=spelling, rank=INFINITY, function=monad),
|
204
|
+
dyad=Dyad(
|
205
|
+
name=spelling,
|
206
|
+
left_rank=verb.dyad.right_rank,
|
207
|
+
right_rank=verb.dyad.left_rank,
|
208
|
+
function=dyad,
|
209
|
+
),
|
210
|
+
)
|
211
|
+
|
212
|
+
|
213
|
+
def _modify_rank(verb: Verb, rank: np.ndarray | int | float) -> Verb:
|
214
|
+
rank = np.atleast_1d(rank)
|
215
|
+
if np.issubdtype(rank.dtype, np.floating):
|
216
|
+
if not np.isinf(rank).any():
|
217
|
+
raise DomainError(f"Rank must be an integer or infinity, got {rank.dtype}")
|
218
|
+
|
219
|
+
elif not np.issubdtype(rank.dtype, np.integer):
|
220
|
+
raise DomainError(f"Rank must be an integer or infinity, got {rank.dtype}")
|
221
|
+
|
222
|
+
if rank.size > 3 or rank.ndim > 1:
|
223
|
+
raise DomainError(
|
224
|
+
f"Rank must be a scalar or 1D array of length <= 3, got {rank.ndim}D array with shape {rank.shape}"
|
225
|
+
)
|
226
|
+
|
227
|
+
rank_list = [int(r) if not np.isinf(r) else INFINITY for r in rank.tolist()]
|
228
|
+
verb_spelling = spelling = maybe_parenthesise_verb_spelling(verb.spelling)
|
229
|
+
|
230
|
+
if len(rank_list) == 1:
|
231
|
+
monad_rank = left_rank = right_rank = rank_list[0]
|
232
|
+
spelling = f'{verb_spelling}"{rank_list[0]}'
|
233
|
+
|
234
|
+
elif len(rank_list) == 2:
|
235
|
+
left_rank, right_rank = rank_list
|
236
|
+
monad_rank = right_rank
|
237
|
+
spelling = f'{verb_spelling}"{left_rank} {right_rank}'
|
238
|
+
|
239
|
+
else:
|
240
|
+
monad_rank, left_rank, right_rank = rank_list
|
241
|
+
spelling = f'{verb_spelling}"{monad_rank} {left_rank} {right_rank}'
|
242
|
+
|
243
|
+
if verb.monad:
|
244
|
+
monad = dataclasses.replace(verb.monad, rank=monad_rank, function=verb)
|
245
|
+
else:
|
246
|
+
monad = None
|
247
|
+
|
248
|
+
if verb.dyad:
|
249
|
+
dyad = dataclasses.replace(
|
250
|
+
verb.dyad,
|
251
|
+
left_rank=left_rank,
|
252
|
+
right_rank=right_rank,
|
253
|
+
function=verb,
|
254
|
+
)
|
255
|
+
else:
|
256
|
+
dyad = None
|
257
|
+
|
258
|
+
return dataclasses.replace(
|
259
|
+
verb,
|
260
|
+
spelling=spelling,
|
261
|
+
name=spelling,
|
262
|
+
monad=monad,
|
263
|
+
dyad=dyad,
|
264
|
+
)
|
265
|
+
|
266
|
+
|
267
|
+
def slashdot_adverb(verb: Verb) -> Verb:
|
268
|
+
def monad(y: np.ndarray) -> np.ndarray:
|
269
|
+
y = np.atleast_1d(y)
|
270
|
+
|
271
|
+
if y.ndim == 1:
|
272
|
+
result = [_apply_monad(verb, item) for item in y]
|
273
|
+
elif y.ndim <= 3:
|
274
|
+
result = []
|
275
|
+
for offset in range(1 - y.shape[0], y.shape[1]):
|
276
|
+
item = np.diagonal(y[::-1], offset).T[::-1]
|
277
|
+
result.append(_apply_monad(verb, item))
|
278
|
+
else:
|
279
|
+
JinxNotImplementedError(
|
280
|
+
f"Monad {verb.spelling} dooes not yet support array rank > 3."
|
281
|
+
)
|
282
|
+
|
283
|
+
result = maybe_pad_with_fill_value(result, fill_value=get_fill_value(y))
|
284
|
+
return np.asarray(result)
|
285
|
+
|
286
|
+
def dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
|
287
|
+
x = np.atleast_1d(x)
|
288
|
+
y = np.atleast_1d(y)
|
289
|
+
|
290
|
+
if len(x) != len(y):
|
291
|
+
raise LengthError(
|
292
|
+
f"x and y must have the same length, got {len(x)} and {len(y)}"
|
293
|
+
)
|
294
|
+
|
295
|
+
item_indices: dict[bytes, list[int]] = {}
|
296
|
+
|
297
|
+
if is_box(x):
|
298
|
+
for i, x_item in enumerate(x):
|
299
|
+
item_indices.setdefault(x_item[0].tobytes(), []).append(i)
|
300
|
+
|
301
|
+
else:
|
302
|
+
for i, x_item in enumerate(x):
|
303
|
+
item_indices.setdefault(x_item.tobytes(), []).append(i)
|
304
|
+
|
305
|
+
result = []
|
306
|
+
for idx in item_indices.values():
|
307
|
+
result.append(_apply_monad(verb, y[idx]))
|
308
|
+
|
309
|
+
result = maybe_pad_with_fill_value(result, fill_value=get_fill_value(y))
|
310
|
+
return np.asarray(result)
|
311
|
+
|
312
|
+
spelling = maybe_parenthesise_verb_spelling(verb.spelling)
|
313
|
+
spelling = f"{verb.spelling}/."
|
314
|
+
|
315
|
+
return Verb(
|
316
|
+
name=spelling,
|
317
|
+
spelling=spelling,
|
318
|
+
monad=Monad(name=spelling, rank=INFINITY, function=monad),
|
319
|
+
dyad=Dyad(
|
320
|
+
name=spelling,
|
321
|
+
left_rank=INFINITY,
|
322
|
+
right_rank=INFINITY,
|
323
|
+
function=dyad,
|
324
|
+
),
|
325
|
+
)
|
326
|
+
|
327
|
+
|
328
|
+
ADVERB_MAP: dict[str, Callable[[Verb[np.ndarray]], Verb[np.ndarray]]] = {
|
329
|
+
"SLASH": slash_adverb,
|
330
|
+
"SLASHDOT": slashdot_adverb,
|
331
|
+
"BSLASH": bslash_adverb,
|
332
|
+
"BSLASHDOT": bslashdot_adverb,
|
333
|
+
"TILDE": tilde_adverb,
|
334
|
+
}
|
@@ -0,0 +1,343 @@
|
|
1
|
+
"""Methods for applying verb implementations to nouns and verbs.
|
2
|
+
|
3
|
+
Main references:
|
4
|
+
* https://code.jsoftware.com/wiki/Vocabulary/Agreement
|
5
|
+
* https://www.jsoftware.com/help/jforc/loopless_code_i_verbs_have_r.htm
|
6
|
+
|
7
|
+
"""
|
8
|
+
|
9
|
+
import functools
|
10
|
+
from dataclasses import dataclass
|
11
|
+
|
12
|
+
import numpy as np
|
13
|
+
from jinx.errors import JinxNotImplementedError, LengthError, ValenceError
|
14
|
+
from jinx.execution.numpy.conversion import ndarray_or_scalar_to_noun
|
15
|
+
from jinx.execution.numpy.helpers import (
|
16
|
+
is_ufunc,
|
17
|
+
is_ufunc_based,
|
18
|
+
maybe_pad_with_fill_value,
|
19
|
+
)
|
20
|
+
from jinx.vocabulary import Adverb, Conjunction, Dyad, Monad, Noun, RankT, Verb
|
21
|
+
|
22
|
+
|
23
|
+
def get_rank(verb_rank: RankT, noun_rank: int) -> int:
|
24
|
+
"""Get the rank at which to apply the verb to the noun.
|
25
|
+
|
26
|
+
If the verb rank is negative, it means that the verb rank is subtracted
|
27
|
+
from the noun rank, to a minimum of 0.
|
28
|
+
"""
|
29
|
+
if verb_rank < 0:
|
30
|
+
return max(0, noun_rank + verb_rank) # type: ignore[return-value]
|
31
|
+
return min(verb_rank, noun_rank) # type: ignore[return-value]
|
32
|
+
|
33
|
+
|
34
|
+
def fill_and_assemble(
|
35
|
+
cells: list[np.ndarray], frame_shape: tuple[int, ...]
|
36
|
+
) -> np.ndarray:
|
37
|
+
if not cells:
|
38
|
+
return np.asarray(cells).reshape(frame_shape)
|
39
|
+
|
40
|
+
cells = maybe_pad_with_fill_value(cells)
|
41
|
+
return np.asarray(cells).reshape(frame_shape + cells[0].shape)
|
42
|
+
|
43
|
+
|
44
|
+
@dataclass
|
45
|
+
class ArrayCells:
|
46
|
+
cell_shape: tuple[int, ...]
|
47
|
+
frame_shape: tuple[int, ...]
|
48
|
+
cells: np.ndarray
|
49
|
+
|
50
|
+
|
51
|
+
def split_into_cells(arr: np.ndarray, rank: int) -> ArrayCells:
|
52
|
+
"""
|
53
|
+
Look at the array shape and rank to determine frame and cell shape.
|
54
|
+
|
55
|
+
The trailing `rank` axes define the cell shape and the preceding
|
56
|
+
axes define the frame shape. E.g. for rank=2:
|
57
|
+
|
58
|
+
arr.shape = (n0, n1, n2, n3, n4)
|
59
|
+
---------- ------
|
60
|
+
^ frame ^ cell
|
61
|
+
|
62
|
+
If rank=0, the frame shape is the same as the shape and the monad
|
63
|
+
applies to each atom of the array.
|
64
|
+
"""
|
65
|
+
if arr.size == 0:
|
66
|
+
return ArrayCells(cell_shape=(), frame_shape=arr.shape, cells=arr)
|
67
|
+
|
68
|
+
if rank == 0:
|
69
|
+
return ArrayCells(cell_shape=(), frame_shape=arr.shape, cells=arr.ravel())
|
70
|
+
|
71
|
+
return ArrayCells(
|
72
|
+
cell_shape=arr.shape[-rank:],
|
73
|
+
frame_shape=arr.shape[:-rank],
|
74
|
+
cells=arr.reshape(-1, *arr.shape[-rank:]),
|
75
|
+
)
|
76
|
+
|
77
|
+
|
78
|
+
def apply_monad(verb: Verb[np.ndarray], noun: Noun[np.ndarray]) -> Noun[np.ndarray]:
|
79
|
+
result = _apply_monad(verb, noun.implementation)
|
80
|
+
return ndarray_or_scalar_to_noun(result)
|
81
|
+
|
82
|
+
|
83
|
+
def _apply_monad(verb: Verb[np.ndarray], arr: np.ndarray) -> np.ndarray:
|
84
|
+
if verb.monad is None or verb.monad.function is None:
|
85
|
+
raise ValenceError(f"Verb {verb.spelling} has no monadic valence.")
|
86
|
+
if verb.monad.function is NotImplemented:
|
87
|
+
raise JinxNotImplementedError(
|
88
|
+
f"Verb {verb.spelling} monad function is not yet implemented in Jinx."
|
89
|
+
)
|
90
|
+
|
91
|
+
if isinstance(verb.monad.function, Verb):
|
92
|
+
function = functools.partial(_apply_monad, verb.monad.function)
|
93
|
+
else:
|
94
|
+
function = verb.monad.function # type: ignore[assignment]
|
95
|
+
|
96
|
+
if arr.size == 0:
|
97
|
+
return function(arr)
|
98
|
+
|
99
|
+
rank = get_rank(verb.monad.rank, arr.ndim)
|
100
|
+
|
101
|
+
# If the verb rank is 0 it applies to each atom of the array.
|
102
|
+
# NumPy's unary ufuncs are typically designed to work this way
|
103
|
+
# Apply the function directly here as an optimisation.
|
104
|
+
if rank == 0 and (is_ufunc(function) or is_ufunc_based(function)):
|
105
|
+
return function(arr)
|
106
|
+
|
107
|
+
array_cells = split_into_cells(arr, rank)
|
108
|
+
cells = [function(cell) for cell in array_cells.cells]
|
109
|
+
return fill_and_assemble(cells, array_cells.frame_shape)
|
110
|
+
|
111
|
+
|
112
|
+
def apply_dyad(
|
113
|
+
verb: Verb[np.ndarray], noun_1: Noun[np.ndarray], noun_2: Noun[np.ndarray]
|
114
|
+
) -> Noun[np.ndarray]:
|
115
|
+
result = _apply_dyad(verb, noun_1.implementation, noun_2.implementation)
|
116
|
+
return ndarray_or_scalar_to_noun(result)
|
117
|
+
|
118
|
+
|
119
|
+
def _apply_dyad(
|
120
|
+
verb: Verb[np.ndarray], left_arr: np.ndarray, right_arr: np.ndarray
|
121
|
+
) -> np.ndarray:
|
122
|
+
if verb.dyad is None or verb.dyad.function is None:
|
123
|
+
raise ValenceError(f"Verb {verb.spelling} has no dyadic valence.")
|
124
|
+
if verb.dyad.function is NotImplemented:
|
125
|
+
raise JinxNotImplementedError(
|
126
|
+
f"Verb {verb.spelling} dyad function is not yet implemented."
|
127
|
+
)
|
128
|
+
|
129
|
+
if isinstance(verb.dyad.function, Verb):
|
130
|
+
function = functools.partial(_apply_dyad, verb.dyad.function)
|
131
|
+
else:
|
132
|
+
function = verb.dyad.function # type: ignore[assignment]
|
133
|
+
|
134
|
+
if left_arr.size == 0 or right_arr.size == 0:
|
135
|
+
return function(left_arr, right_arr)
|
136
|
+
|
137
|
+
left_rank = get_rank(verb.dyad.left_rank, left_arr.ndim)
|
138
|
+
right_rank = get_rank(verb.dyad.right_rank, right_arr.ndim)
|
139
|
+
|
140
|
+
# If the left and right ranks are both 0 and one of the arrays is a scalar,
|
141
|
+
# apply the dyad directly as an optimisation.
|
142
|
+
if (
|
143
|
+
left_rank == right_rank == 0
|
144
|
+
and (is_ufunc(function) or is_ufunc_based(function))
|
145
|
+
and (left_arr.ndim == 0 or right_arr.ndim == 0)
|
146
|
+
):
|
147
|
+
return function(left_arr, right_arr)
|
148
|
+
|
149
|
+
left = split_into_cells(left_arr, left_rank)
|
150
|
+
right = split_into_cells(right_arr, right_rank)
|
151
|
+
|
152
|
+
# If the left and right frame shapes are the same, we can apply the dyad immediately.
|
153
|
+
if left.frame_shape == right.frame_shape:
|
154
|
+
cells = [
|
155
|
+
function(left_cell, right_cell)
|
156
|
+
for left_cell, right_cell in zip(left.cells, right.cells, strict=True)
|
157
|
+
]
|
158
|
+
return fill_and_assemble(cells, left.frame_shape)
|
159
|
+
|
160
|
+
# Otherwise we need to find the common frame shape. One of the frame shapes must
|
161
|
+
# be a prefix of the other, otherwise it's not possible to apply the dyad.
|
162
|
+
common_frame_shape = find_common_frame_shape(left.frame_shape, right.frame_shape)
|
163
|
+
if common_frame_shape is None:
|
164
|
+
raise LengthError(
|
165
|
+
f"Cannot apply dyad {verb.spelling} to arrays of shape {left.frame_shape} and {right.frame_shape}"
|
166
|
+
)
|
167
|
+
|
168
|
+
rcf = len(common_frame_shape)
|
169
|
+
|
170
|
+
left_rcf_cell_shape = left_arr.shape[rcf:]
|
171
|
+
right_rcf_cell_shape = right_arr.shape[rcf:]
|
172
|
+
|
173
|
+
left_arr_reshaped = left_arr.reshape(-1, *left_rcf_cell_shape)
|
174
|
+
right_arr_reshaped = right_arr.reshape(-1, *right_rcf_cell_shape)
|
175
|
+
|
176
|
+
cells = []
|
177
|
+
for left_cell, right_cell in zip(
|
178
|
+
left_arr_reshaped, right_arr_reshaped, strict=True
|
179
|
+
):
|
180
|
+
subcells = []
|
181
|
+
if common_frame_shape == left.frame_shape:
|
182
|
+
# right_cell is longer and contains multiple operand cells
|
183
|
+
if right_rank == 0:
|
184
|
+
right_subcells = right_cell.ravel()
|
185
|
+
else:
|
186
|
+
right_subcells = right_cell.reshape(-1, *right.cell_shape)
|
187
|
+
|
188
|
+
for right_subcell in right_subcells:
|
189
|
+
subcells.append(function(left_cell, right_subcell))
|
190
|
+
else:
|
191
|
+
# left_cell is longer and contains multiple operand cells
|
192
|
+
if left_rank == 0:
|
193
|
+
left_subcells = left_cell.ravel()
|
194
|
+
else:
|
195
|
+
left_subcells = left_cell.reshape(-1, *left.cell_shape)
|
196
|
+
|
197
|
+
for left_subcell in left_subcells:
|
198
|
+
subcells.append(function(left_subcell, right_cell))
|
199
|
+
|
200
|
+
subcells = maybe_pad_with_fill_value(subcells)
|
201
|
+
subarray = np.asarray(subcells)
|
202
|
+
if subarray.shape:
|
203
|
+
cells.extend(subcells)
|
204
|
+
else:
|
205
|
+
cells.append(subarray)
|
206
|
+
|
207
|
+
cells = maybe_pad_with_fill_value(cells)
|
208
|
+
array = np.asarray(cells)
|
209
|
+
|
210
|
+
# Gather the cells into the final frame shape (the longer of the left
|
211
|
+
# and right frame shapes, plus the result cell shape).
|
212
|
+
collecting_frame = max(left.frame_shape, right.frame_shape, key=len)
|
213
|
+
return array.reshape(collecting_frame + cells[0].shape)
|
214
|
+
|
215
|
+
|
216
|
+
def find_common_frame_shape(
|
217
|
+
left_frame_shape: tuple[int, ...], right_frame_shape: tuple[int, ...]
|
218
|
+
) -> tuple[int, ...] | None:
|
219
|
+
if len(left_frame_shape) <= len(right_frame_shape):
|
220
|
+
shorter = left_frame_shape
|
221
|
+
longer = right_frame_shape
|
222
|
+
else:
|
223
|
+
shorter = right_frame_shape
|
224
|
+
longer = left_frame_shape
|
225
|
+
|
226
|
+
if all(a == b for a, b in zip(shorter, longer)):
|
227
|
+
return shorter
|
228
|
+
|
229
|
+
return None
|
230
|
+
|
231
|
+
|
232
|
+
def apply_conjunction(
|
233
|
+
verb_or_noun_1: Verb | Noun, conjunction: Conjunction, verb_or_noun_2: Verb | Noun
|
234
|
+
) -> Verb | Noun:
|
235
|
+
return conjunction.function(verb_or_noun_1, verb_or_noun_2)
|
236
|
+
|
237
|
+
|
238
|
+
def apply_adverb(verb_or_noun: Verb | Noun, adverb: Adverb) -> Verb:
|
239
|
+
return adverb.function(verb_or_noun)
|
240
|
+
|
241
|
+
|
242
|
+
INFINITY = float("inf")
|
243
|
+
|
244
|
+
|
245
|
+
def build_hook(f: Verb[np.ndarray], g: Verb[np.ndarray]) -> Verb[np.ndarray]:
|
246
|
+
"""Build a hook given verbs f and g.
|
247
|
+
|
248
|
+
(f g) y -> y f (g y)
|
249
|
+
x (f g) y -> x f (g y)
|
250
|
+
|
251
|
+
The new verb has infinite rank.
|
252
|
+
"""
|
253
|
+
|
254
|
+
def _monad(y: np.ndarray) -> np.ndarray:
|
255
|
+
a = _apply_monad(g, y)
|
256
|
+
return _apply_dyad(f, y, a)
|
257
|
+
|
258
|
+
def _dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
|
259
|
+
a = _apply_monad(g, y)
|
260
|
+
return _apply_dyad(f, x, a)
|
261
|
+
|
262
|
+
f_spelling = f"({f.spelling})" if " " in f.spelling else f.spelling
|
263
|
+
g_spelling = f"({g.spelling})" if " " in g.spelling else g.spelling
|
264
|
+
spelling = f"{f_spelling} {g_spelling}"
|
265
|
+
|
266
|
+
return Verb[np.ndarray](
|
267
|
+
spelling=spelling,
|
268
|
+
name=spelling,
|
269
|
+
monad=Monad(
|
270
|
+
name=spelling,
|
271
|
+
rank=INFINITY,
|
272
|
+
function=_monad,
|
273
|
+
),
|
274
|
+
dyad=Dyad(
|
275
|
+
name=spelling,
|
276
|
+
left_rank=INFINITY,
|
277
|
+
right_rank=INFINITY,
|
278
|
+
function=_dyad,
|
279
|
+
),
|
280
|
+
)
|
281
|
+
|
282
|
+
|
283
|
+
def build_fork(
|
284
|
+
f: Verb[np.ndarray] | Noun[np.ndarray], g: Verb[np.ndarray], h: Verb[np.ndarray]
|
285
|
+
) -> Verb[np.ndarray]:
|
286
|
+
"""Build a fork given verbs f, g, h.
|
287
|
+
|
288
|
+
(f g h) y -> (f y) g (h y)
|
289
|
+
x (f g h) y -> (x f y) g (x h y)
|
290
|
+
|
291
|
+
The new verb has infinite rank.
|
292
|
+
|
293
|
+
Note that f can be a noun, in which case there is one fewer function calls.
|
294
|
+
"""
|
295
|
+
|
296
|
+
def _monad(y: np.ndarray) -> np.ndarray:
|
297
|
+
if isinstance(f, Verb) and f.spelling == "[:":
|
298
|
+
hy = _apply_monad(h, y)
|
299
|
+
return _apply_monad(g, hy)
|
300
|
+
|
301
|
+
if isinstance(f, Verb):
|
302
|
+
a = _apply_monad(f, y)
|
303
|
+
else:
|
304
|
+
a = f.implementation
|
305
|
+
b = _apply_monad(h, y)
|
306
|
+
return _apply_dyad(g, a, b)
|
307
|
+
|
308
|
+
def _dyad(x: np.ndarray, y: np.ndarray) -> np.ndarray:
|
309
|
+
if isinstance(f, Verb) and f.spelling == "[:":
|
310
|
+
hy = _apply_dyad(h, x, y)
|
311
|
+
return _apply_monad(g, hy)
|
312
|
+
|
313
|
+
if isinstance(f, Verb):
|
314
|
+
a = _apply_dyad(f, x, y)
|
315
|
+
else:
|
316
|
+
a = f.implementation
|
317
|
+
b = _apply_dyad(h, x, y)
|
318
|
+
return _apply_dyad(g, a, b)
|
319
|
+
|
320
|
+
if isinstance(f, Verb):
|
321
|
+
f_spelling = f"({f.spelling})" if " " in f.spelling else f.spelling
|
322
|
+
else:
|
323
|
+
f_spelling = str(f.implementation)
|
324
|
+
|
325
|
+
g_spelling = f"({g.spelling})" if " " in g.spelling else g.spelling
|
326
|
+
h_spelling = f"({h.spelling})" if " " in h.spelling else h.spelling
|
327
|
+
spelling = f"{f_spelling} {g_spelling} {h_spelling}"
|
328
|
+
|
329
|
+
return Verb[np.ndarray](
|
330
|
+
spelling=spelling,
|
331
|
+
name=spelling,
|
332
|
+
monad=Monad(
|
333
|
+
name=spelling,
|
334
|
+
rank=INFINITY,
|
335
|
+
function=_monad,
|
336
|
+
),
|
337
|
+
dyad=Dyad(
|
338
|
+
name=spelling,
|
339
|
+
left_rank=INFINITY,
|
340
|
+
right_rank=INFINITY,
|
341
|
+
function=_dyad,
|
342
|
+
),
|
343
|
+
)
|