klongpy 0.6.9__py3-none-any.whl → 0.7.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.
Files changed (70) hide show
  1. klongpy/__init__.py +17 -1
  2. klongpy/adverbs.py +84 -82
  3. klongpy/autograd.py +299 -0
  4. klongpy/backend.py +38 -103
  5. klongpy/backends/__init__.py +26 -0
  6. klongpy/backends/base.py +469 -0
  7. klongpy/backends/numpy_backend.py +123 -0
  8. klongpy/backends/registry.py +76 -0
  9. klongpy/backends/torch_backend.py +1047 -0
  10. klongpy-0.6.9.data/scripts/kgpy → klongpy/cli.py +110 -90
  11. klongpy/core.py +113 -974
  12. klongpy/db/sys_fn_db.py +7 -6
  13. klongpy/db/sys_fn_kvs.py +2 -4
  14. klongpy/dyads.py +332 -160
  15. klongpy/interpreter.py +60 -15
  16. klongpy/monads.py +121 -75
  17. klongpy/parser.py +328 -0
  18. klongpy/repl.py +23 -5
  19. klongpy/sys_fn.py +170 -21
  20. klongpy/sys_fn_autograd.py +290 -0
  21. klongpy/sys_fn_ipc.py +22 -15
  22. klongpy/sys_fn_timer.py +13 -3
  23. klongpy/types.py +503 -0
  24. klongpy/web/sys_fn_web.py +14 -4
  25. klongpy/writer.py +122 -0
  26. klongpy/ws/sys_fn_ws.py +5 -8
  27. klongpy-0.7.1.dist-info/METADATA +544 -0
  28. klongpy-0.7.1.dist-info/RECORD +52 -0
  29. {klongpy-0.6.9.dist-info → klongpy-0.7.1.dist-info}/WHEEL +1 -1
  30. klongpy-0.7.1.dist-info/entry_points.txt +2 -0
  31. {klongpy-0.6.9.dist-info → klongpy-0.7.1.dist-info}/top_level.txt +0 -1
  32. klongpy-0.6.9.dist-info/METADATA +0 -448
  33. klongpy-0.6.9.dist-info/RECORD +0 -77
  34. tests/__init__.py +0 -6
  35. tests/gen_join_over.py +0 -119
  36. tests/gen_py_suite.py +0 -77
  37. tests/gen_test_fn.py +0 -259
  38. tests/perf_async.py +0 -25
  39. tests/perf_avg.py +0 -18
  40. tests/perf_duckdb.py +0 -32
  41. tests/perf_gen.py +0 -38
  42. tests/perf_ipc_overhead.py +0 -34
  43. tests/perf_join.py +0 -53
  44. tests/perf_load.py +0 -17
  45. tests/perf_prog.py +0 -18
  46. tests/perf_serdes.py +0 -52
  47. tests/perf_sys_fn_db.py +0 -263
  48. tests/perf_vector.py +0 -40
  49. tests/test_accel.py +0 -227
  50. tests/test_df_cache.py +0 -85
  51. tests/test_eval_monad_list.py +0 -34
  52. tests/test_examples.py +0 -64
  53. tests/test_extra_suite.py +0 -382
  54. tests/test_file_cache.py +0 -185
  55. tests/test_interop.py +0 -180
  56. tests/test_kg_asarray.py +0 -94
  57. tests/test_kgtests.py +0 -65
  58. tests/test_known_bugs.py +0 -206
  59. tests/test_prog.py +0 -107
  60. tests/test_reshape_strings.py +0 -33
  61. tests/test_suite.py +0 -1480
  62. tests/test_suite_file.py +0 -153
  63. tests/test_sys_fn.py +0 -420
  64. tests/test_sys_fn_db.py +0 -88
  65. tests/test_sys_fn_ipc.py +0 -587
  66. tests/test_sys_fn_timer.py +0 -133
  67. tests/test_sys_fn_web.py +0 -50
  68. tests/test_util.py +0 -233
  69. tests/utils.py +0 -126
  70. {klongpy-0.6.9.dist-info → klongpy-0.7.1.dist-info}/licenses/LICENSE +0 -0
klongpy/dyads.py CHANGED
@@ -1,8 +1,9 @@
1
1
  from .core import *
2
- import sys
2
+ from .autograd import grad_of_fn, numeric_grad, jacobian_of_fn, multi_jacobian_of_fn, multi_grad_of_fn
3
+ import numpy
3
4
 
4
5
 
5
- def eval_dyad_add(a, b):
6
+ def eval_dyad_add(a, b, backend):
6
7
  """
7
8
 
8
9
  a+b [Plus]
@@ -17,10 +18,10 @@ def eval_dyad_add(a, b):
17
18
  1+0.3 --> 1.3
18
19
 
19
20
  """
20
- return np.add(a, b)
21
+ return backend.np.add(a, b)
21
22
 
22
23
 
23
- def eval_dyad_amend(a, b):
24
+ def eval_dyad_amend(a, b, backend):
24
25
  """
25
26
 
26
27
  a:=b [Amend]
@@ -46,13 +47,14 @@ def eval_dyad_amend(a, b):
46
47
  "abc":="def",3 --> "abcdef"
47
48
 
48
49
  """
49
- if not (isinstance(a, (str,list)) or np.isarray(a)):
50
+ np_backend = backend.np
51
+ if not (isinstance(a, (str,list)) or np_backend.isarray(a)):
50
52
  raise RuntimeError(f"a must be list or str: {a}")
51
53
  if len(b) <= 1:
52
54
  return a
53
55
  if isinstance(a, str):
54
- r = str_to_chr_arr(a)
55
- q = str_to_chr_arr(b[0])
56
+ r = backend.str_to_chr_arr(a)
57
+ q = backend.str_to_chr_arr(b[0])
56
58
  for i in b[1:]:
57
59
  try:
58
60
  r[i:i+len(q)] = q
@@ -61,29 +63,29 @@ def eval_dyad_amend(a, b):
61
63
  if i > len(r):
62
64
  RangeError(i)
63
65
  elif i == len(r):
64
- r = np.append(r, b[0])
66
+ r = numpy.append(r, b[0])
65
67
  else:
66
68
  r[i] = b[0]
67
69
  return "".join(["".join(x) for x in r])
68
- r = np.array(a) # clone
69
- if is_list(b[0]): # TOOD: use np.put if we can
70
+ r = np_backend.array(a) # clone
71
+ if is_list(b[0]): # TOOD: use bknp.put if we can
70
72
  r = r.tolist()
71
73
  for i in b[1:]:
72
74
  r[i] = b[0]
73
- r = kg_asarray(r)
75
+ r = backend.kg_asarray(r)
74
76
  else:
75
- np.put(r, np.asarray(b[1:],dtype=int), b[0])
77
+ numpy.put(r, numpy.asarray(b[1:],dtype=int), b[0])
76
78
  return r
77
79
 
78
80
 
79
81
  def _e_dyad_amend_in_depth(p, q, v):
80
- if np.isarray(q) and len(q) > 1:
82
+ if bknp.isarray(q) and len(q) > 1:
81
83
  r = _e_dyad_amend_in_depth(p[q[0]], q[1:] if len(q) > 2 else q[1], v)
82
- p = np.array(p, dtype=r.dtype)
84
+ p = bknp.array(p, dtype=r.dtype)
83
85
  p[q[0]] = r
84
86
  return p
85
87
  else:
86
- p = np.array(p, dtype=object) if isinstance(v, (str, KGSym)) else np.array(p)
88
+ p = bknp.array(p, dtype=object) if isinstance(v, (str, KGSym)) else bknp.array(p)
87
89
  p[q] = v
88
90
  return p
89
91
 
@@ -105,7 +107,7 @@ def eval_dyad_amend_in_depth(a, b):
105
107
  return _e_dyad_amend_in_depth(a, b[1:], b[0])
106
108
 
107
109
 
108
- def eval_dyad_cut(a, b):
110
+ def eval_dyad_cut(a, b, backend):
109
111
  """
110
112
 
111
113
  a:_b [Cut]
@@ -127,12 +129,12 @@ def eval_dyad_cut(a, b):
127
129
 
128
130
  """
129
131
  j = isinstance(b, str)
130
- b = np.asarray(str_to_chr_arr(b) if j else b)
131
- a = a if np.isarray(a) else [a]
132
- r = np.array_split(b, a)
132
+ b = bknp.asarray(backend.str_to_chr_arr(b) if j else b)
133
+ a = a if bknp.isarray(a) else [a]
134
+ r = bknp.array_split(b, a)
133
135
  if len(b) == 0 and len(a) > 0:
134
136
  r = r[1:]
135
- return np.asarray(["".join(x) for x in r]) if j else kg_asarray(r)
137
+ return bknp.asarray(["".join(x) for x in r]) if j else backend.kg_asarray(r)
136
138
 
137
139
 
138
140
  def eval_dyad_at_index(klong, a, b):
@@ -166,24 +168,25 @@ def eval_dyad_at_index(klong, a, b):
166
168
 
167
169
  """
168
170
  if isinstance(a, (KGFn, KGSym)) or issubclass(type(a), KGLambda):
169
- b = [x for x in b] if np.isarray(b) else b
171
+ b = [x for x in b] if klong._backend.is_array(b) else b
170
172
  return klong.eval(KGCall(a, b, arity=1))
173
+ backend = klong._backend
171
174
  j = isinstance(a,str)
172
- a = str_to_chr_arr(a) if j else a
175
+ a = backend.str_to_chr_arr(a) if j else a
173
176
  if is_list(b):
174
177
  if is_empty(b):
175
- r = np.asarray([])
178
+ r = bknp.asarray([])
176
179
  else:
177
180
  # TODO: return None for missing keys? or raise?
178
- r = kg_asarray([a[x] for x in b])
179
- elif is_integer(b):
181
+ r = backend.kg_asarray([a[x] for x in b])
182
+ elif backend.is_integer(b):
180
183
  r = a[b]
181
184
  j = False
182
185
  else:
183
186
  r = a
184
187
  if j:
185
- if np.isarray(r) and r.ndim > 1:
186
- return np.asarray(["".join(x) for x in r], dtype=object)
188
+ if bknp.isarray(r) and r.ndim > 1:
189
+ return bknp.asarray(["".join(x) for x in r], dtype=object)
187
190
  return "".join(r)
188
191
  return r
189
192
 
@@ -208,7 +211,7 @@ def eval_dyad_define(klong, n, v):
208
211
  return v
209
212
 
210
213
 
211
- def eval_dyad_divide(a, b):
214
+ def eval_dyad_divide(a, b, backend):
212
215
  """
213
216
 
214
217
  a%b [Divide]
@@ -222,7 +225,11 @@ def eval_dyad_divide(a, b):
222
225
  10%8 --> 1.25
223
226
 
224
227
  """
225
- return np.divide(a, b)
228
+ if not is_list(a) and not is_list(b) and backend.is_number(b):
229
+ b_val = backend.scalar_to_python(b) if backend.is_backend_array(b) or (hasattr(b, 'ndim') and b.ndim == 0) else b
230
+ if b_val == 0:
231
+ return KLONG_UNDEFINED
232
+ return backend.np.divide(a, b)
226
233
 
227
234
 
228
235
  def eval_dyad_drop(a, b):
@@ -254,7 +261,12 @@ def eval_dyad_drop(a, b):
254
261
  return b[a:] if a >= 0 else b[:a]
255
262
 
256
263
 
257
- def eval_dyad_equal(a, b):
264
+ def _safe_equal(x, y, backend):
265
+ """Compare two values, handling backend arrays correctly."""
266
+ return kg_truth(backend.safe_equal(x, y))
267
+
268
+
269
+ def eval_dyad_equal(a, b, backend):
258
270
  """
259
271
 
260
272
  a=b [Equal]
@@ -279,7 +291,7 @@ def eval_dyad_equal(a, b):
279
291
  [1 2 3]=[1 4 3] --> [1 0 1]
280
292
 
281
293
  """
282
- return vec_fn2(a, b, lambda x, y: kg_truth(np.asarray(x,dtype=object) == np.asarray(y,dtype=object)))
294
+ return backend.vec_fn2(a, b, lambda x, y: _safe_equal(x, y, backend))
283
295
 
284
296
 
285
297
  def finditer(s, sub):
@@ -292,7 +304,7 @@ def finditer(s, sub):
292
304
  i += 1
293
305
 
294
306
 
295
- def eval_dyad_find(a, b):
307
+ def eval_dyad_find(a, b, backend):
296
308
  """
297
309
 
298
310
  a?b [Find]
@@ -321,44 +333,44 @@ def eval_dyad_find(a, b):
321
333
 
322
334
  """
323
335
  if isinstance(a,str):
324
- return np.asarray(list(finditer(a,str(b))))
336
+ return bknp.asarray(list(finditer(a,str(b))))
325
337
  elif is_dict(a):
326
338
  v = a.get(b)
327
- return np.inf if v is None else v
339
+ return KLONG_UNDEFINED if v is None else v
328
340
  if is_list(b):
329
- return np.asarray([i for i,x in enumerate(a) if kg_equal(x,b)])
330
- return np.where(np.asarray(a) == b)[0]
341
+ return bknp.asarray([i for i,x in enumerate(a) if backend.kg_equal(x, b)])
342
+ return bknp.where(bknp.asarray(a) == b)[0]
331
343
 
332
344
 
333
- def __e_dyad_form(a, b):
345
+ def __e_dyad_form(a, b, backend):
334
346
  if isinstance(a,KGSym):
335
347
  if is_empty(b):
336
- return np.inf
348
+ return KLONG_UNDEFINED
337
349
  return KGSym(b[1:] if isinstance(b,str) and b.startswith(":") else b)
338
- if is_integer(a):
339
- if is_float(b) or is_empty(b) or ('.' in b and str_is_float(b)):
340
- return np.inf
350
+ if backend.is_integer(a):
351
+ if backend.is_float(b) or is_empty(b) or ('.' in b and str_is_float(b)):
352
+ return KLONG_UNDEFINED
341
353
  return int(b)
342
- if is_float(a):
354
+ if backend.is_float(a):
343
355
  if is_empty(b):
344
- return np.inf
356
+ return KLONG_UNDEFINED
345
357
  return float(b)
346
358
  if isinstance(a,KGChar):
347
359
  b = str(b)
348
360
  if len(b) != 1:
349
- return np.inf
361
+ return KLONG_UNDEFINED
350
362
  return KGChar(str(b)[0])
351
363
  return b
352
364
 
353
- def _e_dyad_form(a, b):
365
+ def _e_dyad_form(a, b, backend):
354
366
  """
355
367
  Unravel the broadcasting of a and b and apply __e_dyad_form
356
368
  """
357
- if np.isarray(a) and np.isarray(b):
358
- return np.asarray([vec_fn2(x,y,_e_dyad_form) for x,y in zip(a,b)])
359
- return __e_dyad_form(a,b)
369
+ if bknp.isarray(a) and bknp.isarray(b):
370
+ return bknp.asarray([backend.vec_fn2(x, y, lambda x, y: _e_dyad_form(x, y, backend)) for x,y in zip(a,b)])
371
+ return __e_dyad_form(a, b, backend)
360
372
 
361
- def eval_dyad_form(a, b):
373
+ def eval_dyad_form(a, b, backend):
362
374
  """
363
375
 
364
376
  a:$b [Form]
@@ -385,13 +397,17 @@ def eval_dyad_form(a, b):
385
397
  :x:$":symbol" --> :symbol
386
398
 
387
399
  """
388
- return vec_fn2(a, b, _e_dyad_form)
400
+ return backend.vec_fn2(a, b, lambda x, y: _e_dyad_form(x, y, backend))
389
401
 
390
402
 
391
- def __e_dyad_format2(a, b):
403
+ def __e_dyad_format2(a, b, backend):
404
+ if hasattr(a, 'ndim') and a.ndim == 0:
405
+ a = a.item()
406
+ if hasattr(b, 'ndim') and b.ndim == 0:
407
+ b = b.item()
392
408
  if safe_eq(int(a), 0):
393
409
  return str(b)
394
- if (is_float(b) and not isinstance(b,int)) and (is_float(a) and not isinstance(a,int)):
410
+ if (backend.is_float(b) and not isinstance(b,int)) and (backend.is_float(a) and not isinstance(a,int)):
395
411
  b = "{:Xf}".replace("X",str(a)).format(b)
396
412
  p = b.split('.')
397
413
  p[0] = p[0].rjust(int(a))
@@ -401,17 +417,17 @@ def __e_dyad_format2(a, b):
401
417
  r = str(b).ljust(abs(a)) if a >= 0 else str(b).rjust(abs(a))
402
418
  return r
403
419
 
404
- def _e_dyad_format2(a, b):
420
+ def _e_dyad_format2(a, b, backend):
405
421
  """
406
422
  Unravel the broadcasting of a and b and apply __e_dyad_format2
407
423
  """
408
424
  if is_list(a) and is_list(b):
409
- return kg_asarray([vec_fn2(x, y, _e_dyad_format2) for x, y in zip(to_list(a), to_list(b))])
410
- if np.isarray(a) and np.isarray(b):
411
- return np.asarray([vec_fn2(x, y, _e_dyad_format2) for x, y in zip(a, b)])
412
- return __e_dyad_format2(a, b)
425
+ return backend.kg_asarray([backend.vec_fn2(x, y, lambda x, y: _e_dyad_format2(x, y, backend)) for x, y in zip(to_list(a), to_list(b))])
426
+ if backend.np.isarray(a) and backend.np.isarray(b):
427
+ return backend.np.asarray([backend.vec_fn2(x, y, lambda x, y: _e_dyad_format2(x, y, backend)) for x, y in zip(a, b)])
428
+ return __e_dyad_format2(a, b, backend)
413
429
 
414
- def eval_dyad_format2(a, b):
430
+ def eval_dyad_format2(a, b, backend):
415
431
  """
416
432
 
417
433
  a$b [Format2]
@@ -436,7 +452,7 @@ def eval_dyad_format2(a, b):
436
452
  5.3$123.45 --> " 123.450"
437
453
 
438
454
  """
439
- return vec_fn2(a, b, _e_dyad_format2)
455
+ return backend.vec_fn2(a, b, lambda x, y: _e_dyad_format2(x, y, backend))
440
456
 
441
457
 
442
458
  def eval_dyad_index_in_depth(a, b):
@@ -456,15 +472,16 @@ def eval_dyad_index_in_depth(a, b):
456
472
  {y+x*x}:@[2 3] --> 7
457
473
 
458
474
  """
459
- return np.asarray(a)[tuple(b) if is_list(b) else b] if not is_empty(b) else b
475
+ return bknp.asarray(a)[tuple(b) if is_list(b) else b] if not is_empty(b) else b
460
476
 
461
477
 
462
- def _e_dyad_integer_divide(x,y):
463
- a = np.divide(x, y)
464
- a = kg_asarray(rec_fn(a,np.trunc)) if np.isarray(a) else a
465
- return np.asarray(a,dtype='int') if np.isarray(a) else int(a)
478
+ def _e_dyad_integer_divide(x, y, backend):
479
+ np_backend = backend.np
480
+ a = np_backend.divide(x, y)
481
+ a = backend.kg_asarray(backend.rec_fn(a, np_backend.trunc)) if np_backend.isarray(a) else a
482
+ return backend.to_int_array(a)
466
483
 
467
- def eval_dyad_integer_divide(a, b):
484
+ def eval_dyad_integer_divide(a, b, backend):
468
485
  """
469
486
 
470
487
  a:%b [Integer-Divide]
@@ -480,7 +497,11 @@ def eval_dyad_integer_divide(a, b):
480
497
  10:%8 --> 1
481
498
 
482
499
  """
483
- return vec_fn2(a, b, _e_dyad_integer_divide)
500
+ if not is_list(a) and not is_list(b) and backend.is_number(b):
501
+ b_val = backend.scalar_to_python(b) if backend.is_backend_array(b) or (hasattr(b, 'ndim') and b.ndim == 0) else b
502
+ if b_val == 0:
503
+ return KLONG_UNDEFINED
504
+ return backend.vec_fn2(a, b, lambda x, y: _e_dyad_integer_divide(x, y, backend))
484
505
 
485
506
 
486
507
  def _arr_to_list(a):
@@ -488,7 +509,7 @@ def _arr_to_list(a):
488
509
  return a if is_list(a) else [a]# if not is_list(a) else a
489
510
 
490
511
 
491
- def eval_dyad_join(a, b):
512
+ def eval_dyad_join(a, b, backend):
492
513
  """
493
514
 
494
515
  a,b [Join]
@@ -539,22 +560,32 @@ def eval_dyad_join(a, b):
539
560
  b[a[0]] = a[1]
540
561
  return b
541
562
 
542
- if np.isarray(a) and np.isarray(b):
543
- if len(a) == 0:
544
- return b
545
- if len(a.shape) == len(b.shape) and a.shape[-1] == b.shape[-1]:
546
- return np.concatenate((a,b))
563
+ if bknp.isarray(a) and bknp.isarray(b):
564
+ # Only use fast path for 1D+ arrays (not 0D scalars)
565
+ a_is_1d_plus = hasattr(a, 'ndim') and a.ndim >= 1
566
+ b_is_1d_plus = hasattr(b, 'ndim') and b.ndim >= 1
567
+ if a_is_1d_plus and b_is_1d_plus:
568
+ if len(a) == 0:
569
+ return b
570
+ if len(a.shape) == len(b.shape) and a.shape[-1] == b.shape[-1]:
571
+ return bknp.concatenate((a,b))
547
572
 
548
573
  aa = _arr_to_list(a)
549
574
  bb = _arr_to_list(b)
550
575
 
551
576
  r = [*aa,*bb]
552
- nr = kg_asarray(r)
553
- t = nr.dtype.type
554
- return nr if issubclass(t, np.integer) or issubclass(t, np.floating) else np.asarray(r,dtype=object)
555
-
556
-
557
- def eval_dyad_less(a, b):
577
+ nr = backend.kg_asarray(r)
578
+ # Check dtype kind for compatibility across backends
579
+ dtype_kind = backend.get_dtype_kind(nr)
580
+ if dtype_kind in ('i', 'f', 'u'):
581
+ return nr
582
+ # Use numpy directly for object arrays (backends without object dtype need this)
583
+ # Convert backend arrays to numpy first (needed for device-backed arrays)
584
+ r_numpy = [backend.to_numpy(x) if backend.is_array(x) else x for x in r]
585
+ return numpy.asarray(r_numpy, dtype=object)
586
+
587
+
588
+ def eval_dyad_less(a, b, backend):
558
589
  """
559
590
 
560
591
  a<b [Less]
@@ -575,10 +606,10 @@ def eval_dyad_less(a, b):
575
606
  [1 2 3]<[1 4 3] --> [0 1 0]
576
607
 
577
608
  """
578
- return kg_truth(vec_fn2(a, b, lambda x,y: x < y if (isinstance(x,str) and isinstance(y,str)) else np.less(x,y)))
609
+ return kg_truth(backend.vec_fn2(a, b, lambda x,y: x < y if (isinstance(x,str) and isinstance(y,str)) else backend.np.less(x,y)))
579
610
 
580
611
 
581
- def eval_dyad_match(a,b):
612
+ def eval_dyad_match(a, b, backend):
582
613
  """
583
614
 
584
615
  a~b [Match]
@@ -612,10 +643,10 @@ def eval_dyad_match(a,b):
612
643
  [1 [2] 3]~[1 [4] 3] --> 0
613
644
 
614
645
  """
615
- return kg_truth(kg_equal(a,b))
646
+ return kg_truth(backend.kg_equal(a, b))
616
647
 
617
648
 
618
- def eval_dyad_maximum(a, b):
649
+ def eval_dyad_maximum(a, b, backend):
619
650
  """
620
651
 
621
652
  a|b [Max/Or]
@@ -639,10 +670,10 @@ def eval_dyad_maximum(a, b):
639
670
  1.0|1.1 --> 1.1
640
671
 
641
672
  """
642
- return np.maximum(a, b)
673
+ return backend.np.maximum(a, b)
643
674
 
644
675
 
645
- def eval_dyad_minimum(a, b):
676
+ def eval_dyad_minimum(a, b, backend):
646
677
  """
647
678
 
648
679
  a&b [Min/And]
@@ -666,10 +697,10 @@ def eval_dyad_minimum(a, b):
666
697
  1.0&1.1 --> 1.0
667
698
 
668
699
  """
669
- return np.minimum(a, b)
700
+ return backend.np.minimum(a, b)
670
701
 
671
702
 
672
- def eval_dyad_more(a, b):
703
+ def eval_dyad_more(a, b, backend):
673
704
  """
674
705
 
675
706
  a>b [More]
@@ -688,10 +719,10 @@ def eval_dyad_more(a, b):
688
719
  [1 4 3]>[1 2 3] --> [0 1 0]
689
720
 
690
721
  """
691
- return kg_truth(vec_fn2(a, b, lambda x,y: x > y if (isinstance(x,str) and isinstance(y,str)) else np.greater(x,y)))
722
+ return kg_truth(backend.vec_fn2(a, b, lambda x,y: x > y if (isinstance(x,str) and isinstance(y,str)) else backend.np.greater(x,y)))
692
723
 
693
724
 
694
- def eval_dyad_multiply(a, b):
725
+ def eval_dyad_multiply(a, b, backend):
695
726
  """
696
727
 
697
728
  a*b [Times]
@@ -705,15 +736,31 @@ def eval_dyad_multiply(a, b):
705
736
  0.3*7 --> 2.1
706
737
 
707
738
  """
708
- return np.multiply(a, b)
709
-
710
-
711
- def _e_dyad_power(a,b):
712
- r = np.power(float(a) if is_integer(a) else a, b)
713
- br = all([np.trunc(x) == x for x in r]) if is_list(r) else np.trunc(r) == r
714
- return np.dtype('int').type(r) if br else r
739
+ return backend.np.multiply(a, b)
740
+
741
+
742
+ def _e_dyad_power(a, b, backend):
743
+ # Check if input requires grad - if so, preserve float for autograd
744
+ input_has_grad = backend.has_gradient(a)
745
+ # Use backend power function which handles gradient-aware power
746
+ r = backend.power(a, b)
747
+ # If input had gradients, keep result as float to preserve autograd
748
+ if input_has_grad:
749
+ return r
750
+ # Check if result is integer using vectorized operations
751
+ r_val = backend.detach_if_needed(r)
752
+ if is_list(r_val):
753
+ # Vectorized check: trunc(r) == r for all elements
754
+ trunc_r = numpy.trunc(r_val) if isinstance(r_val, numpy.ndarray) else r_val.trunc()
755
+ br = bool((trunc_r == r_val).all())
756
+ else:
757
+ val = float(r_val) if hasattr(r_val, 'item') else r_val
758
+ br = numpy.trunc(val) == val
759
+ if br:
760
+ return backend.to_int_array(r)
761
+ return r
715
762
 
716
- def eval_dyad_power(a, b):
763
+ def eval_dyad_power(a, b, backend):
717
764
  """
718
765
 
719
766
  a^b [Power]
@@ -732,10 +779,10 @@ def eval_dyad_power(a, b):
732
779
  2^0.5 --> 1.41421356237309504
733
780
 
734
781
  """
735
- return vec_fn2(a, b, _e_dyad_power)
782
+ return backend.vec_fn2(a, b, lambda x, y: _e_dyad_power(x, y, backend))
736
783
 
737
784
 
738
- def eval_dyad_remainder(a, b):
785
+ def eval_dyad_remainder(a, b, backend):
739
786
  """
740
787
 
741
788
  a!b [Remainder]
@@ -753,10 +800,10 @@ def eval_dyad_remainder(a, b):
753
800
  -7!-5 --> -2
754
801
 
755
802
  """
756
- return np.fmod(a, b)
803
+ return backend.np.fmod(a, b)
757
804
 
758
805
 
759
- def eval_dyad_reshape(a, b):
806
+ def eval_dyad_reshape(a, b, backend):
760
807
  """
761
808
 
762
809
  a:^b [Reshape]
@@ -803,49 +850,52 @@ def eval_dyad_reshape(a, b):
803
850
  [2]:^[[1 2 3]] --> [[1 2 3] [1 2 3]]
804
851
 
805
852
  """
853
+ np_backend = backend.np
806
854
  j = isinstance(b, str)
807
- b = str_to_chr_arr(b) if j else b
808
- if np.isarray(a):
809
- if np.isarray(b):
810
- y = np.where(a < 0)[0]
855
+ b = backend.str_to_chr_arr(b) if j else b
856
+ if np_backend.isarray(a):
857
+ if np_backend.isarray(b):
858
+ y = np_backend.where(a < 0)[0]
811
859
  if len(y) > 0:
812
- a = np.copy(a)
813
- a[y] = b.size // 2
814
- b_s = b.size
815
- a_s = np.prod(a)
860
+ a = np_backend.copy(a)
861
+ a[y] = backend.array_size(b) // 2
862
+ b_s = backend.array_size(b)
863
+ a_s = int(np_backend.prod(a)) # Ensure it's a Python int for comparison
864
+ # Convert shape to tuple of ints for backend compatibility
865
+ a_shape = tuple(int(x) for x in (a.tolist() if hasattr(a, 'tolist') else a))
816
866
  if a_s > b_s:
817
- b = np.tile(b.flatten(), (a_s // b_s))
818
- b = np.concatenate((b, b[:a_s - b.size]))
819
- b_s = b.size
820
- r = b.reshape(a)
821
- r = np.asarray(["".join(x) for x in r]) if j else r
867
+ b = np_backend.tile(b.flatten(), (a_s // b_s))
868
+ b = np_backend.concatenate((b, b[:a_s - backend.array_size(b)]))
869
+ b_s = backend.array_size(b)
870
+ r = b.reshape(a_shape)
871
+ r = np_backend.asarray(["".join(x) for x in r]) if j else r
822
872
  j = False
823
873
  elif a_s == b_s:
824
- r = b.reshape(a)
874
+ r = b.reshape(a_shape)
825
875
  else:
826
- r = np.resize(b, a)
876
+ r = np_backend.resize(b, a_shape)
827
877
  else:
828
- r = np.full(a, b)
878
+ r = np_backend.full(a, b)
829
879
  else:
830
880
  if a == 0:
831
881
  r = b
832
- elif np.isarray(b):
882
+ elif np_backend.isarray(b):
833
883
  if a < b.shape[0]:
834
- r = np.resize(b, (a,))
884
+ r = np_backend.resize(b, (a,))
835
885
  else:
836
- ns = np.ones(len(b.shape),dtype=int)
886
+ ns = np_backend.ones(len(b.shape),dtype=int)
837
887
  ns[0] = a // b.shape[0]
838
- r = np.concatenate((np.tile(b,ns), b[:a - b.shape[0]*ns[0]]))
888
+ r = np_backend.concatenate((np_backend.tile(b,ns), b[:a - b.shape[0]*ns[0]]))
839
889
  else:
840
- r = np.full((a,), b)
890
+ r = np_backend.full((a,), b)
841
891
  if j:
842
- if np.isarray(r) and r.ndim > 1:
843
- return np.asarray(["".join(x) for x in r], dtype=object)
892
+ if np_backend.isarray(r) and r.ndim > 1:
893
+ return np_backend.asarray(["".join(x) for x in r], dtype=object)
844
894
  return "".join(r)
845
895
  return r
846
896
 
847
897
 
848
- def eval_dyad_rotate(a, b):
898
+ def eval_dyad_rotate(a, b, backend):
849
899
  """
850
900
 
851
901
  a:+b [Rotate]
@@ -860,7 +910,7 @@ def eval_dyad_rotate(a, b):
860
910
  rotated will be a!#b.
861
911
 
862
912
  Note that n:+M rotates the rows of a matrix M (i.e. it rotates
863
- it vertically); to rotate its columns (horizontally), use n:+:\M
913
+ it vertically); to rotate its columns (horizontally), use n:+:\\M
864
914
  (Rotate-Each-Left).
865
915
 
866
916
  Examples: 1:+[1 2 3 4 5] --> [5 1 2 3 4]
@@ -872,12 +922,12 @@ def eval_dyad_rotate(a, b):
872
922
  if a == 0 or not is_iterable(b):
873
923
  return b
874
924
  j = isinstance(b, str)
875
- b = str_to_chr_arr(b) if j else b
876
- r = np.roll(b, a)
925
+ b = backend.str_to_chr_arr(b) if j else b
926
+ r = bknp.roll(b, a)
877
927
  return "".join(r) if j else r
878
928
 
879
929
 
880
- def eval_dyad_split(a, b):
930
+ def eval_dyad_split(a, b, backend):
881
931
  """
882
932
 
883
933
  a:#b [Split]
@@ -896,12 +946,12 @@ def eval_dyad_split(a, b):
896
946
 
897
947
  """
898
948
  if len(b) == 0:
899
- return np.asarray([])
949
+ return bknp.asarray([])
900
950
 
901
951
  j = isinstance(b, str)
902
- b = str_to_chr_arr(b) if j else b
952
+ b = backend.str_to_chr_arr(b) if j else b
903
953
 
904
- a = a if np.isarray(a) else [a]
954
+ a = a if bknp.isarray(a) else [a]
905
955
  if len(a) == 1:
906
956
  if a[0] >= len(b):
907
957
  r = [b]
@@ -909,7 +959,7 @@ def eval_dyad_split(a, b):
909
959
  k = len(b) // a[0]
910
960
  if (k*a[0]) < len(b):
911
961
  k += 1
912
- r = np.array_split(b, k)
962
+ r = bknp.array_split(b, k)
913
963
  else:
914
964
  p, q = 0, 0
915
965
  r = []
@@ -920,10 +970,10 @@ def eval_dyad_split(a, b):
920
970
  if p >= len(a):
921
971
  p = 0
922
972
 
923
- return np.asarray(["".join(x) for x in r],dtype=object) if j else kg_asarray(r)
973
+ return bknp.asarray(["".join(x) for x in r],dtype=object) if j else backend.kg_asarray(r)
924
974
 
925
975
 
926
- def eval_dyad_subtract(a, b):
976
+ def eval_dyad_subtract(a, b, backend):
927
977
  """
928
978
 
929
979
  a-b [Minus]
@@ -938,10 +988,10 @@ def eval_dyad_subtract(a, b):
938
988
  1-0.3 --> 0.7
939
989
 
940
990
  """
941
- return np.subtract(a, b)
991
+ return backend.np.subtract(a, b)
942
992
 
943
993
 
944
- def eval_dyad_take(a, b):
994
+ def eval_dyad_take(a, b, backend):
945
995
  """
946
996
 
947
997
  a#b [Take]
@@ -964,32 +1014,154 @@ def eval_dyad_take(a, b):
964
1014
  0#"" --> ""
965
1015
 
966
1016
  """
1017
+ np_backend = backend.np
967
1018
  j = isinstance(b,str)
968
- b = str_to_chr_arr(b) if j else np.asarray(b)
969
- aa = np.abs(a)
970
- if aa > b.size:
971
- b = np.tile(b,aa // len(b))
972
- b = np.concatenate((b, b[:aa-b.size]) if a > 0 else (b[-(aa-b.size):],b))
973
- r = b[a:] if a < 0 else b[:a]
1019
+ b = backend.str_to_chr_arr(b) if j else np_backend.asarray(b)
1020
+ abs_a = np_backend.abs(a)
1021
+ aa = int(abs_a) if hasattr(abs_a, 'item') else abs_a # Convert tensor to int
1022
+ b_size = backend.array_size(b)
1023
+ if b_size == 0:
1024
+ # Handle empty array/string case
1025
+ r = b
1026
+ elif aa > b_size:
1027
+ b = np_backend.tile(b, aa // len(b))
1028
+ b = np_backend.concatenate((b, b[:aa-backend.array_size(b)]) if a > 0 else (b[-(aa-backend.array_size(b)):], b))
1029
+ r = b[a:] if a < 0 else b[:a]
1030
+ else:
1031
+ r = b[a:] if a < 0 else b[:a]
974
1032
  return "".join(r) if j else r
975
1033
 
976
1034
 
1035
+ def eval_dyad_grad(klong, a, b):
1036
+ """
1037
+
1038
+ a∇b [Grad]
1039
+
1040
+ Compute the numeric gradient of the monadic function ``b`` at ``a``
1041
+ using finite differences. Always uses numeric differentiation.
1042
+
1043
+ For automatic differentiation, use the :> operator instead.
1044
+
1045
+ """
1046
+ def call_fn(v):
1047
+ if isinstance(b, (KGSym, KGLambda, KGFn, KGCall)):
1048
+ return klong.call(KGCall(b, [v], 1))
1049
+ return b(v)
1050
+
1051
+ if isinstance(a, KGSym):
1052
+ orig = klong[a]
1053
+
1054
+ def func(v):
1055
+ klong[a] = v
1056
+ try:
1057
+ return call_fn(v)
1058
+ finally:
1059
+ klong[a] = orig
1060
+
1061
+ return numeric_grad(func, orig, klong._backend)
1062
+ else:
1063
+ return numeric_grad(call_fn, a, klong._backend)
1064
+
1065
+
1066
+ def eval_dyad_jacobian(klong, a, b):
1067
+ """
1068
+
1069
+ a∂b [Jacobian]
1070
+
1071
+ Compute Jacobian matrix of function ``b`` at point ``a``.
1072
+ For f: R^n -> R^m, returns m x n matrix where J[i,j] = df_i/dx_j.
1073
+
1074
+ Two modes based on what ``a`` contains:
1075
+ 1. Single point: [1 2]∂f -> Jacobian at that point
1076
+ 2. List of symbols: [w b]∂f -> [J_w J_b] (multi-param mode)
1077
+
1078
+ In multi-param mode, ``b`` should be a niladic function
1079
+ that references the parameter symbols.
1080
+
1081
+ Examples:
1082
+ [1 2]∂{[x@0^2 x@1^2]} --> [[2 0] [0 4]]
1083
+ [w b]∂f --> [J_w J_b] (multi-param mode)
1084
+
1085
+ """
1086
+ # Check if a is a list of symbols (multi-param mode)
1087
+ if is_list(a) and len(a) > 0 and all(isinstance(p, KGSym) for p in a):
1088
+ return multi_jacobian_of_fn(klong, b, list(a))
1089
+ else:
1090
+ return jacobian_of_fn(klong, b, a) # Note: a is point, b is function
1091
+
1092
+
1093
+ def eval_dyad_autograd(klong, a, b):
1094
+ """
1095
+
1096
+ a:>b [Autograd]
1097
+
1098
+ Compute gradient of function ``a`` with respect to ``b``.
1099
+
1100
+ Two modes based on what ``b`` contains:
1101
+ 1. Single param/point: a:>x or a:>[1 2 3] -> gradient at that point
1102
+ 2. List of symbols: a:>[w b] -> [grad_w grad_b] (multi-param mode)
1103
+
1104
+ In multi-param mode, ``a`` should be a niladic function (loss)
1105
+ that references the parameter symbols.
1106
+
1107
+ Examples:
1108
+ {x^2}:>3.0 --> 6.0 (derivative of x^2 at x=3)
1109
+ {x^3}:>2.0 --> 12.0 (derivative of x^3 at x=2)
1110
+ {+/x^2}:>[1 2 3] --> [2 4 6] (gradient of sum of squares)
1111
+ loss:>[w b] --> [grad_w grad_b] (multi-param mode)
1112
+
1113
+ """
1114
+ # Check if b is a list of symbols (multi-param mode)
1115
+ if is_list(b) and len(b) > 0 and all(isinstance(p, KGSym) for p in b):
1116
+ return multi_grad_of_fn(klong, a, list(b))
1117
+ else:
1118
+ return grad_of_fn(klong, a, b)
1119
+
1120
+
977
1121
  def create_dyad_functions(klong):
978
- def _get_name(s):
979
- s = s.strip()
980
- i = s.index("a")
981
- return s[i+1:i+s.index('b')]
982
-
983
- registry = {}
984
-
985
- m = sys.modules[__name__]
986
- for name in filter(lambda n: n.startswith("eval_dyad_"), dir(m)):
987
- fn = getattr(m,name)
988
- name = _get_name(fn.__doc__)
989
- if fn.__code__.co_argcount == 3:
990
- fn = lambda x,y,f=fn,klong=klong: f(klong, x, y)
991
- elif fn.__code__.co_argcount == 2 and 'klong' in fn.__code__.co_varnames:
992
- fn = lambda x,f=fn,klong=klong: f(klong, x)
993
- registry[name] = fn
994
-
995
- return registry
1122
+ backend = klong._backend
1123
+
1124
+ # Simple dyads that don't need backend or klong
1125
+ simple = {
1126
+ ':-': eval_dyad_amend_in_depth,
1127
+ '_': eval_dyad_drop,
1128
+ ':@': eval_dyad_index_in_depth,
1129
+ }
1130
+
1131
+ # Dyads needing backend
1132
+ backend_dyads = {
1133
+ '+': lambda a, b: eval_dyad_add(a, b, backend),
1134
+ '|': lambda a, b: eval_dyad_maximum(a, b, backend),
1135
+ '&': lambda a, b: eval_dyad_minimum(a, b, backend),
1136
+ '!': lambda a, b: eval_dyad_remainder(a, b, backend),
1137
+ '%': lambda a, b: eval_dyad_divide(a, b, backend),
1138
+ '*': lambda a, b: eval_dyad_multiply(a, b, backend),
1139
+ '-': lambda a, b: eval_dyad_subtract(a, b, backend),
1140
+ ':=': lambda a, b: eval_dyad_amend(a, b, backend),
1141
+ ':_': lambda a, b: eval_dyad_cut(a, b, backend),
1142
+ '=': lambda a, b: eval_dyad_equal(a, b, backend),
1143
+ '?': lambda a, b: eval_dyad_find(a, b, backend),
1144
+ ':$': lambda a, b: eval_dyad_form(a, b, backend),
1145
+ '$': lambda a, b: eval_dyad_format2(a, b, backend),
1146
+ ':%': lambda a, b: eval_dyad_integer_divide(a, b, backend),
1147
+ ',': lambda a, b: eval_dyad_join(a, b, backend),
1148
+ '<': lambda a, b: eval_dyad_less(a, b, backend),
1149
+ '~': lambda a, b: eval_dyad_match(a, b, backend),
1150
+ '>': lambda a, b: eval_dyad_more(a, b, backend),
1151
+ '^': lambda a, b: eval_dyad_power(a, b, backend),
1152
+ ':^': lambda a, b: eval_dyad_reshape(a, b, backend),
1153
+ ':+': lambda a, b: eval_dyad_rotate(a, b, backend),
1154
+ ':#': lambda a, b: eval_dyad_split(a, b, backend),
1155
+ '#': lambda a, b: eval_dyad_take(a, b, backend),
1156
+ }
1157
+
1158
+ # Dyads needing klong
1159
+ klong_dyads = {
1160
+ '@': lambda a, b: eval_dyad_at_index(klong, a, b),
1161
+ '::': lambda a, b: eval_dyad_define(klong, a, b),
1162
+ '∇': lambda a, b: eval_dyad_grad(klong, a, b),
1163
+ '∂': lambda a, b: eval_dyad_jacobian(klong, a, b),
1164
+ ':>': lambda a, b: eval_dyad_autograd(klong, a, b),
1165
+ }
1166
+
1167
+ return {**simple, **backend_dyads, **klong_dyads}