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/backend.py CHANGED
@@ -1,103 +1,38 @@
1
- import os
2
- import warnings
3
-
4
- # Attempt to import CuPy. If not available, set use_gpu to False.
5
- use_gpu = bool(os.environ.get('USE_GPU') == '1')
6
- if use_gpu:
7
- try:
8
- import cupy as np
9
- use_gpu = True
10
- except ImportError:
11
- import numpy as np
12
- use_gpu = False
13
- else:
14
- import numpy as np
15
-
16
-
17
- def is_supported_type(x):
18
- """
19
- CuPy does not support strings or jagged arrays.
20
- Note: add any other unsupported types here.
21
- """
22
- if isinstance(x, str) or is_jagged_array(x):
23
- return False
24
- return True
25
-
26
-
27
- def is_jagged_array(x):
28
- """
29
- Check if an array is jagged.
30
- """
31
- if isinstance(x, list):
32
- # If the lengths of sublists vary, it's a jagged array.
33
- return len(set(map(len, x))) > 1
34
- return False
35
-
36
- if use_gpu:
37
- import cupy
38
- import numpy
39
-
40
- class CuPyReductionKernelWrapper:
41
- def __init__(self, fn, reduce_fn_1, reduce_fn_2):
42
- self.fn = fn
43
- self.reduce_fn_1 = reduce_fn_1
44
- self.reduce_fn_2 = reduce_fn_2
45
-
46
- def __call__(self, *args, **kwargs):
47
- return self.fn(*args, **kwargs)
48
-
49
- def reduce(self, x):
50
- return self.reduce_fn_1(x) if x.ndim == 1 else self.reduce_fn_2(x[0], x[1])
51
-
52
- add_reduce_2 = cupy.ElementwiseKernel(
53
- 'T x, T y',
54
- 'T z',
55
- 'z = (x + y)',
56
- 'add_reduce_2')
57
- np.add = CuPyReductionKernelWrapper(cupy.add, cupy.sum, add_reduce_2)
58
-
59
- def subtract_reduce_1(x):
60
- return 2*x[0] - cupy.sum(x)
61
-
62
- subtract_reduce_2 = cupy.ElementwiseKernel(
63
- 'T x, T y',
64
- 'T z',
65
- 'z = (x - y)',
66
- 'subtract_reduce_2')
67
- np.subtract = CuPyReductionKernelWrapper(cupy.subtract, subtract_reduce_1, subtract_reduce_2)
68
-
69
- multiply_reduce_1 = cupy.ReductionKernel(
70
- 'T x',
71
- 'T y',
72
- 'x',
73
- 'a * b',
74
- 'y = a',
75
- '1',
76
- 'multiply_reduce_1'
77
- )
78
- multiply_reduce_2 = cupy.ElementwiseKernel(
79
- 'T x, T y',
80
- 'T z',
81
- 'z = (x * y)',
82
- 'multiply_reduce_2')
83
- np.multiply = CuPyReductionKernelWrapper(cupy.multiply, multiply_reduce_1, multiply_reduce_2)
84
-
85
- def divide_reduce_1(x):
86
- raise NotImplementedError()
87
-
88
- divide_reduce_2 = cupy.ElementwiseKernel(
89
- 'T x, T y',
90
- 'T z',
91
- 'z = (x / y)',
92
- 'divide_reduce_2')
93
- np.divide = CuPyReductionKernelWrapper(cupy.divide, divide_reduce_1, divide_reduce_2)
94
-
95
- np.isarray = lambda x: isinstance(x, (numpy.ndarray, cupy.ndarray))
96
-
97
- # np.hstack = lambda x: cupy.hstack(x) if use_gpu and is_supported_type(x) else numpy.hstack(x)
98
- else:
99
- np.seterr(divide='ignore')
100
- warnings.filterwarnings("error", category=np.VisibleDeprecationWarning)
101
- np.isarray = lambda x: isinstance(x, np.ndarray)
102
-
103
- np
1
+ """
2
+ Backend module for KlongPy.
3
+
4
+ Prefer using the backends package directly:
5
+
6
+ from klongpy.backends import get_backend, BackendProvider
7
+
8
+ For per-interpreter backends, use:
9
+
10
+ klong = KlongInterpreter(backend='torch')
11
+ """
12
+ from .backends.base import (
13
+ BackendProvider,
14
+ UnsupportedDtypeError,
15
+ is_jagged_array,
16
+ is_supported_type,
17
+ )
18
+ from .backends.numpy_backend import KGChar, NumpyBackendProvider
19
+ from .backends.registry import get_backend, list_backends, register_backend, TorchBackendProvider
20
+
21
+ _default_np_backend = get_backend('numpy')
22
+ np = _default_np_backend.np
23
+ bknp = np
24
+
25
+ __all__ = [
26
+ 'np',
27
+ 'bknp',
28
+ 'get_backend',
29
+ 'register_backend',
30
+ 'list_backends',
31
+ 'BackendProvider',
32
+ 'UnsupportedDtypeError',
33
+ 'NumpyBackendProvider',
34
+ 'TorchBackendProvider',
35
+ 'KGChar',
36
+ 'is_supported_type',
37
+ 'is_jagged_array',
38
+ ]
@@ -0,0 +1,26 @@
1
+ """
2
+ Backend public API for KlongPy.
3
+
4
+ Re-exports registry helpers and core backend types.
5
+ """
6
+ from .base import (
7
+ BackendProvider,
8
+ UnsupportedDtypeError,
9
+ is_jagged_array,
10
+ is_supported_type,
11
+ )
12
+ from .numpy_backend import NumpyBackendProvider, KGChar
13
+ from .registry import get_backend, list_backends, register_backend, TorchBackendProvider
14
+
15
+ __all__ = [
16
+ 'BackendProvider',
17
+ 'UnsupportedDtypeError',
18
+ 'NumpyBackendProvider',
19
+ 'TorchBackendProvider',
20
+ 'KGChar',
21
+ 'get_backend',
22
+ 'register_backend',
23
+ 'list_backends',
24
+ 'is_jagged_array',
25
+ 'is_supported_type',
26
+ ]
@@ -0,0 +1,469 @@
1
+ """
2
+ Base interface for array backends.
3
+
4
+ All backends must implement the BackendProvider interface to ensure
5
+ consistent behavior across numpy, torch, and any future backends.
6
+ """
7
+ from abc import ABC, abstractmethod
8
+ import numpy as np
9
+
10
+
11
+ def is_jagged_array(x):
12
+ """Check if x is a jagged (ragged) array - a list of lists with different lengths."""
13
+ if isinstance(x, list) and len(x) > 0:
14
+ if all(isinstance(item, (list, tuple)) for item in x):
15
+ return len(set(map(len, x))) > 1
16
+ return False
17
+
18
+
19
+ def is_supported_type(x):
20
+ """Check if x can be converted to a tensor/array by the current backend.
21
+
22
+ Default implementation returns True for everything except strings and jagged arrays.
23
+ """
24
+ return not (isinstance(x, str) or is_jagged_array(x))
25
+
26
+
27
+ class BackendProvider(ABC):
28
+ """Abstract interface for array backends."""
29
+
30
+ @property
31
+ @abstractmethod
32
+ def name(self) -> str:
33
+ """Return the backend name."""
34
+ pass
35
+
36
+ @property
37
+ @abstractmethod
38
+ def np(self):
39
+ """Return numpy-compatible array module."""
40
+ pass
41
+
42
+ @abstractmethod
43
+ def supports_object_dtype(self) -> bool:
44
+ """Whether this backend supports object dtype."""
45
+ pass
46
+
47
+ @abstractmethod
48
+ def supports_strings(self) -> bool:
49
+ """Whether this backend supports string operations."""
50
+ pass
51
+
52
+ @abstractmethod
53
+ def supports_float64(self) -> bool:
54
+ """Whether this backend supports float64 (double precision)."""
55
+ pass
56
+
57
+ @abstractmethod
58
+ def str_to_char_array(self, s):
59
+ """Convert string to character array."""
60
+ pass
61
+
62
+ @abstractmethod
63
+ def kg_asarray(self, a):
64
+ """
65
+ Klong-specific array conversion.
66
+
67
+ Converts input data into an array suitable for Klong operations.
68
+ For backends that don't support object dtype, this should raise
69
+ an appropriate exception for unsupported data types.
70
+ """
71
+ pass
72
+
73
+ @abstractmethod
74
+ def is_array(self, x) -> bool:
75
+ """Check if x is an array type for this backend."""
76
+ pass
77
+
78
+ @abstractmethod
79
+ def is_backend_array(self, x) -> bool:
80
+ """Check if x is specifically this backend's array type (not numpy)."""
81
+ pass
82
+
83
+ @abstractmethod
84
+ def get_dtype_kind(self, arr) -> str:
85
+ """
86
+ Get the dtype 'kind' character for an array.
87
+
88
+ Returns:
89
+ 'O' for object dtype
90
+ 'i' for integer types
91
+ 'f' for float types
92
+ 'u' for unsigned integer
93
+ 'b' for boolean
94
+ 'c' for complex
95
+ None if not an array
96
+ """
97
+ pass
98
+
99
+ @abstractmethod
100
+ def to_numpy(self, x):
101
+ """
102
+ Convert backend array to numpy array.
103
+
104
+ Handles device transfers (e.g., GPU to CPU) and gradient detachment.
105
+ """
106
+ pass
107
+
108
+ def to_display(self, x):
109
+ """
110
+ Convert backend array to display-friendly format.
111
+
112
+ For display purposes, converts arrays to numpy for consistent formatting.
113
+ 0-dim arrays are converted to Python scalars.
114
+ Override in subclasses if different behavior is needed.
115
+ """
116
+ if self.is_backend_array(x):
117
+ x = self.to_numpy(x)
118
+ # Convert 0-dim arrays to Python scalars
119
+ if hasattr(x, 'item') and hasattr(x, 'ndim') and x.ndim == 0:
120
+ return x.item()
121
+ return x
122
+
123
+ @abstractmethod
124
+ def is_scalar_integer(self, x) -> bool:
125
+ """Check if x is a 0-dim integer array/tensor."""
126
+ pass
127
+
128
+ @abstractmethod
129
+ def is_scalar_float(self, x) -> bool:
130
+ """Check if x is a 0-dim float array/tensor."""
131
+ pass
132
+
133
+ def scalar_to_python(self, x):
134
+ """Convert a 0-dim array/tensor to Python scalar."""
135
+ if hasattr(x, 'item'):
136
+ return x.item()
137
+ return x
138
+
139
+ @abstractmethod
140
+ def argsort(self, a, descending=False):
141
+ """Return indices that would sort the array."""
142
+ pass
143
+
144
+ def is_integer(self, x) -> bool:
145
+ """Check if x is an integer type (scalar, numpy integer, or 0-dim integer tensor)."""
146
+ if issubclass(type(x), (int, np.integer)):
147
+ return True
148
+ return self.is_scalar_integer(x)
149
+
150
+ def is_float(self, x) -> bool:
151
+ """Check if x is a float type (scalar, numpy float, int, or 0-dim float tensor)."""
152
+ if issubclass(type(x), (float, np.floating, int)):
153
+ return True
154
+ return self.is_scalar_float(x) or self.is_scalar_integer(x)
155
+
156
+ def is_number(self, a) -> bool:
157
+ """Check if a is a number (integer or float)."""
158
+ return self.is_float(a) or self.is_integer(a)
159
+
160
+ def str_to_chr_arr(self, s):
161
+ """Convert string to character array (alias for str_to_char_array)."""
162
+ return self.str_to_char_array(s)
163
+
164
+ @abstractmethod
165
+ def array_size(self, a):
166
+ """
167
+ Get the total number of elements in an array/tensor.
168
+
169
+ Works with both numpy arrays and torch tensors.
170
+
171
+ Returns:
172
+ int: Total element count (product of all dimensions)
173
+ """
174
+ pass
175
+
176
+ def safe_equal(self, x, y):
177
+ """
178
+ Compare two values for equality, handling backend-specific array types.
179
+
180
+ Returns a truth value (0 or 1) suitable for Klong.
181
+ """
182
+ return np.asarray(x, dtype=object) == np.asarray(y, dtype=object)
183
+
184
+ def detach_if_needed(self, x):
185
+ """
186
+ Detach array from computation graph if needed.
187
+
188
+ For backends without autograd, this is a no-op.
189
+ """
190
+ return x
191
+
192
+ def to_int_array(self, a):
193
+ """
194
+ Convert array to integer type.
195
+ """
196
+ return np.asarray(a, dtype=int) if self.is_array(a) else int(a)
197
+
198
+ def floor_to_int(self, a):
199
+ """
200
+ Floor a value and convert to integer.
201
+ """
202
+ result = np.floor(np.asarray(a, dtype=float))
203
+ return result.astype(int) if hasattr(result, 'astype') else int(result)
204
+
205
+ def power(self, a, b):
206
+ """
207
+ Compute a^b, handling gradient tracking if applicable.
208
+
209
+ Returns integer result if the result is a whole number.
210
+ """
211
+ r = np.power(float(a) if isinstance(a, (int, np.integer)) else a, b)
212
+ return r
213
+
214
+ def has_gradient(self, x) -> bool:
215
+ """Check if x is tracking gradients (for autograd)."""
216
+ return False
217
+
218
+ def supports_autograd(self) -> bool:
219
+ """Whether this backend supports automatic differentiation."""
220
+ return False
221
+
222
+ def array_equal(self, a, b) -> bool:
223
+ """Backend-native exact equality for arrays/tensors."""
224
+ return bool(np.array_equal(a, b))
225
+
226
+ def create_grad_tensor(self, x):
227
+ """Create a tensor that tracks gradients. Raises if not supported."""
228
+ raise NotImplementedError("This backend does not support autograd")
229
+
230
+ def compute_autograd(self, func, x):
231
+ """Compute gradient using automatic differentiation. Raises if not supported."""
232
+ raise NotImplementedError("This backend does not support autograd")
233
+
234
+ def compute_multi_autograd(self, func, params):
235
+ """
236
+ Compute gradients for multiple parameters in one backward pass.
237
+
238
+ Args:
239
+ func: Callable that takes a list of tensors and returns a scalar loss
240
+ params: List of parameter values to compute gradients for
241
+
242
+ Returns:
243
+ List of gradients, one per parameter
244
+ """
245
+ raise NotImplementedError("This backend does not support multi-parameter autograd")
246
+
247
+ def compute_jacobian(self, func, x):
248
+ """
249
+ Compute Jacobian matrix of func at point x.
250
+
251
+ Args:
252
+ func: Callable that takes x and returns a vector
253
+ x: Input point (tensor/array)
254
+
255
+ Returns:
256
+ Jacobian matrix J where J[i,j] = df_i/dx_j
257
+ """
258
+ raise NotImplementedError("This backend does not support Jacobian computation")
259
+
260
+ def compile_function(self, func, example_input, output_path=None, mode="default",
261
+ backend="inductor", fullgraph=False, dynamic=None):
262
+ """
263
+ Compile a function for optimized execution and optionally export for inspection.
264
+
265
+ Args:
266
+ func: Callable to compile
267
+ example_input: Example input for tracing the function
268
+ output_path: Optional path to export the compiled graph
269
+ mode: Compilation mode ("default", "reduce-overhead", "max-autotune")
270
+ backend: Compilation backend ("inductor", "eager", "cudagraphs")
271
+ fullgraph: If True, requires entire function to compile as one graph
272
+ dynamic: If True, enables dynamic shapes
273
+
274
+ Returns:
275
+ Compiled function or export info dict
276
+ """
277
+ raise NotImplementedError("This backend does not support function compilation")
278
+
279
+ def get_compile_modes(self):
280
+ """
281
+ Return information about available compilation modes.
282
+
283
+ Returns:
284
+ Dict with mode descriptions and recommendations
285
+ """
286
+ raise NotImplementedError("This backend does not support function compilation")
287
+
288
+ def gradcheck(self, func, inputs, eps=1e-6, atol=1e-5, rtol=1e-3):
289
+ """
290
+ Check gradients computed by autograd against numeric gradients.
291
+
292
+ Args:
293
+ func: Function to check
294
+ inputs: Tuple of input tensors
295
+ eps: Step size for numeric differentiation
296
+ atol: Absolute tolerance
297
+ rtol: Relative tolerance
298
+
299
+ Returns:
300
+ True if gradients match, raises error otherwise
301
+ """
302
+ raise NotImplementedError("This backend does not support gradcheck")
303
+
304
+ def klong_gradcheck(self, klong, fn, inputs):
305
+ """
306
+ Check gradients for a Klong function.
307
+
308
+ This is a higher-level interface that handles wrapping the Klong function
309
+ and converting inputs appropriately for the backend.
310
+
311
+ Args:
312
+ klong: KlongInterpreter instance
313
+ fn: Klong function to check
314
+ inputs: Input value or list of inputs
315
+
316
+ Returns:
317
+ 1 if gradients are correct, raises error otherwise
318
+ """
319
+ raise RuntimeError(
320
+ ".gradcheck() requires PyTorch backend. "
321
+ "Run with USE_TORCH=1 environment variable."
322
+ )
323
+
324
+
325
+ def kg_equal(self, a, b):
326
+ """Compare two values or arrays for equality, handling nested arrays and tensors."""
327
+ if a is b:
328
+ return True
329
+
330
+ # Backend-native comparison for backend arrays
331
+ if self.is_backend_array(a) and self.is_backend_array(b):
332
+ return self.array_equal(a, b)
333
+
334
+ # Fast path for numpy arrays (non-object)
335
+ if isinstance(a, np.ndarray) and isinstance(b, np.ndarray):
336
+ if a.dtype != object and b.dtype != object:
337
+ return bool(np.array_equal(a, b))
338
+
339
+ # Convert backend arrays to numpy for mixed comparisons
340
+ if self.is_backend_array(a):
341
+ a = self.to_numpy(a)
342
+ if self.is_backend_array(b):
343
+ b = self.to_numpy(b)
344
+
345
+ # Fast path for numpy arrays (after any conversion)
346
+ if isinstance(a, np.ndarray) and isinstance(b, np.ndarray):
347
+ if a.dtype != object and b.dtype != object:
348
+ return bool(np.array_equal(a, b))
349
+
350
+ # Normalize 0-d numpy arrays to scalars for mixed comparisons
351
+ if isinstance(a, np.ndarray) and a.ndim == 0:
352
+ a = a.item()
353
+ if isinstance(b, np.ndarray) and b.ndim == 0:
354
+ b = b.item()
355
+
356
+ # List/sequence comparison
357
+ a_is_seq = isinstance(a, (list, tuple)) or (isinstance(a, np.ndarray) and a.ndim > 0)
358
+ b_is_seq = isinstance(b, (list, tuple)) or (isinstance(b, np.ndarray) and b.ndim > 0)
359
+ if a_is_seq or b_is_seq:
360
+ if not (a_is_seq and b_is_seq):
361
+ return False
362
+ if isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)):
363
+ def _is_int_scalar(x):
364
+ return isinstance(x, (int, bool, np.integer))
365
+ if len(a) == len(b) and len(a) >= 32 and all(_is_int_scalar(x) for x in a) and all(_is_int_scalar(y) for y in b):
366
+ return a == b
367
+ # Fast path for object numpy arrays when possible
368
+ if isinstance(a, np.ndarray) and isinstance(b, np.ndarray) and a.dtype == object and b.dtype == object:
369
+ if a.size >= 128:
370
+ try:
371
+ return bool(np.array_equal(a, b))
372
+ except Exception:
373
+ pass
374
+ if len(a) != len(b):
375
+ return False
376
+ return all(self.kg_equal(x, y) for x, y in zip(a, b))
377
+
378
+ # Numeric scalars: tolerant comparison
379
+ if self.is_number(a) and self.is_number(b):
380
+ result = np.isclose(a, b)
381
+ if hasattr(result, 'item'):
382
+ return bool(result.item())
383
+ return bool(result)
384
+
385
+ # Fallback: direct equality
386
+ result = a == b
387
+ if hasattr(result, 'all'):
388
+ return bool(result.all())
389
+ if hasattr(result, 'item'):
390
+ return bool(result.item())
391
+ return bool(result)
392
+
393
+ def vec_fn(self, a, f):
394
+ """
395
+ Apply function f to array a, with support for nested object arrays.
396
+ """
397
+ if self.np.isarray(a) and a.dtype == 'O':
398
+ result = [self.vec_fn(x, f) if self._is_list(x) else f(x) for x in a]
399
+ return np.asarray(result, dtype=object)
400
+ return f(a)
401
+
402
+ def vec_fn2(self, a, b, f):
403
+ """
404
+ Apply function f to elements of a and b, handling nested structures.
405
+ """
406
+ if self.np.isarray(a):
407
+ if a.dtype == 'O':
408
+ if self.np.isarray(b):
409
+ assert len(a) == len(b)
410
+ return self.kg_asarray([self.vec_fn2(x, y, f) for x, y in zip(a, b)])
411
+ else:
412
+ return self.kg_asarray([self.vec_fn2(x, b, f) for x in a])
413
+ elif self.np.isarray(b) and b.dtype == 'O':
414
+ assert len(a) == len(b)
415
+ return self.kg_asarray([self.vec_fn2(x, y, f) for x, y in zip(a, b)])
416
+ elif self.np.isarray(b) and b.dtype == 'O':
417
+ return self.kg_asarray([self.vec_fn2(a, x, f) for x in b])
418
+ return f(a, b)
419
+
420
+ def rec_fn(self, a, f):
421
+ """
422
+ Recursively apply function f to all elements of a nested structure.
423
+ """
424
+ return self.kg_asarray([self.rec_fn(x, f) for x in a]) if self._is_list(a) else f(a)
425
+
426
+ def _is_list(self, x):
427
+ """Check if x is a list-like structure (array or list, non-empty)."""
428
+ if isinstance(x, np.ndarray):
429
+ return x.size > 0
430
+ if isinstance(x, (list, tuple)):
431
+ return len(x) > 0
432
+ return False
433
+
434
+ @property
435
+ def device(self):
436
+ """Return the current device for this backend (e.g., 'cpu', 'cuda:0', 'mps')."""
437
+ return 'cpu'
438
+
439
+ def list_devices(self):
440
+ """
441
+ List available devices for this backend.
442
+
443
+ Returns:
444
+ list: List of available device names (e.g., ['cpu'], ['cpu', 'cuda:0', 'mps'])
445
+ """
446
+ return ['cpu']
447
+
448
+ def get_info(self):
449
+ """
450
+ Get comprehensive information about this backend.
451
+
452
+ Returns:
453
+ dict: Dictionary with backend name, current device, available devices,
454
+ and feature support flags.
455
+ """
456
+ return {
457
+ 'name': self.name,
458
+ 'device': self.device,
459
+ 'devices': self.list_devices(),
460
+ 'supports_float64': self.supports_float64(),
461
+ 'supports_strings': self.supports_strings(),
462
+ 'supports_object_dtype': self.supports_object_dtype(),
463
+ 'supports_autograd': self.supports_autograd(),
464
+ }
465
+
466
+
467
+ class UnsupportedDtypeError(Exception):
468
+ """Raised when an operation requires a dtype not supported by the backend."""
469
+ pass