klongpy 0.6.9__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.
Files changed (64) hide show
  1. klongpy/__init__.py +19 -1
  2. klongpy/adverbs.py +5 -5
  3. klongpy/autograd.py +308 -0
  4. klongpy/backend.py +167 -99
  5. klongpy/backends/__init__.py +94 -0
  6. klongpy/backends/base.py +320 -0
  7. klongpy/backends/numpy_backend.py +122 -0
  8. klongpy/backends/torch_backend.py +995 -0
  9. klongpy-0.6.9.data/scripts/kgpy → klongpy/cli.py +65 -88
  10. klongpy/core.py +228 -108
  11. klongpy/db/sys_fn_db.py +4 -3
  12. klongpy/dyads.py +159 -28
  13. klongpy/interpreter.py +31 -3
  14. klongpy/monads.py +39 -3
  15. klongpy/repl.py +21 -3
  16. klongpy/sys_fn.py +128 -17
  17. klongpy/sys_fn_autograd.py +290 -0
  18. klongpy/sys_fn_ipc.py +18 -6
  19. klongpy/sys_fn_timer.py +13 -3
  20. klongpy/web/sys_fn_web.py +14 -4
  21. klongpy-0.7.0.dist-info/METADATA +493 -0
  22. klongpy-0.7.0.dist-info/RECORD +48 -0
  23. {klongpy-0.6.9.dist-info → klongpy-0.7.0.dist-info}/WHEEL +1 -1
  24. klongpy-0.7.0.dist-info/entry_points.txt +2 -0
  25. {klongpy-0.6.9.dist-info → klongpy-0.7.0.dist-info}/top_level.txt +0 -1
  26. klongpy-0.6.9.dist-info/METADATA +0 -448
  27. klongpy-0.6.9.dist-info/RECORD +0 -77
  28. tests/__init__.py +0 -6
  29. tests/gen_join_over.py +0 -119
  30. tests/gen_py_suite.py +0 -77
  31. tests/gen_test_fn.py +0 -259
  32. tests/perf_async.py +0 -25
  33. tests/perf_avg.py +0 -18
  34. tests/perf_duckdb.py +0 -32
  35. tests/perf_gen.py +0 -38
  36. tests/perf_ipc_overhead.py +0 -34
  37. tests/perf_join.py +0 -53
  38. tests/perf_load.py +0 -17
  39. tests/perf_prog.py +0 -18
  40. tests/perf_serdes.py +0 -52
  41. tests/perf_sys_fn_db.py +0 -263
  42. tests/perf_vector.py +0 -40
  43. tests/test_accel.py +0 -227
  44. tests/test_df_cache.py +0 -85
  45. tests/test_eval_monad_list.py +0 -34
  46. tests/test_examples.py +0 -64
  47. tests/test_extra_suite.py +0 -382
  48. tests/test_file_cache.py +0 -185
  49. tests/test_interop.py +0 -180
  50. tests/test_kg_asarray.py +0 -94
  51. tests/test_kgtests.py +0 -65
  52. tests/test_known_bugs.py +0 -206
  53. tests/test_prog.py +0 -107
  54. tests/test_reshape_strings.py +0 -33
  55. tests/test_suite.py +0 -1480
  56. tests/test_suite_file.py +0 -153
  57. tests/test_sys_fn.py +0 -420
  58. tests/test_sys_fn_db.py +0 -88
  59. tests/test_sys_fn_ipc.py +0 -587
  60. tests/test_sys_fn_timer.py +0 -133
  61. tests/test_sys_fn_web.py +0 -50
  62. tests/test_util.py +0 -233
  63. tests/utils.py +0 -126
  64. {klongpy-0.6.9.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
+ ]
@@ -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