klongpy 0.6.8__py3-none-any.whl → 0.7.0__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.
- klongpy/__init__.py +19 -1
- klongpy/adverbs.py +5 -5
- klongpy/autograd.py +308 -0
- klongpy/backend.py +167 -99
- klongpy/backends/__init__.py +94 -0
- klongpy/backends/base.py +320 -0
- klongpy/backends/numpy_backend.py +122 -0
- klongpy/backends/torch_backend.py +995 -0
- klongpy-0.6.8.data/scripts/kgpy → klongpy/cli.py +65 -88
- klongpy/core.py +228 -106
- klongpy/db/sys_fn_db.py +4 -3
- klongpy/dyads.py +173 -32
- klongpy/interpreter.py +31 -3
- klongpy/lib/help.kg +2 -2
- klongpy/monads.py +49 -12
- klongpy/repl.py +91 -0
- klongpy/sys_fn.py +129 -18
- klongpy/sys_fn_autograd.py +290 -0
- klongpy/sys_fn_ipc.py +18 -7
- klongpy/sys_fn_timer.py +13 -3
- klongpy/web/sys_fn_web.py +28 -6
- klongpy-0.7.0.dist-info/METADATA +493 -0
- klongpy-0.7.0.dist-info/RECORD +48 -0
- {klongpy-0.6.8.dist-info → klongpy-0.7.0.dist-info}/WHEEL +1 -1
- klongpy-0.7.0.dist-info/entry_points.txt +2 -0
- {klongpy-0.6.8.dist-info → klongpy-0.7.0.dist-info}/top_level.txt +0 -1
- klongpy-0.6.8.dist-info/METADATA +0 -412
- klongpy-0.6.8.dist-info/RECORD +0 -72
- tests/__init__.py +0 -6
- tests/gen_join_over.py +0 -119
- tests/gen_py_suite.py +0 -77
- tests/gen_test_fn.py +0 -259
- tests/perf_async.py +0 -25
- tests/perf_avg.py +0 -18
- tests/perf_duckdb.py +0 -32
- tests/perf_gen.py +0 -38
- tests/perf_ipc_overhead.py +0 -34
- tests/perf_join.py +0 -53
- tests/perf_load.py +0 -17
- tests/perf_prog.py +0 -18
- tests/perf_serdes.py +0 -52
- tests/perf_sys_fn_db.py +0 -263
- tests/perf_vector.py +0 -40
- tests/test_accel.py +0 -227
- tests/test_df_cache.py +0 -85
- tests/test_examples.py +0 -64
- tests/test_extra_suite.py +0 -382
- tests/test_file_cache.py +0 -185
- tests/test_interop.py +0 -181
- tests/test_kgtests.py +0 -65
- tests/test_known_bugs.py +0 -206
- tests/test_prog.py +0 -107
- tests/test_suite.py +0 -1479
- tests/test_suite_file.py +0 -153
- tests/test_sys_fn.py +0 -420
- tests/test_sys_fn_db.py +0 -88
- tests/test_sys_fn_ipc.py +0 -587
- tests/test_sys_fn_timer.py +0 -133
- tests/test_util.py +0 -233
- tests/utils.py +0 -126
- {klongpy-0.6.8.dist-info → klongpy-0.7.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Backend registry for KlongPy.
|
|
3
|
+
|
|
4
|
+
Provides a unified interface for registering and retrieving array backends.
|
|
5
|
+
The default backend is 'numpy'.
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
from .base import BackendProvider, UnsupportedDtypeError, is_jagged_array, is_supported_type
|
|
10
|
+
from .numpy_backend import NumpyBackendProvider, KGChar
|
|
11
|
+
|
|
12
|
+
# Registry of available backends
|
|
13
|
+
_BACKENDS = {}
|
|
14
|
+
|
|
15
|
+
# Default backend name
|
|
16
|
+
_DEFAULT_BACKEND = 'numpy'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def register_backend(name: str, provider_class):
|
|
20
|
+
"""Register a backend provider class."""
|
|
21
|
+
_BACKENDS[name] = provider_class
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_backend(name: str = None, **kwargs) -> BackendProvider:
|
|
25
|
+
"""
|
|
26
|
+
Get a backend provider instance.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
name : str, optional
|
|
31
|
+
Backend name ('numpy' or 'torch'). If None, uses default.
|
|
32
|
+
**kwargs
|
|
33
|
+
Additional arguments passed to the backend provider constructor.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
BackendProvider
|
|
38
|
+
The backend provider instance.
|
|
39
|
+
"""
|
|
40
|
+
if name is None:
|
|
41
|
+
# Check environment variable for default
|
|
42
|
+
env_backend = os.environ.get('KLONGPY_BACKEND', '').lower()
|
|
43
|
+
if env_backend == 'torch' or os.environ.get('USE_TORCH') == '1':
|
|
44
|
+
name = 'torch'
|
|
45
|
+
else:
|
|
46
|
+
name = _DEFAULT_BACKEND
|
|
47
|
+
|
|
48
|
+
if name not in _BACKENDS:
|
|
49
|
+
available = ', '.join(_BACKENDS.keys())
|
|
50
|
+
raise ValueError(f"Unknown backend: '{name}'. Available: {available}")
|
|
51
|
+
|
|
52
|
+
return _BACKENDS[name](**kwargs)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def list_backends():
|
|
56
|
+
"""Return list of available backend names."""
|
|
57
|
+
return list(_BACKENDS.keys())
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def set_default_backend(name: str):
|
|
61
|
+
"""Set the default backend name."""
|
|
62
|
+
global _DEFAULT_BACKEND
|
|
63
|
+
if name not in _BACKENDS:
|
|
64
|
+
raise ValueError(f"Unknown backend: '{name}'")
|
|
65
|
+
_DEFAULT_BACKEND = name
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Register built-in backends
|
|
69
|
+
register_backend('numpy', NumpyBackendProvider)
|
|
70
|
+
|
|
71
|
+
# Try to register torch backend if available
|
|
72
|
+
try:
|
|
73
|
+
from .torch_backend import TorchBackendProvider, TorchUnsupportedDtypeError
|
|
74
|
+
register_backend('torch', TorchBackendProvider)
|
|
75
|
+
except ImportError:
|
|
76
|
+
# Torch not available
|
|
77
|
+
TorchBackendProvider = None
|
|
78
|
+
TorchUnsupportedDtypeError = UnsupportedDtypeError
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = [
|
|
82
|
+
'BackendProvider',
|
|
83
|
+
'UnsupportedDtypeError',
|
|
84
|
+
'TorchUnsupportedDtypeError',
|
|
85
|
+
'NumpyBackendProvider',
|
|
86
|
+
'TorchBackendProvider',
|
|
87
|
+
'KGChar',
|
|
88
|
+
'get_backend',
|
|
89
|
+
'register_backend',
|
|
90
|
+
'list_backends',
|
|
91
|
+
'set_default_backend',
|
|
92
|
+
'is_jagged_array',
|
|
93
|
+
'is_supported_type',
|
|
94
|
+
]
|
klongpy/backends/base.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
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
|
+
|
|
9
|
+
|
|
10
|
+
def is_jagged_array(x):
|
|
11
|
+
"""Check if x is a jagged (ragged) array - a list of lists with different lengths."""
|
|
12
|
+
if isinstance(x, list) and len(x) > 0:
|
|
13
|
+
if all(isinstance(item, (list, tuple)) for item in x):
|
|
14
|
+
return len(set(map(len, x))) > 1
|
|
15
|
+
return False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def is_supported_type(x):
|
|
19
|
+
"""Check if x can be converted to a tensor/array by the current backend.
|
|
20
|
+
|
|
21
|
+
Default implementation returns True for everything except strings and jagged arrays.
|
|
22
|
+
"""
|
|
23
|
+
return not (isinstance(x, str) or is_jagged_array(x))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BackendProvider(ABC):
|
|
27
|
+
"""Abstract interface for array backends."""
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def name(self) -> str:
|
|
32
|
+
"""Return the backend name."""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def np(self):
|
|
38
|
+
"""Return numpy-compatible array module."""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def supports_object_dtype(self) -> bool:
|
|
43
|
+
"""Whether this backend supports object dtype."""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def supports_strings(self) -> bool:
|
|
48
|
+
"""Whether this backend supports string operations."""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def supports_float64(self) -> bool:
|
|
53
|
+
"""Whether this backend supports float64 (double precision)."""
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def str_to_char_array(self, s):
|
|
58
|
+
"""Convert string to character array."""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def kg_asarray(self, a):
|
|
63
|
+
"""
|
|
64
|
+
Klong-specific array conversion.
|
|
65
|
+
|
|
66
|
+
Converts input data into an array suitable for Klong operations.
|
|
67
|
+
For backends that don't support object dtype, this should raise
|
|
68
|
+
an appropriate exception for unsupported data types.
|
|
69
|
+
"""
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def is_array(self, x) -> bool:
|
|
74
|
+
"""Check if x is an array type for this backend."""
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def is_backend_array(self, x) -> bool:
|
|
79
|
+
"""Check if x is specifically this backend's array type (not numpy)."""
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
@abstractmethod
|
|
83
|
+
def get_dtype_kind(self, arr) -> str:
|
|
84
|
+
"""
|
|
85
|
+
Get the dtype 'kind' character for an array.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
'O' for object dtype
|
|
89
|
+
'i' for integer types
|
|
90
|
+
'f' for float types
|
|
91
|
+
'u' for unsigned integer
|
|
92
|
+
'b' for boolean
|
|
93
|
+
'c' for complex
|
|
94
|
+
None if not an array
|
|
95
|
+
"""
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
@abstractmethod
|
|
99
|
+
def to_numpy(self, x):
|
|
100
|
+
"""
|
|
101
|
+
Convert backend array to numpy array.
|
|
102
|
+
|
|
103
|
+
Handles device transfers (e.g., GPU to CPU) and gradient detachment.
|
|
104
|
+
"""
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
def to_display(self, x):
|
|
108
|
+
"""
|
|
109
|
+
Convert backend array to display-friendly format.
|
|
110
|
+
|
|
111
|
+
For display purposes, converts arrays to numpy for consistent formatting.
|
|
112
|
+
0-dim arrays are converted to Python scalars.
|
|
113
|
+
Override in subclasses if different behavior is needed.
|
|
114
|
+
"""
|
|
115
|
+
if self.is_backend_array(x):
|
|
116
|
+
x = self.to_numpy(x)
|
|
117
|
+
# Convert 0-dim arrays to Python scalars
|
|
118
|
+
if hasattr(x, 'item') and hasattr(x, 'ndim') and x.ndim == 0:
|
|
119
|
+
return x.item()
|
|
120
|
+
return x
|
|
121
|
+
|
|
122
|
+
@abstractmethod
|
|
123
|
+
def is_scalar_integer(self, x) -> bool:
|
|
124
|
+
"""Check if x is a 0-dim integer array/tensor."""
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
@abstractmethod
|
|
128
|
+
def is_scalar_float(self, x) -> bool:
|
|
129
|
+
"""Check if x is a 0-dim float array/tensor."""
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
def scalar_to_python(self, x):
|
|
133
|
+
"""Convert a 0-dim array/tensor to Python scalar."""
|
|
134
|
+
if hasattr(x, 'item'):
|
|
135
|
+
return x.item()
|
|
136
|
+
return x
|
|
137
|
+
|
|
138
|
+
@abstractmethod
|
|
139
|
+
def argsort(self, a, descending=False):
|
|
140
|
+
"""Return indices that would sort the array."""
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
def is_integer(self, x) -> bool:
|
|
144
|
+
"""Check if x is an integer type (scalar, numpy integer, or 0-dim integer tensor)."""
|
|
145
|
+
import numpy as np
|
|
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
|
+
import numpy as np
|
|
153
|
+
if issubclass(type(x), (float, np.floating, int)):
|
|
154
|
+
return True
|
|
155
|
+
return self.is_scalar_float(x) or self.is_scalar_integer(x)
|
|
156
|
+
|
|
157
|
+
def is_number(self, a) -> bool:
|
|
158
|
+
"""Check if a is a number (integer or float)."""
|
|
159
|
+
return self.is_float(a) or self.is_integer(a)
|
|
160
|
+
|
|
161
|
+
def str_to_chr_arr(self, s):
|
|
162
|
+
"""Convert string to character array (alias for str_to_char_array)."""
|
|
163
|
+
return self.str_to_char_array(s)
|
|
164
|
+
|
|
165
|
+
@abstractmethod
|
|
166
|
+
def array_size(self, a):
|
|
167
|
+
"""
|
|
168
|
+
Get the total number of elements in an array/tensor.
|
|
169
|
+
|
|
170
|
+
Works with both numpy arrays and torch tensors.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
int: Total element count (product of all dimensions)
|
|
174
|
+
"""
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
def safe_equal(self, x, y):
|
|
178
|
+
"""
|
|
179
|
+
Compare two values for equality, handling backend-specific array types.
|
|
180
|
+
|
|
181
|
+
Returns a truth value (0 or 1) suitable for Klong.
|
|
182
|
+
"""
|
|
183
|
+
import numpy as np
|
|
184
|
+
return np.asarray(x, dtype=object) == np.asarray(y, dtype=object)
|
|
185
|
+
|
|
186
|
+
def detach_if_needed(self, x):
|
|
187
|
+
"""
|
|
188
|
+
Detach array from computation graph if needed.
|
|
189
|
+
|
|
190
|
+
For backends without autograd, this is a no-op.
|
|
191
|
+
"""
|
|
192
|
+
return x
|
|
193
|
+
|
|
194
|
+
def to_int_array(self, a):
|
|
195
|
+
"""
|
|
196
|
+
Convert array to integer type.
|
|
197
|
+
"""
|
|
198
|
+
import numpy as np
|
|
199
|
+
return np.asarray(a, dtype=int) if self.is_array(a) else int(a)
|
|
200
|
+
|
|
201
|
+
def power(self, a, b):
|
|
202
|
+
"""
|
|
203
|
+
Compute a^b, handling gradient tracking if applicable.
|
|
204
|
+
|
|
205
|
+
Returns integer result if the result is a whole number.
|
|
206
|
+
"""
|
|
207
|
+
import numpy as np
|
|
208
|
+
r = np.power(float(a) if isinstance(a, (int, np.integer)) else a, b)
|
|
209
|
+
return r
|
|
210
|
+
|
|
211
|
+
def has_gradient(self, x) -> bool:
|
|
212
|
+
"""Check if x is tracking gradients (for autograd)."""
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
def supports_autograd(self) -> bool:
|
|
216
|
+
"""Whether this backend supports automatic differentiation."""
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
def create_grad_tensor(self, x):
|
|
220
|
+
"""Create a tensor that tracks gradients. Raises if not supported."""
|
|
221
|
+
raise NotImplementedError("This backend does not support autograd")
|
|
222
|
+
|
|
223
|
+
def compute_autograd(self, func, x):
|
|
224
|
+
"""Compute gradient using automatic differentiation. Raises if not supported."""
|
|
225
|
+
raise NotImplementedError("This backend does not support autograd")
|
|
226
|
+
|
|
227
|
+
def compute_multi_autograd(self, func, params):
|
|
228
|
+
"""
|
|
229
|
+
Compute gradients for multiple parameters in one backward pass.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
func: Callable that takes a list of tensors and returns a scalar loss
|
|
233
|
+
params: List of parameter values to compute gradients for
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
List of gradients, one per parameter
|
|
237
|
+
"""
|
|
238
|
+
raise NotImplementedError("This backend does not support multi-parameter autograd")
|
|
239
|
+
|
|
240
|
+
def compute_jacobian(self, func, x):
|
|
241
|
+
"""
|
|
242
|
+
Compute Jacobian matrix of func at point x.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
func: Callable that takes x and returns a vector
|
|
246
|
+
x: Input point (tensor/array)
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Jacobian matrix J where J[i,j] = df_i/dx_j
|
|
250
|
+
"""
|
|
251
|
+
raise NotImplementedError("This backend does not support Jacobian computation")
|
|
252
|
+
|
|
253
|
+
def compile_function(self, func, example_input, output_path=None, mode="default",
|
|
254
|
+
backend="inductor", fullgraph=False, dynamic=None):
|
|
255
|
+
"""
|
|
256
|
+
Compile a function for optimized execution and optionally export for inspection.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
func: Callable to compile
|
|
260
|
+
example_input: Example input for tracing the function
|
|
261
|
+
output_path: Optional path to export the compiled graph
|
|
262
|
+
mode: Compilation mode ("default", "reduce-overhead", "max-autotune")
|
|
263
|
+
backend: Compilation backend ("inductor", "eager", "cudagraphs")
|
|
264
|
+
fullgraph: If True, requires entire function to compile as one graph
|
|
265
|
+
dynamic: If True, enables dynamic shapes
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Compiled function or export info dict
|
|
269
|
+
"""
|
|
270
|
+
raise NotImplementedError("This backend does not support function compilation")
|
|
271
|
+
|
|
272
|
+
def get_compile_modes(self):
|
|
273
|
+
"""
|
|
274
|
+
Return information about available compilation modes.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Dict with mode descriptions and recommendations
|
|
278
|
+
"""
|
|
279
|
+
raise NotImplementedError("This backend does not support function compilation")
|
|
280
|
+
|
|
281
|
+
def gradcheck(self, func, inputs, eps=1e-6, atol=1e-5, rtol=1e-3):
|
|
282
|
+
"""
|
|
283
|
+
Check gradients computed by autograd against numeric gradients.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
func: Function to check
|
|
287
|
+
inputs: Tuple of input tensors
|
|
288
|
+
eps: Step size for numeric differentiation
|
|
289
|
+
atol: Absolute tolerance
|
|
290
|
+
rtol: Relative tolerance
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
True if gradients match, raises error otherwise
|
|
294
|
+
"""
|
|
295
|
+
raise NotImplementedError("This backend does not support gradcheck")
|
|
296
|
+
|
|
297
|
+
def klong_gradcheck(self, klong, fn, inputs):
|
|
298
|
+
"""
|
|
299
|
+
Check gradients for a Klong function.
|
|
300
|
+
|
|
301
|
+
This is a higher-level interface that handles wrapping the Klong function
|
|
302
|
+
and converting inputs appropriately for the backend.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
klong: KlongInterpreter instance
|
|
306
|
+
fn: Klong function to check
|
|
307
|
+
inputs: Input value or list of inputs
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
1 if gradients are correct, raises error otherwise
|
|
311
|
+
"""
|
|
312
|
+
raise RuntimeError(
|
|
313
|
+
".gradcheck() requires PyTorch backend. "
|
|
314
|
+
"Run with USE_TORCH=1 environment variable."
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class UnsupportedDtypeError(Exception):
|
|
319
|
+
"""Raised when an operation requires a dtype not supported by the backend."""
|
|
320
|
+
pass
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NumPy backend provider for KlongPy.
|
|
3
|
+
|
|
4
|
+
This is the default backend that supports all Klong operations including
|
|
5
|
+
string manipulation and object dtype arrays.
|
|
6
|
+
"""
|
|
7
|
+
import warnings
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from .base import BackendProvider
|
|
11
|
+
|
|
12
|
+
# numpy 2.x moved VisibleDeprecationWarning to numpy.exceptions
|
|
13
|
+
from numpy.exceptions import VisibleDeprecationWarning as NumpyVisibleDeprecationWarning
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class KGChar(str):
|
|
17
|
+
"""Character type for Klong."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NumpyBackendProvider(BackendProvider):
|
|
22
|
+
"""NumPy-based backend provider."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, device=None):
|
|
25
|
+
# device parameter is ignored for numpy backend (accepted for API consistency)
|
|
26
|
+
self._np = np
|
|
27
|
+
np.seterr(divide='ignore')
|
|
28
|
+
warnings.filterwarnings("error", category=NumpyVisibleDeprecationWarning)
|
|
29
|
+
# Add isarray method to numpy module reference
|
|
30
|
+
self._np.isarray = lambda x: isinstance(x, np.ndarray)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def name(self) -> str:
|
|
34
|
+
return 'numpy'
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def np(self):
|
|
38
|
+
return self._np
|
|
39
|
+
|
|
40
|
+
def supports_object_dtype(self) -> bool:
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
def supports_strings(self) -> bool:
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
def supports_float64(self) -> bool:
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
def is_array(self, x) -> bool:
|
|
50
|
+
return isinstance(x, np.ndarray)
|
|
51
|
+
|
|
52
|
+
def is_backend_array(self, x) -> bool:
|
|
53
|
+
return False # numpy arrays are the base case, not a "backend" type
|
|
54
|
+
|
|
55
|
+
def get_dtype_kind(self, arr) -> str:
|
|
56
|
+
if hasattr(arr, 'dtype') and hasattr(arr.dtype, 'kind'):
|
|
57
|
+
return arr.dtype.kind
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
def to_numpy(self, x):
|
|
61
|
+
# Already numpy, just return as-is
|
|
62
|
+
return x
|
|
63
|
+
|
|
64
|
+
def is_scalar_integer(self, x) -> bool:
|
|
65
|
+
if isinstance(x, np.ndarray) and x.ndim == 0:
|
|
66
|
+
return np.issubdtype(x.dtype, np.integer)
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
def is_scalar_float(self, x) -> bool:
|
|
70
|
+
if isinstance(x, np.ndarray) and x.ndim == 0:
|
|
71
|
+
return np.issubdtype(x.dtype, np.floating)
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
def argsort(self, a, descending=False):
|
|
75
|
+
"""Return indices that would sort the array."""
|
|
76
|
+
indices = np.argsort(a)
|
|
77
|
+
if descending:
|
|
78
|
+
indices = indices[::-1].copy()
|
|
79
|
+
return indices
|
|
80
|
+
|
|
81
|
+
def array_size(self, a):
|
|
82
|
+
"""Get the total number of elements in an array."""
|
|
83
|
+
if hasattr(a, 'size'):
|
|
84
|
+
return a.size
|
|
85
|
+
return len(a) if hasattr(a, '__len__') else 1
|
|
86
|
+
|
|
87
|
+
def safe_equal(self, x, y):
|
|
88
|
+
"""Compare two values for equality."""
|
|
89
|
+
return np.asarray(x, dtype=object) == np.asarray(y, dtype=object)
|
|
90
|
+
|
|
91
|
+
def to_int_array(self, a):
|
|
92
|
+
"""Convert array to integer type."""
|
|
93
|
+
return np.asarray(a, dtype=int) if self.is_array(a) else int(a)
|
|
94
|
+
|
|
95
|
+
def power(self, a, b):
|
|
96
|
+
"""Compute a^b, returning integer if result is whole number."""
|
|
97
|
+
r = np.power(float(a) if isinstance(a, (int, np.integer)) else a, b)
|
|
98
|
+
return r
|
|
99
|
+
|
|
100
|
+
def str_to_char_array(self, s):
|
|
101
|
+
"""Convert string to character array."""
|
|
102
|
+
return self._np.asarray([KGChar(x) for x in s], dtype=object)
|
|
103
|
+
|
|
104
|
+
def kg_asarray(self, a):
|
|
105
|
+
"""Convert input to numpy array, handling strings and jagged/nested data."""
|
|
106
|
+
if isinstance(a, str):
|
|
107
|
+
return self.str_to_char_array(a)
|
|
108
|
+
try:
|
|
109
|
+
arr = self._np.asarray(a)
|
|
110
|
+
if arr.dtype.kind not in ['O', 'i', 'f']:
|
|
111
|
+
raise ValueError
|
|
112
|
+
except (NumpyVisibleDeprecationWarning, ValueError):
|
|
113
|
+
try:
|
|
114
|
+
arr = self._np.asarray(a, dtype=object)
|
|
115
|
+
except ValueError:
|
|
116
|
+
arr = [x.tolist() if self.is_array(x) else x for x in a]
|
|
117
|
+
arr = self._np.asarray(arr, dtype=object)
|
|
118
|
+
arr = self._np.asarray(
|
|
119
|
+
[self.kg_asarray(x) if isinstance(x, list) else x for x in arr],
|
|
120
|
+
dtype=object
|
|
121
|
+
)
|
|
122
|
+
return arr
|