calc-flow-python 4.0.0__cp313-abi3-win_amd64.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.
calc_flow/array.py ADDED
@@ -0,0 +1,1324 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import math
5
+ import operator
6
+ from collections.abc import Callable, Mapping
7
+ from dataclasses import dataclass
8
+ from functools import lru_cache
9
+ from typing import TYPE_CHECKING, Any, Never
10
+
11
+ from calc_flow import _native
12
+ from calc_flow.capabilities import (
13
+ CapabilityRule,
14
+ ProviderArrayRules,
15
+ ProviderOption,
16
+ ProviderOptionsSchema,
17
+ )
18
+
19
+ if TYPE_CHECKING:
20
+ from calc_flow.pipeline import Runtime
21
+
22
+ _MAX_AST_NODES = 128
23
+ _MAX_AST_DEPTH = 24
24
+ _MAX_EXPRESSION_LENGTH = 4096
25
+ _MAX_INTEGER_MAGNITUDE = 2**63 - 1
26
+ _MAX_POWER_EXPONENT_MAGNITUDE = 64
27
+ _MAX_RESHAPE_RANK = 16
28
+ _MAX_RESHAPE_DIMENSION = 1_000_000
29
+ _MAX_RESHAPE_ELEMENTS = 10_000_000
30
+ _MAX_OPERATION_ELEMENTS = 10_000_000
31
+ _TABLE_MATMUL_INPUT_PORTS = (("table", "table"), ("weights", "array"))
32
+ _TABLE_MATMUL_OUTPUT_PORTS = (("output", "array"),)
33
+ _SYMBOLIC_MATRIX_INPUT_PORTS = (("input", "table"), ("weights", "array"))
34
+ _SYMBOLIC_MATRIX_OUTPUT_PORTS = (("output", "table"),)
35
+ _EXPRESSION_OPTIONS_SCHEMA = ProviderOptionsSchema(
36
+ fields=(ProviderOption("expression", "string", required=True),)
37
+ )
38
+ _NUMPY_STREAM_ARRAY_RULES = ProviderArrayRules(
39
+ supported_dtypes=(
40
+ "bool",
41
+ "complex128",
42
+ "complex64",
43
+ "float32",
44
+ "float64",
45
+ "int16",
46
+ "int32",
47
+ "int64",
48
+ "int8",
49
+ "uint16",
50
+ "uint32",
51
+ "uint64",
52
+ "uint8",
53
+ ),
54
+ safe_dtype_rule=CapabilityRule("array_api_safe_dtype", "1"),
55
+ shape_rules=(CapabilityRule("elementwise_broadcast", "1"),),
56
+ )
57
+ _JAX_STREAM_ARRAY_RULES = ProviderArrayRules(
58
+ supported_dtypes=(
59
+ "bool",
60
+ "complex64",
61
+ "float32",
62
+ "int16",
63
+ "int32",
64
+ "int8",
65
+ "uint16",
66
+ "uint32",
67
+ "uint8",
68
+ ),
69
+ safe_dtype_rule=CapabilityRule("array_api_safe_dtype", "1"),
70
+ shape_rules=(CapabilityRule("elementwise_broadcast", "1"),),
71
+ )
72
+ _NUMPY_MATRIX_STREAM_ARRAY_RULES = ProviderArrayRules(
73
+ supported_dtypes=_NUMPY_STREAM_ARRAY_RULES.supported_dtypes,
74
+ safe_dtype_rule=_NUMPY_STREAM_ARRAY_RULES.safe_dtype_rule,
75
+ shape_rules=(
76
+ CapabilityRule("elementwise_broadcast", "1"),
77
+ CapabilityRule("table_matmul_static_rhs", "1"),
78
+ ),
79
+ )
80
+ _JAX_MATRIX_STREAM_ARRAY_RULES = ProviderArrayRules(
81
+ supported_dtypes=_JAX_STREAM_ARRAY_RULES.supported_dtypes,
82
+ safe_dtype_rule=_JAX_STREAM_ARRAY_RULES.safe_dtype_rule,
83
+ shape_rules=(
84
+ CapabilityRule("elementwise_broadcast", "1"),
85
+ CapabilityRule("table_matmul_static_rhs", "1"),
86
+ ),
87
+ )
88
+
89
+ _ALLOWED_BINARY = {
90
+ ast.Add: operator.add,
91
+ ast.Sub: operator.sub,
92
+ ast.Mult: operator.mul,
93
+ ast.Div: operator.truediv,
94
+ ast.MatMult: operator.matmul,
95
+ ast.Pow: operator.pow,
96
+ }
97
+ _ALLOWED_UNARY = {ast.UAdd: operator.pos, ast.USub: operator.neg}
98
+ _UNARY_FUNCTIONS = {"sum", "mean", "max", "min", "transpose"}
99
+ _ALLOWED_FUNCTIONS = _UNARY_FUNCTIONS | {"reshape"}
100
+
101
+
102
+ def _array_error(message: str) -> ValueError:
103
+ return ValueError(f"invalid array expression: {message}")
104
+
105
+
106
+ def _parse_expression(expression: object) -> ast.Expression:
107
+ if not isinstance(expression, str) or not expression.strip():
108
+ raise _array_error("expression must be a non-empty string")
109
+ if len(expression) > _MAX_EXPRESSION_LENGTH:
110
+ raise _array_error(
111
+ f"expression length limit is {_MAX_EXPRESSION_LENGTH} characters"
112
+ )
113
+ return _parse_valid_expression(expression)
114
+
115
+
116
+ @lru_cache(maxsize=256)
117
+ def _parse_valid_expression(expression: str) -> ast.Expression:
118
+ try:
119
+ parsed = ast.parse(expression, mode="eval")
120
+ except (SyntaxError, ValueError) as error:
121
+ raise _array_error("syntax is invalid") from error
122
+ if not isinstance(parsed, ast.Expression):
123
+ raise _array_error("syntax is invalid")
124
+ nodes = list(ast.walk(parsed))
125
+ if len(nodes) > _MAX_AST_NODES:
126
+ raise _array_error(f"node limit is {_MAX_AST_NODES}")
127
+ if _ast_depth(parsed) > _MAX_AST_DEPTH:
128
+ raise _array_error(f"depth limit is {_MAX_AST_DEPTH}")
129
+ _validate_node(parsed.body)
130
+ return parsed
131
+
132
+
133
+ def _ast_depth(node: ast.AST) -> int:
134
+ children = list(ast.iter_child_nodes(node))
135
+ if not children:
136
+ return 1
137
+ return 1 + max(_ast_depth(child) for child in children)
138
+
139
+
140
+ def _validate_node(node: ast.AST) -> None:
141
+ if isinstance(node, ast.Name):
142
+ if node.id != "x":
143
+ raise _array_error(f"unknown name {node.id!r}")
144
+ return
145
+ if isinstance(node, ast.Constant):
146
+ if type(node.value) not in (int, float):
147
+ raise _array_error("constants must be finite numbers")
148
+ if type(node.value) is int and abs(node.value) > _MAX_INTEGER_MAGNITUDE:
149
+ raise _array_error(
150
+ f"integer constant magnitude limit is {_MAX_INTEGER_MAGNITUDE}"
151
+ )
152
+ if isinstance(node.value, float) and not math.isfinite(node.value):
153
+ raise _array_error("constants must be finite numbers")
154
+ return
155
+ if isinstance(node, ast.BinOp) and type(node.op) in _ALLOWED_BINARY:
156
+ _validate_node(node.left)
157
+ if isinstance(node.op, ast.Pow):
158
+ exponent = _numeric_literal(node.right)
159
+ if exponent is None:
160
+ raise _array_error("power exponent must be a finite numeric literal")
161
+ if abs(exponent) > _MAX_POWER_EXPONENT_MAGNITUDE:
162
+ raise _array_error(
163
+ f"power exponent magnitude limit is {_MAX_POWER_EXPONENT_MAGNITUDE}"
164
+ )
165
+ _validate_node(node.right)
166
+ return
167
+ if isinstance(node, ast.UnaryOp) and type(node.op) in _ALLOWED_UNARY:
168
+ _validate_node(node.operand)
169
+ return
170
+ if isinstance(node, ast.Call):
171
+ _validate_call(node)
172
+ return
173
+ raise _array_error(f"unsupported syntax {type(node).__name__}")
174
+
175
+
176
+ def _numeric_literal(node: ast.AST) -> int | float | None:
177
+ sign = 1
178
+ if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.UAdd, ast.USub)):
179
+ sign = -1 if isinstance(node.op, ast.USub) else 1
180
+ node = node.operand
181
+ if not isinstance(node, ast.Constant) or type(node.value) not in (int, float):
182
+ return None
183
+ if isinstance(node.value, float) and not math.isfinite(node.value):
184
+ return None
185
+ return sign * node.value
186
+
187
+
188
+ def _validate_call(node: ast.Call) -> None:
189
+ if not isinstance(node.func, ast.Name):
190
+ raise _array_error("functions must be called directly")
191
+ name = node.func.id
192
+ if name not in _ALLOWED_FUNCTIONS:
193
+ raise _array_error(f"unknown function {name!r}")
194
+ if node.keywords:
195
+ raise _array_error("function keyword arguments are unsupported")
196
+ expected = 1 if name in _UNARY_FUNCTIONS else 2
197
+ if len(node.args) != expected:
198
+ suffix = "" if expected == 1 else "s"
199
+ raise _array_error(f"{name} expects {expected} argument{suffix}")
200
+ _validate_node(node.args[0])
201
+ if name == "reshape":
202
+ _reshape_shape(node.args[1])
203
+
204
+
205
+ def _reshape_shape(node: ast.AST) -> tuple[int, ...]:
206
+ if not isinstance(node, (ast.Tuple, ast.List)):
207
+ raise _array_error("reshape shape must be a tuple or list")
208
+ if len(node.elts) > _MAX_RESHAPE_RANK:
209
+ raise _array_error(f"reshape rank limit is {_MAX_RESHAPE_RANK}")
210
+ dimensions: list[int] = []
211
+ for item in node.elts:
212
+ value: object
213
+ if isinstance(item, ast.Constant):
214
+ value = item.value
215
+ elif (
216
+ isinstance(item, ast.UnaryOp)
217
+ and isinstance(item.op, ast.USub)
218
+ and isinstance(item.operand, ast.Constant)
219
+ and type(item.operand.value) is int
220
+ ):
221
+ value = -item.operand.value
222
+ else:
223
+ raise _array_error("reshape dimensions must be integers")
224
+ if type(value) is not int:
225
+ raise _array_error("reshape dimensions must be integers")
226
+ dimensions.append(value)
227
+ if dimensions.count(-1) > 1:
228
+ raise _array_error("reshape shape allows at most one -1")
229
+ if -1 in dimensions and 0 in dimensions:
230
+ raise _array_error("reshape cannot combine a zero dimension with -1")
231
+ if any(dimension < -1 for dimension in dimensions):
232
+ raise _array_error("reshape dimensions must be non-negative or -1")
233
+ if any(dimension > _MAX_RESHAPE_DIMENSION for dimension in dimensions):
234
+ raise _array_error(f"reshape dimension limit is {_MAX_RESHAPE_DIMENSION}")
235
+ known_elements = math.prod(dimension for dimension in dimensions if dimension != -1)
236
+ if known_elements > _MAX_RESHAPE_ELEMENTS:
237
+ raise _array_error(f"reshape output limit is {_MAX_RESHAPE_ELEMENTS} elements")
238
+ return tuple(dimensions)
239
+
240
+
241
+ def _validate_reshape_result(value: object) -> None:
242
+ dimensions = tuple(int(dimension) for dimension in getattr(value, "shape", ()))
243
+ if any(dimension > _MAX_RESHAPE_DIMENSION for dimension in dimensions):
244
+ raise _array_error(f"reshape dimension limit is {_MAX_RESHAPE_DIMENSION}")
245
+ if math.prod(dimensions) > _MAX_RESHAPE_ELEMENTS:
246
+ raise _array_error(f"reshape output limit is {_MAX_RESHAPE_ELEMENTS} elements")
247
+
248
+
249
+ def _evaluate(
250
+ node: ast.AST,
251
+ value: object,
252
+ namespace: Any,
253
+ validate_result: Callable[[object], None] | None = None,
254
+ ) -> object:
255
+ if isinstance(node, ast.Name):
256
+ # The input batch is validated at construction; only operation
257
+ # results pass through validate_result.
258
+ return value
259
+ if isinstance(node, ast.Constant):
260
+ result = node.value
261
+ elif isinstance(node, ast.BinOp):
262
+ function = _ALLOWED_BINARY[type(node.op)]
263
+ left = _evaluate(node.left, value, namespace, validate_result)
264
+ right = _evaluate(node.right, value, namespace, validate_result)
265
+ if isinstance(node.op, ast.MatMult):
266
+ _validate_matmul_output_size(left, right)
267
+ else:
268
+ _validate_broadcast_output_size(left, right)
269
+ result = function(left, right)
270
+ elif isinstance(node, ast.UnaryOp):
271
+ function = _ALLOWED_UNARY[type(node.op)]
272
+ result = function(_evaluate(node.operand, value, namespace, validate_result))
273
+ elif isinstance(node, ast.Call):
274
+ name = node.func.id # type: ignore[union-attr]
275
+ function = getattr(namespace, name)
276
+ arguments = [_evaluate(node.args[0], value, namespace, validate_result)]
277
+ if name == "reshape":
278
+ arguments.append(_reshape_shape(node.args[1]))
279
+ result = function(*arguments)
280
+ if name == "reshape":
281
+ _validate_reshape_result(result)
282
+ else:
283
+ raise AssertionError("validated array expression contained an unsupported node")
284
+ if validate_result is not None:
285
+ validate_result(result)
286
+ return result
287
+
288
+
289
+ def _validate_options(options: Mapping[str, object]) -> ast.Expression:
290
+ unknown = set(options) - {"expression", "udfs"}
291
+ if unknown:
292
+ raise _array_error(f"unsupported options: {', '.join(sorted(unknown))}")
293
+ udfs = options.get("udfs", [])
294
+ if udfs:
295
+ raise _array_error("custom array UDFs are unavailable")
296
+ return _parse_expression(options.get("expression"))
297
+
298
+
299
+ def _validate_stream_options(options: Mapping[str, object]) -> None:
300
+ parsed = _validate_options(options)
301
+ if not any(
302
+ isinstance(node, ast.Name) and node.id == "x" for node in ast.walk(parsed)
303
+ ):
304
+ raise _array_error("stream expressions must depend on the input rows")
305
+ if any(isinstance(node, (ast.Call, ast.MatMult)) for node in ast.walk(parsed)):
306
+ raise _array_error(
307
+ "stream expressions support only row-axis-independent "
308
+ "elementwise operations"
309
+ )
310
+
311
+
312
+ def _validate_provider_options(
313
+ provider: str, name: str, version: str, options: Mapping[str, object]
314
+ ) -> None:
315
+ if provider in {"numpy", "jax"} and name == "expression" and version == "1":
316
+ _validate_options(options)
317
+
318
+
319
+ def _table_matmul_columns(options: Mapping[str, object]) -> tuple[str, ...]:
320
+ unknown = set(options) - {"columns"}
321
+ if unknown:
322
+ raise ValueError(
323
+ "invalid table_matmul options: unsupported options: "
324
+ + ", ".join(sorted(unknown))
325
+ )
326
+ columns = options.get("columns")
327
+ if isinstance(columns, (str, bytes)) or not isinstance(columns, list):
328
+ raise ValueError("invalid table_matmul options: columns must be a JSON array")
329
+ if not columns:
330
+ raise ValueError(
331
+ "invalid table_matmul options: columns must contain at least one name"
332
+ )
333
+ if not all(isinstance(column, str) and column for column in columns):
334
+ raise ValueError(
335
+ "invalid table_matmul options: columns must contain non-empty strings"
336
+ )
337
+ if len(set(columns)) != len(columns):
338
+ raise ValueError("invalid table_matmul options: columns must be unique")
339
+ return tuple(columns)
340
+
341
+
342
+ def _table_matmul_inputs(
343
+ inputs: Mapping[str, _native.Batch],
344
+ backend: str,
345
+ ) -> tuple[_native.Batch, _native.Batch]:
346
+ copied = dict(inputs)
347
+ expected = {"table", "weights"}
348
+ missing = expected - set(copied)
349
+ unexpected = set(copied) - expected
350
+ if missing:
351
+ raise ValueError(
352
+ "invalid table_matmul inputs: missing required inputs: "
353
+ + ", ".join(sorted(missing))
354
+ )
355
+ if unexpected:
356
+ raise ValueError(
357
+ "invalid table_matmul inputs: unsupported inputs: "
358
+ + ", ".join(sorted(unexpected))
359
+ )
360
+ table_batch = copied["table"]
361
+ weights_batch = copied["weights"]
362
+ if not isinstance(table_batch, _native.Batch) or table_batch.kind != "table":
363
+ received = getattr(table_batch, "kind", type(table_batch).__name__)
364
+ raise TypeError(
365
+ f"invalid table_matmul table: expected a table batch, received {received}"
366
+ )
367
+ if not isinstance(weights_batch, _native.Batch) or weights_batch.kind != "array":
368
+ received = getattr(weights_batch, "kind", type(weights_batch).__name__)
369
+ raise TypeError(
370
+ "invalid table_matmul weights: "
371
+ f"expected an array batch, received {received}"
372
+ )
373
+ if weights_batch.backend != backend:
374
+ raise ValueError(
375
+ "invalid table_matmul weights.backend: "
376
+ f"expected {backend}, received {weights_batch.backend}"
377
+ )
378
+ return table_batch, weights_batch
379
+
380
+
381
+ def _validated_table_dtypes(
382
+ table: object,
383
+ columns: tuple[str, ...],
384
+ ) -> tuple[object, ...]:
385
+ import numpy as np
386
+ import pyarrow as pa
387
+
388
+ if table.num_rows <= 0:
389
+ raise ValueError("invalid table_matmul table.rows: expected at least one row")
390
+ dtypes: list[object] = []
391
+ for name in columns:
392
+ indices = table.schema.get_all_field_indices(name)
393
+ if not indices:
394
+ raise ValueError(
395
+ f"invalid table_matmul columns: selected column {name!r} is missing"
396
+ )
397
+ if len(indices) != 1:
398
+ raise ValueError(
399
+ f"invalid table_matmul columns: selected column {name!r} is ambiguous"
400
+ )
401
+ column = table.column(indices[0])
402
+ if column.null_count:
403
+ raise ValueError(
404
+ f"invalid table_matmul columns: selected column {name!r} contains nulls"
405
+ )
406
+ data_type = column.type
407
+ if not (pa.types.is_integer(data_type) or pa.types.is_floating(data_type)):
408
+ raise TypeError(
409
+ "invalid table_matmul columns: "
410
+ f"selected column {name!r} has unsupported Arrow dtype {data_type}"
411
+ )
412
+ family = (
413
+ "int"
414
+ if pa.types.is_signed_integer(data_type)
415
+ else "uint"
416
+ if pa.types.is_unsigned_integer(data_type)
417
+ else "float"
418
+ )
419
+ dtypes.append(np.dtype(f"{family}{data_type.bit_width}"))
420
+ return tuple(dtypes)
421
+
422
+
423
+ def _validated_weights(
424
+ weights_batch: _native.Batch,
425
+ backend: str,
426
+ column_count: int,
427
+ ) -> object:
428
+ weights = weights_batch.array
429
+ if backend == "numpy":
430
+ import numpy as np
431
+
432
+ if type(weights) is not np.ndarray:
433
+ raise TypeError(
434
+ "invalid table_matmul weights: NumPy weights must be an ndarray"
435
+ )
436
+ shape = getattr(weights, "shape", ())
437
+ if len(shape) != 2:
438
+ raise ValueError(
439
+ "invalid table_matmul weights.rank: "
440
+ f"expected rank two, received rank {len(shape)}"
441
+ )
442
+ if shape[0] != column_count:
443
+ raise ValueError(
444
+ "invalid table_matmul weights.shape[0]: "
445
+ f"expected {column_count}, received {shape[0]}"
446
+ )
447
+ if shape[1] <= 0:
448
+ raise ValueError(
449
+ "invalid table_matmul weights.shape[1]: expected a positive output width"
450
+ )
451
+ return weights
452
+
453
+
454
+ def _common_matrix_dtype(
455
+ backend: str,
456
+ namespace: object,
457
+ table_dtypes: tuple[object, ...],
458
+ weights: object,
459
+ ) -> object:
460
+ import numpy as np
461
+
462
+ weight_dtype = np.dtype(weights.dtype)
463
+ involved = ", ".join(
464
+ [*(np.dtype(dtype).name for dtype in table_dtypes), weight_dtype.name]
465
+ )
466
+ if backend == "jax":
467
+ import jax
468
+
469
+ x64_dtypes = frozenset(
470
+ np.dtype(dtype)
471
+ for dtype in (np.int64, np.uint64, np.float64, np.complex128)
472
+ )
473
+ if not jax.config.x64_enabled and any(
474
+ np.dtype(source) in x64_dtypes for source in (*table_dtypes, weight_dtype)
475
+ ):
476
+ raise TypeError(
477
+ "invalid table_matmul dtype: "
478
+ f"JAX x64 is disabled for [{involved}]; enable the required dtype "
479
+ "or choose a lossless supported dtype"
480
+ )
481
+ try:
482
+ result_dtype = np.dtype(namespace.result_type(*table_dtypes, weight_dtype))
483
+ except (TypeError, ValueError) as error:
484
+ raise TypeError(
485
+ "invalid table_matmul dtype: "
486
+ f"{backend} cannot promote Arrow and weight dtypes [{involved}]"
487
+ ) from error
488
+ sources = (*table_dtypes, weight_dtype)
489
+ if not all(
490
+ namespace.can_cast(source, result_dtype, casting="safe") for source in sources
491
+ ):
492
+ raise TypeError(
493
+ "invalid table_matmul dtype: "
494
+ f"common dtype {result_dtype.name} is lossy for [{involved}]"
495
+ )
496
+ try:
497
+ _validate_numpy_dtype(result_dtype)
498
+ except ValueError as error:
499
+ raise TypeError(
500
+ "invalid table_matmul dtype: "
501
+ f"common dtype {result_dtype.name} is unsupported for [{involved}]"
502
+ ) from error
503
+ return result_dtype
504
+
505
+
506
+ def _numpy_table_matrix(
507
+ table: object,
508
+ columns: tuple[str, ...],
509
+ dtype: object,
510
+ ) -> object:
511
+ import numpy as np
512
+
513
+ matrix, _token = _native.Batch._new_owned_numpy(
514
+ (table.num_rows, len(columns)),
515
+ np.dtype(dtype).name,
516
+ )
517
+ for column_index, name in enumerate(columns):
518
+ offset = 0
519
+ for chunk in table[name].chunks:
520
+ values = chunk.to_numpy(zero_copy_only=True)
521
+ next_offset = offset + len(values)
522
+ np.copyto(
523
+ matrix[offset:next_offset, column_index],
524
+ values,
525
+ casting="safe",
526
+ )
527
+ offset = next_offset
528
+ return matrix
529
+
530
+
531
+ def _owned_numpy(value: object) -> object:
532
+ import numpy as np
533
+
534
+ array = np.asarray(value)
535
+ _validate_numpy_dtype(array.dtype)
536
+ immutable_bytes = array.tobytes(order="C")
537
+ return np.frombuffer(immutable_bytes, dtype=array.dtype).reshape(array.shape)
538
+
539
+
540
+ def _validate_numpy_dtype(dtype: object) -> None:
541
+ import numpy as np
542
+
543
+ normalized = np.dtype(dtype)
544
+ allowed_dtypes = frozenset(
545
+ np.dtype(scalar_type)
546
+ for scalar_type in (
547
+ np.bool_,
548
+ np.int8,
549
+ np.int16,
550
+ np.int32,
551
+ np.int64,
552
+ np.uint8,
553
+ np.uint16,
554
+ np.uint32,
555
+ np.uint64,
556
+ np.float32,
557
+ np.float64,
558
+ np.complex64,
559
+ np.complex128,
560
+ )
561
+ )
562
+ if normalized not in allowed_dtypes or not normalized.isnative:
563
+ raise ValueError(
564
+ f"NumPy arrays require a NumPy Array API dtype; received {normalized}"
565
+ )
566
+
567
+
568
+ def _validate_python_operation_scalar(value: object) -> bool:
569
+ if type(value) is int:
570
+ if abs(value) > _MAX_INTEGER_MAGNITUDE:
571
+ raise _array_error(
572
+ f"integer constant magnitude limit is {_MAX_INTEGER_MAGNITUDE}"
573
+ )
574
+ return True
575
+ return type(value) in (float, complex)
576
+
577
+
578
+ def _result_shape(value: object) -> tuple[int, ...]:
579
+ return tuple(int(dimension) for dimension in getattr(value, "shape", ()))
580
+
581
+
582
+ def _validate_operation_output_size(value: object) -> None:
583
+ _validate_operation_shape(_result_shape(value))
584
+
585
+
586
+ def _validate_operation_shape(shape: tuple[int, ...]) -> None:
587
+ if math.prod(shape) > _MAX_OPERATION_ELEMENTS:
588
+ raise _array_error(
589
+ f"operation output limit is {_MAX_OPERATION_ELEMENTS} elements"
590
+ )
591
+
592
+
593
+ def _broadcast_shape(
594
+ left: tuple[int, ...], right: tuple[int, ...]
595
+ ) -> tuple[int, ...] | None:
596
+ dimensions: list[int] = []
597
+ for offset in range(1, max(len(left), len(right)) + 1):
598
+ left_dim = left[-offset] if offset <= len(left) else 1
599
+ right_dim = right[-offset] if offset <= len(right) else 1
600
+ if left_dim == 1:
601
+ dimensions.append(right_dim)
602
+ elif right_dim == 1 or left_dim == right_dim:
603
+ dimensions.append(left_dim)
604
+ else:
605
+ return None
606
+ return tuple(reversed(dimensions))
607
+
608
+
609
+ def _matmul_batch_shape(
610
+ left: tuple[int, ...], right: tuple[int, ...]
611
+ ) -> tuple[int, ...] | None:
612
+ return _broadcast_shape(
613
+ left[:-2] if len(left) > 1 else (),
614
+ right[:-2] if len(right) > 1 else (),
615
+ )
616
+
617
+
618
+ def _matmul_result_axes(
619
+ left: tuple[int, ...], right: tuple[int, ...]
620
+ ) -> tuple[int, ...]:
621
+ if len(left) == 1:
622
+ return () if len(right) == 1 else (right[-1],)
623
+ if len(right) == 1:
624
+ return (left[-2],)
625
+ return (left[-2], right[-1])
626
+
627
+
628
+ def _matmul_output_shape(
629
+ left: tuple[int, ...], right: tuple[int, ...]
630
+ ) -> tuple[int, ...] | None:
631
+ if not left or not right:
632
+ return None
633
+ right_inner = right[-2] if len(right) > 1 else right[-1]
634
+ if left[-1] != right_inner:
635
+ return None
636
+ batch = _matmul_batch_shape(left, right)
637
+ if batch is None:
638
+ return None
639
+ return (*batch, *_matmul_result_axes(left, right))
640
+
641
+
642
+ def _validate_broadcast_output_size(left: object, right: object) -> None:
643
+ shape = _broadcast_shape(_result_shape(left), _result_shape(right))
644
+ if shape is not None:
645
+ _validate_operation_shape(shape)
646
+
647
+
648
+ def _validate_matmul_output_size(left: object, right: object) -> None:
649
+ shape = _matmul_output_shape(_result_shape(left), _result_shape(right))
650
+ if shape is not None:
651
+ _validate_operation_shape(shape)
652
+
653
+
654
+ def _validate_numpy_operation_result(value: object) -> None:
655
+ import numpy as np
656
+
657
+ if _validate_python_operation_scalar(value):
658
+ return
659
+ if type(value) is np.ndarray or isinstance(value, np.generic):
660
+ _validate_numpy_dtype(value.dtype)
661
+ _validate_operation_output_size(value)
662
+ return
663
+ raise TypeError("NumPy provider operations must produce arrays or numeric scalars")
664
+
665
+
666
+ def _validate_jax_operation_result(value: object) -> None:
667
+ import jax
668
+
669
+ if _validate_python_operation_scalar(value):
670
+ return
671
+ if isinstance(value, jax.Array):
672
+ _validate_operation_output_size(value)
673
+ return
674
+ raise TypeError("JAX provider operations must produce arrays or numeric scalars")
675
+
676
+
677
+ def _owned_jax(value: object) -> object:
678
+ import jax
679
+ import jax.numpy as jnp
680
+
681
+ array = jnp.asarray(value)
682
+ if not isinstance(array, jax.Array):
683
+ raise TypeError("JAX batches require a jax.Array payload")
684
+ return array
685
+
686
+
687
+ def _prepare_array(value: object, backend: str) -> tuple[object, int]:
688
+ if backend == "numpy":
689
+ owned = _owned_numpy(value)
690
+ elif backend == "jax":
691
+ owned = _owned_jax(value)
692
+ else:
693
+ owned = value
694
+ shape = getattr(owned, "shape", None)
695
+ length = int(shape[0]) if shape else 1
696
+ if length < 0:
697
+ raise ValueError("array length must not be negative")
698
+ return owned, length
699
+
700
+
701
+ @dataclass(frozen=True, slots=True)
702
+ class _ArrayProvider:
703
+ backend: str
704
+ namespace: object
705
+
706
+ def validate(self, options: Mapping[str, object]) -> None:
707
+ _validate_options(options)
708
+
709
+ def validate_stream(self, options: Mapping[str, object]) -> None:
710
+ _validate_stream_options(options)
711
+
712
+ def __call__(
713
+ self, batch: _native.Batch, options: Mapping[str, object]
714
+ ) -> _native.Batch:
715
+ if batch.backend != self.backend:
716
+ raise TypeError(
717
+ f"provider requires backend {self.backend}, received {batch.backend}"
718
+ )
719
+ parsed = _validate_options(options)
720
+ validate_result = {
721
+ "jax": _validate_jax_operation_result,
722
+ "numpy": _validate_numpy_operation_result,
723
+ }[self.backend]
724
+ result = _evaluate(parsed.body, batch.array, self.namespace, validate_result)
725
+ if self.backend == "jax":
726
+ import jax
727
+
728
+ result = _owned_jax(result)
729
+ if not isinstance(result, jax.Array):
730
+ raise TypeError("JAX provider output must remain a jax.Array")
731
+ return _native.Batch.from_array(
732
+ result, backend=self.backend, metadata=batch.metadata
733
+ )
734
+
735
+
736
+ def _jax_table_matmul(
737
+ table: object,
738
+ columns: tuple[str, ...],
739
+ dtype: object,
740
+ weights: object,
741
+ ) -> tuple[object, None]:
742
+ import jax
743
+ import jax.numpy as jnp
744
+
745
+ host = _numpy_table_matrix(table, columns, dtype)
746
+ dense = jax.device_put(host, device=weights.device)
747
+ expected_dtype = jnp.dtype(dtype)
748
+ if dense.dtype != expected_dtype:
749
+ raise ValueError(
750
+ "invalid table_matmul dtype: JAX changed "
751
+ f"{expected_dtype} to {dense.dtype}; enable the required dtype "
752
+ "or choose a lossless supported dtype"
753
+ )
754
+ result = jnp.matmul(dense, weights)
755
+ if not isinstance(result, jax.Array):
756
+ raise TypeError("table_matmul JAX result must remain a jax.Array")
757
+ if result.dtype != expected_dtype:
758
+ raise ValueError(
759
+ "invalid table_matmul dtype: JAX changed "
760
+ f"{expected_dtype} to {result.dtype}; enable the required dtype "
761
+ "or choose a lossless supported dtype"
762
+ )
763
+ return result, None
764
+
765
+
766
+ @dataclass(frozen=True, slots=True)
767
+ class _TableMatmulProvider:
768
+ backend: str
769
+ namespace: object
770
+
771
+ def validate(self, options: Mapping[str, object]) -> None:
772
+ _table_matmul_columns(options)
773
+
774
+ def __call__(
775
+ self,
776
+ inputs: Mapping[str, _native.Batch],
777
+ options: Mapping[str, object],
778
+ ) -> dict[str, _native.Batch]:
779
+ columns = _table_matmul_columns(options)
780
+ table_batch, weights_batch = _table_matmul_inputs(inputs, self.backend)
781
+ table = table_batch.to_pyarrow()
782
+ table_dtypes = _validated_table_dtypes(table, columns)
783
+ weights = _validated_weights(weights_batch, self.backend, len(columns))
784
+ result_dtype = _common_matrix_dtype(
785
+ self.backend,
786
+ self.namespace,
787
+ table_dtypes,
788
+ weights,
789
+ )
790
+ if self.backend != "numpy":
791
+ output, token = _jax_table_matmul(
792
+ table,
793
+ columns,
794
+ result_dtype,
795
+ weights,
796
+ )
797
+ else:
798
+ import numpy as np
799
+
800
+ dense = _numpy_table_matrix(table, columns, result_dtype)
801
+ output, token = _native.Batch._new_owned_numpy(
802
+ (table.num_rows, weights.shape[1]),
803
+ np.dtype(result_dtype).name,
804
+ )
805
+ np.matmul(dense, weights, out=output)
806
+
807
+ metadata = table_batch.metadata
808
+ metadata.update(
809
+ {
810
+ "backend": self.backend,
811
+ "columns": list(columns),
812
+ "operation": "table_matmul",
813
+ }
814
+ )
815
+ return {
816
+ "output": _native.Batch._from_owned_array(
817
+ output,
818
+ backend=self.backend,
819
+ token=token,
820
+ metadata=metadata,
821
+ )
822
+ }
823
+
824
+
825
+ _SYMBOLIC_BINARY = {
826
+ "add": operator.add,
827
+ "and": operator.and_,
828
+ "eq": operator.eq,
829
+ "ge": operator.ge,
830
+ "gt": operator.gt,
831
+ "le": operator.le,
832
+ "lt": operator.lt,
833
+ "mul": operator.mul,
834
+ "ne": operator.ne,
835
+ "or": operator.or_,
836
+ "sub": operator.sub,
837
+ "truediv": operator.truediv,
838
+ }
839
+ _SYMBOLIC_UNARY = ("neg", "not")
840
+
841
+
842
+ def _symbolic_typed_operand(
843
+ backend: str,
844
+ data_type: str,
845
+ *,
846
+ matrix: bool,
847
+ ) -> object:
848
+ import numpy as np
849
+
850
+ if backend == "numpy":
851
+ namespace = np
852
+ elif backend == "jax":
853
+ import jax
854
+ import jax.numpy as jnp
855
+
856
+ if not jax.config.x64_enabled and np.dtype(data_type) in {
857
+ np.dtype("int64"),
858
+ np.dtype("uint64"),
859
+ np.dtype("float64"),
860
+ np.dtype("complex128"),
861
+ }:
862
+ raise TypeError(f"{backend} cannot represent dtype {data_type!r}")
863
+ namespace = jnp
864
+ else:
865
+ raise TypeError(f"unsupported symbolic array backend {backend!r}")
866
+ shape = (1, 1) if matrix else (1,)
867
+ value = namespace.ones(shape, dtype=data_type)
868
+ if np.dtype(value.dtype).name != data_type:
869
+ raise TypeError(f"{backend} cannot represent dtype {data_type!r}")
870
+ return value
871
+
872
+
873
+ def _symbolic_result_dtype(
874
+ backend: str,
875
+ operation: str,
876
+ left: str | bool | int | float,
877
+ right: str | bool | int | float | None = None,
878
+ ) -> str:
879
+ """Return the selected provider's v1 dtype for one symbolic primitive."""
880
+
881
+ import numpy as np
882
+
883
+ matrix = operation == "matmul"
884
+ left_value = _symbolic_operand(backend, left, matrix=matrix)
885
+ right_value = _symbolic_operand(backend, right, matrix=matrix)
886
+ _validate_symbolic_common_dtype(backend, left_value, right_value)
887
+ result = _apply_symbolic_primitive(
888
+ _symbolic_dtype_namespace(backend),
889
+ operation,
890
+ left_value,
891
+ right_value,
892
+ )
893
+ return np.dtype(result.dtype).name
894
+
895
+
896
+ def _symbolic_operand(
897
+ backend: str,
898
+ value: str | bool | int | float | None,
899
+ *,
900
+ matrix: bool,
901
+ ) -> object:
902
+ if isinstance(value, str):
903
+ return _symbolic_typed_operand(backend, value, matrix=matrix)
904
+ return value
905
+
906
+
907
+ def _symbolic_dtype_namespace(backend: str) -> object:
908
+ if backend == "numpy":
909
+ import numpy as np
910
+
911
+ return np
912
+ import jax.numpy as jnp
913
+
914
+ return jnp
915
+
916
+
917
+ def _validate_symbolic_common_dtype(
918
+ backend: str,
919
+ left: object,
920
+ right: object,
921
+ ) -> None:
922
+ typed_values = tuple(value for value in (left, right) if hasattr(value, "dtype"))
923
+ if len(typed_values) != 2:
924
+ return
925
+ namespace = _symbolic_dtype_namespace(backend)
926
+ common = namespace.result_type(
927
+ typed_values[0].dtype,
928
+ typed_values[1].dtype,
929
+ )
930
+ if not all(
931
+ namespace.can_cast(value.dtype, common, casting="safe")
932
+ for value in typed_values
933
+ ):
934
+ raise TypeError("the provider has no safe common dtype")
935
+
936
+
937
+ def _apply_symbolic_primitive(
938
+ namespace: object,
939
+ operation: str,
940
+ left: object,
941
+ right: object,
942
+ ) -> object:
943
+ if operation == "not":
944
+ return namespace.logical_not(left)
945
+ if operation == "neg":
946
+ return operator.neg(left)
947
+ if operation == "matmul":
948
+ return operator.matmul(left, right)
949
+ return _SYMBOLIC_BINARY[operation](left, right)
950
+
951
+
952
+ def _raise_invalid_symbolic_node(node: Mapping[str, object]) -> Never:
953
+ raise ValueError(
954
+ f"invalid symbolic_matrix expression: unsupported node {node.get('op')!r}"
955
+ )
956
+
957
+
958
+ def _validate_symbolic_leaf(node: dict[str, object], _depth: int) -> None:
959
+ if set(node) != {"op"}:
960
+ _raise_invalid_symbolic_node(node)
961
+
962
+
963
+ def _validate_symbolic_literal(node: dict[str, object], _depth: int) -> None:
964
+ if set(node) != {"op", "value"}:
965
+ _raise_invalid_symbolic_node(node)
966
+ literal = node["value"]
967
+ if type(literal) not in (bool, int, float):
968
+ raise ValueError("invalid symbolic_matrix expression: literal must be finite")
969
+ if type(literal) is float and not math.isfinite(literal):
970
+ raise ValueError("invalid symbolic_matrix expression: literal must be finite")
971
+
972
+
973
+ def _validate_symbolic_unary(node: dict[str, object], depth: int) -> None:
974
+ if set(node) != {"op", "value"}:
975
+ _raise_invalid_symbolic_node(node)
976
+ _validated_symbolic_tree(node["value"], depth=depth + 1)
977
+
978
+
979
+ def _validate_symbolic_binary(node: dict[str, object], depth: int) -> None:
980
+ if set(node) != {"left", "op", "right"}:
981
+ _raise_invalid_symbolic_node(node)
982
+ _validated_symbolic_tree(node["left"], depth=depth + 1)
983
+ _validated_symbolic_tree(node["right"], depth=depth + 1)
984
+
985
+
986
+ _SYMBOLIC_NODE_VALIDATORS: dict[str, Callable[[dict[str, object], int], None]] = {
987
+ "input": _validate_symbolic_leaf,
988
+ "literal": _validate_symbolic_literal,
989
+ "weights": _validate_symbolic_leaf,
990
+ **{
991
+ operation: _validate_symbolic_binary
992
+ for operation in (*_SYMBOLIC_BINARY, "matmul")
993
+ },
994
+ **{operation: _validate_symbolic_unary for operation in _SYMBOLIC_UNARY},
995
+ }
996
+
997
+
998
+ def _validated_symbolic_tree(value: object, *, depth: int = 0) -> dict[str, object]:
999
+ if depth > _MAX_AST_DEPTH:
1000
+ raise ValueError("invalid symbolic_matrix expression: depth limit exceeded")
1001
+ if not isinstance(value, Mapping):
1002
+ raise ValueError("invalid symbolic_matrix expression: node must be a mapping")
1003
+ node = dict(value)
1004
+ validator = _SYMBOLIC_NODE_VALIDATORS.get(node.get("op"))
1005
+ if validator is None:
1006
+ _raise_invalid_symbolic_node(node)
1007
+ validator(node, depth)
1008
+ return node
1009
+
1010
+
1011
+ def _raise_invalid_symbolic_names(field: str) -> Never:
1012
+ raise ValueError(
1013
+ f"invalid symbolic_matrix {field}: expected unique non-empty strings"
1014
+ )
1015
+
1016
+
1017
+ def _symbolic_name(value: object, field: str) -> str:
1018
+ if type(value) is not str:
1019
+ _raise_invalid_symbolic_names(field)
1020
+ if not value:
1021
+ _raise_invalid_symbolic_names(field)
1022
+ return value
1023
+
1024
+
1025
+ def _symbolic_names(value: object, field: str) -> tuple[str, ...]:
1026
+ if not isinstance(value, list):
1027
+ _raise_invalid_symbolic_names(field)
1028
+ if not value:
1029
+ _raise_invalid_symbolic_names(field)
1030
+ names = tuple(_symbolic_name(name, field) for name in value)
1031
+ if len(set(names)) != len(names):
1032
+ _raise_invalid_symbolic_names(field)
1033
+ return names
1034
+
1035
+
1036
+ def _symbolic_matrix_options(
1037
+ options: Mapping[str, object],
1038
+ ) -> tuple[tuple[str, ...], tuple[str, ...], dict[str, object]]:
1039
+ if set(options) != {"columns", "expression", "names"}:
1040
+ raise ValueError(
1041
+ "invalid symbolic_matrix options: expected columns, expression, and names"
1042
+ )
1043
+ columns = _symbolic_names(options["columns"], "columns")
1044
+ names = _symbolic_names(options["names"], "names")
1045
+ return columns, names, _validated_symbolic_tree(options["expression"])
1046
+
1047
+
1048
+ def _symbolic_matrix_inputs(
1049
+ inputs: Mapping[str, _native.Batch], backend: str
1050
+ ) -> tuple[_native.Batch, _native.Batch]:
1051
+ if set(inputs) != {"input", "weights"}:
1052
+ raise ValueError("invalid symbolic_matrix inputs: expected input and weights")
1053
+ table_batch = inputs["input"]
1054
+ weights_batch = inputs["weights"]
1055
+ if table_batch.kind != "table":
1056
+ raise ValueError("invalid symbolic_matrix input: expected a table batch")
1057
+ if weights_batch.kind != "array" or weights_batch.backend != backend:
1058
+ raise ValueError(f"invalid symbolic_matrix weights.backend: expected {backend}")
1059
+ return table_batch, weights_batch
1060
+
1061
+
1062
+ def _evaluate_symbolic_tree(
1063
+ node: Mapping[str, object],
1064
+ namespace: object,
1065
+ dense: object,
1066
+ weights: object,
1067
+ ) -> object:
1068
+ operation = node["op"]
1069
+ if operation == "input":
1070
+ return dense
1071
+ if operation == "weights":
1072
+ return weights
1073
+ if operation == "literal":
1074
+ return node["value"]
1075
+ if operation in _SYMBOLIC_UNARY:
1076
+ result = _apply_symbolic_primitive(
1077
+ namespace,
1078
+ operation,
1079
+ _evaluate_symbolic_tree(node["value"], namespace, dense, weights),
1080
+ None,
1081
+ )
1082
+ else:
1083
+ left = _evaluate_symbolic_tree(node["left"], namespace, dense, weights)
1084
+ right = _evaluate_symbolic_tree(node["right"], namespace, dense, weights)
1085
+ if operation == "matmul":
1086
+ _validate_matmul_output_size(left, right)
1087
+ result = namespace.matmul(left, right)
1088
+ else:
1089
+ _validate_broadcast_output_size(left, right)
1090
+ result = _SYMBOLIC_BINARY[operation](left, right)
1091
+ _validate_operation_output_size(result)
1092
+ return result
1093
+
1094
+
1095
+ def _symbolic_tree_dtype(
1096
+ node: Mapping[str, object],
1097
+ backend: str,
1098
+ input_dtype: str,
1099
+ weights_dtype: str,
1100
+ ) -> str | bool | int | float:
1101
+ operation = node["op"]
1102
+ if operation == "input":
1103
+ return input_dtype
1104
+ if operation == "weights":
1105
+ return weights_dtype
1106
+ if operation == "literal":
1107
+ literal = node["value"]
1108
+ if type(literal) in (bool, int, float):
1109
+ return literal
1110
+ raise TypeError("invalid symbolic_matrix literal dtype")
1111
+ child = "value" if operation in _SYMBOLIC_UNARY else "left"
1112
+ left = _symbolic_tree_dtype(
1113
+ node[child],
1114
+ backend,
1115
+ input_dtype,
1116
+ weights_dtype,
1117
+ )
1118
+ right = (
1119
+ None
1120
+ if operation in _SYMBOLIC_UNARY
1121
+ else _symbolic_tree_dtype(
1122
+ node["right"],
1123
+ backend,
1124
+ input_dtype,
1125
+ weights_dtype,
1126
+ )
1127
+ )
1128
+ return _symbolic_result_dtype(backend, operation, left, right)
1129
+
1130
+
1131
+ @dataclass(frozen=True, slots=True)
1132
+ class _SymbolicMatrixProvider:
1133
+ backend: str
1134
+ namespace: object
1135
+
1136
+ def validate(self, options: Mapping[str, object]) -> None:
1137
+ _symbolic_matrix_options(options)
1138
+
1139
+ def validate_stream(self, options: Mapping[str, object]) -> None:
1140
+ _symbolic_matrix_options(options)
1141
+
1142
+ def __call__(
1143
+ self,
1144
+ inputs: Mapping[str, _native.Batch],
1145
+ options: Mapping[str, object],
1146
+ ) -> dict[str, _native.Batch]:
1147
+ import numpy as np
1148
+ import pyarrow as pa
1149
+
1150
+ columns, names, expression = _symbolic_matrix_options(options)
1151
+ table_batch, weights_batch = _symbolic_matrix_inputs(inputs, self.backend)
1152
+ table = table_batch.to_pyarrow()
1153
+ table_dtypes = _validated_table_dtypes(table, columns)
1154
+ weights = _validated_weights(weights_batch, self.backend, len(columns))
1155
+ result_dtype = _common_matrix_dtype(
1156
+ self.backend, self.namespace, table_dtypes, weights
1157
+ )
1158
+ expected_dtype = _symbolic_tree_dtype(
1159
+ expression,
1160
+ self.backend,
1161
+ np.dtype(result_dtype).name,
1162
+ np.dtype(weights.dtype).name,
1163
+ )
1164
+ if not isinstance(expected_dtype, str):
1165
+ raise TypeError("invalid symbolic_matrix output.dtype: unresolved dtype")
1166
+ host = _numpy_table_matrix(table, columns, result_dtype)
1167
+ if self.backend == "jax":
1168
+ import jax
1169
+
1170
+ dense = jax.device_put(host, device=weights.device)
1171
+ else:
1172
+ dense = host
1173
+ result = _evaluate_symbolic_tree(
1174
+ expression,
1175
+ self.namespace,
1176
+ dense,
1177
+ weights,
1178
+ )
1179
+ result_shape = _result_shape(result)
1180
+ if len(result_shape) != 2:
1181
+ raise ValueError(
1182
+ "invalid symbolic_matrix output.rank: expected rank two; "
1183
+ f"received {len(result_shape)}"
1184
+ )
1185
+ if result_shape[0] != table.num_rows:
1186
+ raise ValueError(
1187
+ "invalid symbolic_matrix output.rows: expected "
1188
+ f"{table.num_rows}, received {result_shape[0]}"
1189
+ )
1190
+ if result_shape[1] != len(names):
1191
+ raise ValueError(
1192
+ "invalid symbolic_matrix output.width: expected "
1193
+ f"{len(names)}, received {result_shape[1]}"
1194
+ )
1195
+ output_host = np.asarray(result)
1196
+ actual_dtype = np.dtype(output_host.dtype).name
1197
+ if actual_dtype != expected_dtype:
1198
+ raise TypeError(
1199
+ "invalid symbolic_matrix output.dtype: expected "
1200
+ f"{expected_dtype}, received {actual_dtype}"
1201
+ )
1202
+ attached = table
1203
+ for index, name in enumerate(names):
1204
+ attached = attached.append_column(name, pa.array(output_host[:, index]))
1205
+ metadata = table_batch.metadata
1206
+ metadata.update(
1207
+ {
1208
+ "backend": self.backend,
1209
+ "copy_bytes": {
1210
+ "array_to_table": int(output_host.nbytes),
1211
+ "table_to_array": int(host.nbytes),
1212
+ "weights": int(
1213
+ weights_batch.metadata.get("static_placement_bytes", 0)
1214
+ ),
1215
+ },
1216
+ "operation": "symbolic_matrix",
1217
+ "provider_calls": 1,
1218
+ }
1219
+ )
1220
+ return {"output": _native.Batch.from_pyarrow(attached, metadata)}
1221
+
1222
+
1223
+ def register_numpy(runtime: Runtime) -> None:
1224
+ import numpy as np
1225
+
1226
+ expression = _ArrayProvider("numpy", np)
1227
+ runtime.register_provider(
1228
+ "numpy",
1229
+ "expression",
1230
+ "1",
1231
+ expression,
1232
+ options_schema=_EXPRESSION_OPTIONS_SCHEMA,
1233
+ )
1234
+ runtime._register_stateless_stream_provider(
1235
+ "numpy",
1236
+ "expression",
1237
+ "1",
1238
+ expression,
1239
+ microbatch_invariant=True,
1240
+ deterministic=True,
1241
+ replay_safe=True,
1242
+ supports_static_inputs=False,
1243
+ array_rules=_NUMPY_STREAM_ARRAY_RULES,
1244
+ )
1245
+ runtime._register_mapping_provider(
1246
+ "numpy",
1247
+ "table_matmul",
1248
+ "1",
1249
+ _TableMatmulProvider("numpy", np),
1250
+ input_ports=_TABLE_MATMUL_INPUT_PORTS,
1251
+ output_ports=_TABLE_MATMUL_OUTPUT_PORTS,
1252
+ )
1253
+ symbolic_matrix = _SymbolicMatrixProvider("numpy", np)
1254
+ runtime._register_mapping_provider(
1255
+ "numpy",
1256
+ "symbolic_matrix",
1257
+ "1",
1258
+ symbolic_matrix,
1259
+ input_ports=_SYMBOLIC_MATRIX_INPUT_PORTS,
1260
+ output_ports=_SYMBOLIC_MATRIX_OUTPUT_PORTS,
1261
+ )
1262
+ runtime._register_stateless_stream_mapping_provider(
1263
+ "numpy",
1264
+ "symbolic_matrix",
1265
+ "1",
1266
+ symbolic_matrix,
1267
+ microbatch_invariant=True,
1268
+ deterministic=True,
1269
+ replay_safe=True,
1270
+ supports_static_inputs=True,
1271
+ array_rules=_NUMPY_MATRIX_STREAM_ARRAY_RULES,
1272
+ )
1273
+
1274
+
1275
+ def register_jax(runtime: Runtime) -> None:
1276
+ import jax.numpy as jnp
1277
+
1278
+ expression = _ArrayProvider("jax", jnp)
1279
+ runtime.register_provider(
1280
+ "jax",
1281
+ "expression",
1282
+ "1",
1283
+ expression,
1284
+ options_schema=_EXPRESSION_OPTIONS_SCHEMA,
1285
+ )
1286
+ runtime._register_stateless_stream_provider(
1287
+ "jax",
1288
+ "expression",
1289
+ "1",
1290
+ expression,
1291
+ microbatch_invariant=True,
1292
+ deterministic=True,
1293
+ replay_safe=True,
1294
+ supports_static_inputs=False,
1295
+ array_rules=_JAX_STREAM_ARRAY_RULES,
1296
+ )
1297
+ runtime._register_mapping_provider(
1298
+ "jax",
1299
+ "table_matmul",
1300
+ "1",
1301
+ _TableMatmulProvider("jax", jnp),
1302
+ input_ports=_TABLE_MATMUL_INPUT_PORTS,
1303
+ output_ports=_TABLE_MATMUL_OUTPUT_PORTS,
1304
+ )
1305
+ symbolic_matrix = _SymbolicMatrixProvider("jax", jnp)
1306
+ runtime._register_mapping_provider(
1307
+ "jax",
1308
+ "symbolic_matrix",
1309
+ "1",
1310
+ symbolic_matrix,
1311
+ input_ports=_SYMBOLIC_MATRIX_INPUT_PORTS,
1312
+ output_ports=_SYMBOLIC_MATRIX_OUTPUT_PORTS,
1313
+ )
1314
+ runtime._register_stateless_stream_mapping_provider(
1315
+ "jax",
1316
+ "symbolic_matrix",
1317
+ "1",
1318
+ symbolic_matrix,
1319
+ microbatch_invariant=True,
1320
+ deterministic=True,
1321
+ replay_safe=True,
1322
+ supports_static_inputs=True,
1323
+ array_rules=_JAX_MATRIX_STREAM_ARRAY_RULES,
1324
+ )