fan-out: 5 π* graduations close the registry chapter
tabular-pinned@v1 + calculus-limit@v1 + calculus-series@v1 + linear-algebra@v1 + function-sampled@v1 — all reserved stubs graduated; the π* registry is now 15 concrete kernels with no remaining reserved-stub entries. #000030 Phase 4 — calculus-limit@v1 ==================================== sp.limit with thread-timeout. One-sided dir support (+/-/+-). Pinned spelling for infinity cases: b"+oo" / b"-oo" / b"zoo" (complex infinity) — bypasses sp.expand since Infinity isn't algebraic. Finite results re-canonicalize through algebra-symbolic recipe (sp.expand + sp.srepr). Unevaluated cases / timeouts emit b"unevaluated:" + sp.srepr(<Limit>) sentinel, mirroring calculus-integral's pattern. #000030 Phase 5 — calculus-series@v1 ===================================== sp.series(f, x, x0, n).removeO() → sp.expand → sp.srepr. Drops O(x**n) remainder explicitly so the canonical form is finite-byte. Sentinel format mirrors limit/integral: b"unevaluated:Series(...)" on timeout. n must be a positive int; 0 / float / negative rejected. #000030 Phase 6 — linear-algebra@v1 ==================================== Single π* covers the whole linear-algebra surface via {op, matrix} JSON. Ops: rref / det / eigenvalues / inverse. Matrix cells go through Fraction(Decimal(str(...))) for floats so 1, 1.0, "1.0" all collapse to Rational(1, 1) — matching arithmetic@v1's discipline. Without this fold, sp.sympify keeps floats as Float (separate type) and downstream det/inverse return Float-shaped bytes. Eigenvalues are sorted by srepr for determinism. Output formats: rref / inverse: rows/cols header + cells joined by | (rows by ||) det: det:<num/den-or-srepr> eigenvalues: eigenvalues:<value-1>x<mult-1>|... #000030 Phase 7 — function-sampled@v1 ====================================== Bridge to time-series-quantized@v1. SymPy expression + linspace grid → quantized integer-vector signature in time-series's exact output format (dt=...;dv=...;n=...;t0=0:v0|v1|...). Two functions that render identically (within sample-grid tolerance) collapse to the same canonical bytes. This is what plotting CAN become in π* terms — the PNG render is a downstream view of the same canonical evidence. Math-only sampler (no numpy in the dep surface); Python's round() is banker's-rounding so the bytes are interchangeable with time-series-quantized@v1's output. Complex / non-finite samples raise PiStarError rather than silently dropping imaginary parts. tabular-pinned@v1 — last reserved stub graduates ================================================= JSON-rows input ({schema, key_columns, rows}); declared key_columns sort policy (stable sort by primary-key tuple); type-fold per column (int/rational/bool through arithmetic@v1 discipline; str verbatim; bool normalized). Header case is PINNED EXACT — Excel and PostgreSQL both care about case; defaulting to lowercase-fold would break operator expectations. Output: header (schema + key + n) + rows joined by \n + cells by |. The π* registry has no remaining reserved stubs. Every modality the substrate paper reserved is now real. Test suite: 1568 passed (was 1467; +101). New closure-criterion test (test_no_stub_pi_stars_remain) replaces the old reserved-stub parametrize — adding a future stub re-opens this list. 110/110 fixtures pass across the 5 new bench-5s-* targets. PHASE_1_CARRIERS gained calculus / linear-algebra / function-sampled / tabular.
This commit is contained in:
parent
d34ecb27c1
commit
abe5988bef
22 changed files with 1950 additions and 35 deletions
30
Makefile
30
Makefile
|
|
@ -298,6 +298,36 @@ bench-5s-time-series: bootstrap ## 5S time-series-quantized π* (SQD §13.5; qua
|
|||
PYTHONUNBUFFERED=1 $(PY) -m bench.batteries.runner --battery 5s --sub semantics \
|
||||
--fixtures bench/fixtures/5s/semantics-time-series-v1.jsonl
|
||||
|
||||
bench-5s-tabular: bootstrap ## 5S tabular-pinned π* (#000030/Phase tabular; declared-schema 2D structured-data canonicalizer)
|
||||
PYTHONUNBUFFERED=1 $(PY) -m bench.batteries.runner --battery 5s --sub syntax \
|
||||
--fixtures bench/fixtures/5s/syntax-tabular-v1.jsonl
|
||||
PYTHONUNBUFFERED=1 $(PY) -m bench.batteries.runner --battery 5s --sub semantics \
|
||||
--fixtures bench/fixtures/5s/semantics-tabular-v1.jsonl
|
||||
|
||||
bench-5s-calculus-limit: bootstrap ## 5S calculus-limit π* (#000030 Phase 4)
|
||||
PYTHONUNBUFFERED=1 $(PY) -m bench.batteries.runner --battery 5s --sub syntax \
|
||||
--fixtures bench/fixtures/5s/syntax-calculus-limit-v1.jsonl
|
||||
PYTHONUNBUFFERED=1 $(PY) -m bench.batteries.runner --battery 5s --sub semantics \
|
||||
--fixtures bench/fixtures/5s/semantics-calculus-limit-v1.jsonl
|
||||
|
||||
bench-5s-calculus-series: bootstrap ## 5S calculus-series π* (#000030 Phase 5)
|
||||
PYTHONUNBUFFERED=1 $(PY) -m bench.batteries.runner --battery 5s --sub syntax \
|
||||
--fixtures bench/fixtures/5s/syntax-calculus-series-v1.jsonl
|
||||
PYTHONUNBUFFERED=1 $(PY) -m bench.batteries.runner --battery 5s --sub semantics \
|
||||
--fixtures bench/fixtures/5s/semantics-calculus-series-v1.jsonl
|
||||
|
||||
bench-5s-linear-algebra: bootstrap ## 5S linear-algebra π* (#000030 Phase 6)
|
||||
PYTHONUNBUFFERED=1 $(PY) -m bench.batteries.runner --battery 5s --sub syntax \
|
||||
--fixtures bench/fixtures/5s/syntax-linear-algebra-v1.jsonl
|
||||
PYTHONUNBUFFERED=1 $(PY) -m bench.batteries.runner --battery 5s --sub semantics \
|
||||
--fixtures bench/fixtures/5s/semantics-linear-algebra-v1.jsonl
|
||||
|
||||
bench-5s-function-sampled: bootstrap ## 5S function-sampled π* (#000030 Phase 7; SymPy → time-series bridge)
|
||||
PYTHONUNBUFFERED=1 $(PY) -m bench.batteries.runner --battery 5s --sub syntax \
|
||||
--fixtures bench/fixtures/5s/syntax-function-sampled-v1.jsonl
|
||||
PYTHONUNBUFFERED=1 $(PY) -m bench.batteries.runner --battery 5s --sub semantics \
|
||||
--fixtures bench/fixtures/5s/semantics-function-sampled-v1.jsonl
|
||||
|
||||
bench-real-shard: bootstrap ## #000026 Phase 2 — real-shard workload baseline (latency, audit, primary-source use)
|
||||
PYTHONUNBUFFERED=1 $(PY) -m bench.scripts.real_shard_baseline \
|
||||
--shards-dir $${ARBORIST_SHARDS_DIR:-$$HOME/.arborist/shards} \
|
||||
|
|
|
|||
|
|
@ -22,16 +22,26 @@ Concrete π*'s shipped today (alphabetical):
|
|||
thread-timeout + expand). Optional; gates on ``sympy``. Returns
|
||||
an ``unevaluated:Integral(...)`` sentinel when no closed form
|
||||
exists or the timeout fires.
|
||||
- ``calculus-limit@v1`` — symbolic limit (one/two-sided +
|
||||
±∞ / complex-infinity sentinels). Optional; gates on ``sympy``.
|
||||
- ``calculus-series@v1`` — truncated Taylor / Maclaurin series
|
||||
(drops the ``O(x**n)`` remainder for finite-byte canonical).
|
||||
Optional; gates on ``sympy``.
|
||||
- ``claim-lattice@v1`` — claim lines → JSON parsed-claim list.
|
||||
- ``code-py-ast@v1`` — Python source → canonical AST S-expression.
|
||||
- ``function-sampled@v1`` — symbolic expression + sample grid →
|
||||
quantized integer-vector signature in time-series-quantized@v1
|
||||
format. Optional; gates on ``sympy``.
|
||||
- ``linear-algebra@v1`` — matrix RREF / det / eigenvalues / inverse
|
||||
via ``{op, matrix}`` JSON. Optional; gates on ``sympy``.
|
||||
- ``logic-kernel@v1`` — propositional Boolean → CNF (SQD §14.3).
|
||||
- ``tabular-pinned@v1`` — JSON-rows → pinned-schema canonical bytes
|
||||
(last reserved-stub graduated; closes the registry chapter).
|
||||
- ``time-series-quantized@v1`` — JSON sample array → quantized
|
||||
integer vector (SQD §13.5).
|
||||
- ``wikitext-base@v1`` — wraps :func:`arborist.wikitext.to_base`.
|
||||
|
||||
Last reserved stub:
|
||||
|
||||
- ``tabular-pinned@v1`` (raises :class:`NotImplementedError`).
|
||||
The registry has no remaining reserved stubs.
|
||||
|
||||
Composition theory: see ``docs/pi-star-composition.md``.
|
||||
"""
|
||||
|
|
@ -63,8 +73,12 @@ from arborist.pi_star import algebra_symbolic_simplified # noqa: F401,E402
|
|||
from arborist.pi_star import arithmetic # noqa: F401,E402
|
||||
from arborist.pi_star import calculus_derivative # noqa: F401,E402
|
||||
from arborist.pi_star import calculus_integral # noqa: F401,E402
|
||||
from arborist.pi_star import calculus_limit # noqa: F401,E402
|
||||
from arborist.pi_star import calculus_series # noqa: F401,E402
|
||||
from arborist.pi_star import claim_lattice # noqa: F401,E402
|
||||
from arborist.pi_star import code # noqa: F401,E402
|
||||
from arborist.pi_star import function_sampled # noqa: F401,E402
|
||||
from arborist.pi_star import linear_algebra # noqa: F401,E402
|
||||
from arborist.pi_star import logic # noqa: F401,E402
|
||||
from arborist.pi_star import tabular # noqa: F401,E402
|
||||
from arborist.pi_star import text # noqa: F401,E402
|
||||
|
|
|
|||
218
arborist/pi_star/calculus_limit.py
Normal file
218
arborist/pi_star/calculus_limit.py
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
"""``calculus-limit@v1`` π* — symbolic-limit canonicalizer.
|
||||
|
||||
Domain: ``calculus``. Computes ``lim_{x → point} f(x)``, optionally
|
||||
one-sided. Wraps ``sp.limit`` with a soft timeout so pathological
|
||||
inputs cannot hang the caller indefinitely.
|
||||
|
||||
Why this matters
|
||||
----------------
|
||||
Ticket #000030 §2.2 Phase 4. Limit is the third calculus primitive
|
||||
after derivative (Phase 2) and integral (Phase 3). Same JSON-shaped
|
||||
input contract; canonicalizes through ``algebra-symbolic@v1`` for
|
||||
finite-result cases. Infinite-result cases pin a stable spelling
|
||||
(``+oo`` / ``-oo`` / ``zoo``) so two LLM-spellings of "infinity"
|
||||
collapse.
|
||||
|
||||
Input shape (JSON, UTF-8)
|
||||
-------------------------
|
||||
::
|
||||
|
||||
{"f": "<expression-text>",
|
||||
"x": "<variable-name>",
|
||||
"point": "<sympify-able-point>",
|
||||
"dir"?: "+" | "-" | "+-",
|
||||
"timeout_seconds"?: <float>}
|
||||
|
||||
- ``f`` — the function (parsed via :func:`sympy.sympify`).
|
||||
- ``x`` — the variable name.
|
||||
- ``point`` — the limit point. Sympify-friendly: ``"0"``, ``"oo"``,
|
||||
``"-oo"``, ``"pi"`` all work.
|
||||
- ``dir`` — optional one-sided limit direction. Default is the
|
||||
two-sided limit (``"+-"`` in SymPy parlance, or omitted from the
|
||||
call). ``"+"`` for right-side only, ``"-"`` for left-side.
|
||||
- ``timeout_seconds`` — optional, default :data:`DEFAULT_TIMEOUT`.
|
||||
|
||||
Output canonical bytes
|
||||
----------------------
|
||||
Three paths:
|
||||
|
||||
1. **Finite result** — ``sp.srepr(sp.expand(limit_result))`` UTF-8.
|
||||
Same recipe as ``algebra-symbolic@v1`` so numerical limits
|
||||
(``lim sin(x)/x → 1``) compose with the rest of the math
|
||||
substrate.
|
||||
2. **Infinite result** — pinned spelling: ``b"+oo"`` for ``+∞``,
|
||||
``b"-oo"`` for ``-∞``, ``b"zoo"`` for SymPy's ``ComplexInfinity``.
|
||||
These don't go through ``sp.expand`` since ``Infinity`` is not
|
||||
an algebraic object.
|
||||
3. **No closed form / timeout** — bytes are
|
||||
``b"unevaluated:" + sp.srepr(<Limit>)``. Mirrors
|
||||
``calculus-integral@v1``'s sentinel discipline.
|
||||
|
||||
Optional dependency
|
||||
-------------------
|
||||
Same as ``algebra-symbolic@v1``: SymPy via the ``[math]`` extras.
|
||||
|
||||
Source: ticket #000030 §2.2 Phase 4 — landed 2026-05-09.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import concurrent.futures
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
|
||||
from arborist.pi_star.protocol import PiStarError
|
||||
from arborist.pi_star.registry import register
|
||||
|
||||
|
||||
try:
|
||||
import sympy as sp # type: ignore[import-not-found]
|
||||
except ImportError: # pragma: no cover — optional extra
|
||||
sp = None # type: ignore[assignment]
|
||||
|
||||
|
||||
DEFAULT_TIMEOUT = 30.0
|
||||
|
||||
|
||||
_SYMPY_REQUIRED_MSG = (
|
||||
"calculus-limit@v1 requires sympy; install with "
|
||||
"`pip install 'arborist[math]'`"
|
||||
)
|
||||
|
||||
|
||||
_VALID_DIRS = ("+", "-", "+-")
|
||||
|
||||
|
||||
def _serialize_infinity(result) -> bytes | None:
|
||||
"""Map SymPy infinities to pinned UTF-8 byte spellings, or None
|
||||
if the result isn't an infinity case."""
|
||||
if result is sp.oo or result == sp.oo:
|
||||
return b"+oo"
|
||||
if result is -sp.oo or result == -sp.oo:
|
||||
return b"-oo"
|
||||
if result is sp.zoo or result == sp.zoo:
|
||||
return b"zoo"
|
||||
return None
|
||||
|
||||
|
||||
def _is_unevaluated(result) -> bool:
|
||||
"""True when ``sp.limit`` returned an unevaluated form."""
|
||||
return isinstance(result, sp.Limit)
|
||||
|
||||
|
||||
def _sentinel(unevaluated) -> bytes:
|
||||
return b"unevaluated:" + sp.srepr(unevaluated).encode("utf-8")
|
||||
|
||||
|
||||
def _build_unevaluated(f_expr, x_sym, point_expr, dir_):
|
||||
if dir_ in (None, "+-"):
|
||||
return sp.Limit(f_expr, x_sym, point_expr)
|
||||
return sp.Limit(f_expr, x_sym, point_expr, dir_)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CalculusLimitV1:
|
||||
name: str = "calculus-limit"
|
||||
version: str = "v1"
|
||||
domain: str = "calculus"
|
||||
|
||||
def canonicalize(self, raw: bytes) -> bytes:
|
||||
if sp is None:
|
||||
raise PiStarError(_SYMPY_REQUIRED_MSG)
|
||||
if not isinstance(raw, (bytes, bytearray)):
|
||||
raise PiStarError(
|
||||
"calculus-limit@v1 expects bytes; got "
|
||||
f"{type(raw).__name__}"
|
||||
)
|
||||
try:
|
||||
text = raw.decode("utf-8", errors="surrogatepass")
|
||||
except UnicodeDecodeError as exc: # pragma: no cover
|
||||
raise PiStarError(f"input not valid UTF-8: {exc}") from exc
|
||||
|
||||
try:
|
||||
obj = json.loads(text)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise PiStarError(
|
||||
f"calculus-limit@v1 input is not valid JSON: {exc}"
|
||||
) from exc
|
||||
if not isinstance(obj, dict):
|
||||
raise PiStarError(
|
||||
"calculus-limit@v1 input must be a JSON object"
|
||||
)
|
||||
for required in ("f", "x", "point"):
|
||||
if required not in obj:
|
||||
raise PiStarError(
|
||||
f"calculus-limit@v1 missing required field {required!r}"
|
||||
)
|
||||
|
||||
f_text = obj["f"]
|
||||
x_name = obj["x"]
|
||||
point_text = obj["point"]
|
||||
dir_ = obj.get("dir")
|
||||
if dir_ is not None and dir_ not in _VALID_DIRS:
|
||||
raise PiStarError(
|
||||
f"calculus-limit@v1 dir must be one of {_VALID_DIRS}; "
|
||||
f"got {dir_!r}"
|
||||
)
|
||||
timeout_s = float(obj.get("timeout_seconds", DEFAULT_TIMEOUT))
|
||||
|
||||
if not isinstance(f_text, str) or not f_text.strip():
|
||||
raise PiStarError("calculus-limit@v1 f must be a non-empty string")
|
||||
if not isinstance(x_name, str) or not x_name.strip():
|
||||
raise PiStarError("calculus-limit@v1 x must be a non-empty string")
|
||||
|
||||
try:
|
||||
x_sym = sp.Symbol(x_name)
|
||||
f_expr = sp.sympify(f_text, locals={x_name: x_sym})
|
||||
# ``point`` may be "oo", "-oo", "pi", "0", etc.
|
||||
point_expr = sp.sympify(point_text)
|
||||
except (sp.SympifyError, SyntaxError, TypeError) as exc:
|
||||
raise PiStarError(
|
||||
f"calculus-limit@v1 cannot parse: {exc}"
|
||||
) from exc
|
||||
|
||||
# SymPy's limit() takes the dir as a string; default
|
||||
# two-sided. Use a thread + timeout for the same reason
|
||||
# calculus-integral does.
|
||||
def _compute():
|
||||
if dir_ in (None, "+-"):
|
||||
return sp.limit(f_expr, x_sym, point_expr)
|
||||
return sp.limit(f_expr, x_sym, point_expr, dir_)
|
||||
|
||||
try:
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as ex:
|
||||
future = ex.submit(_compute)
|
||||
try:
|
||||
result = future.result(timeout=timeout_s)
|
||||
except concurrent.futures.TimeoutError:
|
||||
return _sentinel(
|
||||
_build_unevaluated(f_expr, x_sym, point_expr, dir_)
|
||||
)
|
||||
except (sp.SympifyError, ValueError, TypeError) as exc:
|
||||
raise PiStarError(
|
||||
f"calculus-limit@v1 evaluation failed: {exc}"
|
||||
) from exc
|
||||
|
||||
# Infinity / complex-infinity: pinned spelling.
|
||||
inf_bytes = _serialize_infinity(result)
|
||||
if inf_bytes is not None:
|
||||
return inf_bytes
|
||||
|
||||
# SymPy may also return an unevaluated Limit object on weird
|
||||
# inputs even without timeout firing — fall back to sentinel.
|
||||
if _is_unevaluated(result):
|
||||
return _sentinel(result)
|
||||
|
||||
# Finite result — canonicalize through algebra-symbolic@v1's
|
||||
# recipe (sp.expand + sp.srepr).
|
||||
try:
|
||||
expanded = sp.expand(result)
|
||||
except Exception as exc: # pragma: no cover — sympy edge cases
|
||||
raise PiStarError(
|
||||
f"calculus-limit@v1 cannot expand result: {exc}"
|
||||
) from exc
|
||||
return sp.srepr(expanded).encode("utf-8")
|
||||
|
||||
|
||||
if sp is not None:
|
||||
register(CalculusLimitV1())
|
||||
171
arborist/pi_star/calculus_series.py
Normal file
171
arborist/pi_star/calculus_series.py
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
"""``calculus-series@v1`` π* — Taylor / Maclaurin series canonicalizer.
|
||||
|
||||
Domain: ``calculus``. Computes the truncated Taylor series of
|
||||
``f`` around ``x = x0`` to ``n`` terms via ``sp.series``. The
|
||||
``O(x**n)`` remainder is dropped — that's what makes the series
|
||||
finite-byte canonical.
|
||||
|
||||
Why this matters
|
||||
----------------
|
||||
Ticket #000030 §2.2 Phase 5. Series complete the calculus-primitive
|
||||
quartet (derivative · integral · limit · series) on the SymPy
|
||||
substrate. All four ride ``algebra-symbolic@v1``'s
|
||||
``sp.expand + sp.srepr`` normalizer for the closed-form output, so
|
||||
the four kernels compose cleanly.
|
||||
|
||||
Input shape (JSON, UTF-8)
|
||||
-------------------------
|
||||
::
|
||||
|
||||
{"f": "<expression-text>",
|
||||
"x": "<variable-name>",
|
||||
"x0": "<sympify-able-point>",
|
||||
"n": <positive-int>,
|
||||
"timeout_seconds"?: <float>}
|
||||
|
||||
- ``f`` — the function to expand.
|
||||
- ``x`` — the variable.
|
||||
- ``x0`` — the expansion point. ``"0"`` gives a Maclaurin series.
|
||||
- ``n`` — number of terms (the ``n`` in SymPy's ``series(..., n=n)``).
|
||||
Must be a positive integer.
|
||||
- ``timeout_seconds`` — optional, default :data:`DEFAULT_TIMEOUT`.
|
||||
|
||||
Output canonical bytes
|
||||
----------------------
|
||||
Two paths:
|
||||
|
||||
1. **Series with closed-form coefficients** — the truncated
|
||||
polynomial after ``.removeO()``, fed through
|
||||
``sp.expand + sp.srepr``. Two equivalent series collapse.
|
||||
2. **Timeout** — bytes are
|
||||
``b"unevaluated:" + sp.srepr(<original f, x0, n>)``. Mirrors
|
||||
``calculus-integral@v1`` / ``calculus-limit@v1`` sentinel
|
||||
discipline.
|
||||
|
||||
Optional dependency
|
||||
-------------------
|
||||
Same as ``algebra-symbolic@v1``: SymPy via the ``[math]`` extras.
|
||||
|
||||
Source: ticket #000030 §2.2 Phase 5 — landed 2026-05-09.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import concurrent.futures
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
|
||||
from arborist.pi_star.protocol import PiStarError
|
||||
from arborist.pi_star.registry import register
|
||||
|
||||
|
||||
try:
|
||||
import sympy as sp # type: ignore[import-not-found]
|
||||
except ImportError: # pragma: no cover — optional extra
|
||||
sp = None # type: ignore[assignment]
|
||||
|
||||
|
||||
DEFAULT_TIMEOUT = 30.0
|
||||
|
||||
|
||||
_SYMPY_REQUIRED_MSG = (
|
||||
"calculus-series@v1 requires sympy; install with "
|
||||
"`pip install 'arborist[math]'`"
|
||||
)
|
||||
|
||||
|
||||
def _sentinel(f_expr, x_sym, x0_expr, n: int) -> bytes:
|
||||
# Build a stable sentinel from inputs (we don't have a SymPy
|
||||
# "unevaluated series" class). Hash-able by srepr of the tuple.
|
||||
payload = f"Series({sp.srepr(f_expr)}, {sp.srepr(x_sym)}, {sp.srepr(x0_expr)}, {n})"
|
||||
return b"unevaluated:" + payload.encode("utf-8")
|
||||
|
||||
|
||||
@dataclass
|
||||
class CalculusSeriesV1:
|
||||
name: str = "calculus-series"
|
||||
version: str = "v1"
|
||||
domain: str = "calculus"
|
||||
|
||||
def canonicalize(self, raw: bytes) -> bytes:
|
||||
if sp is None:
|
||||
raise PiStarError(_SYMPY_REQUIRED_MSG)
|
||||
if not isinstance(raw, (bytes, bytearray)):
|
||||
raise PiStarError(
|
||||
"calculus-series@v1 expects bytes; got "
|
||||
f"{type(raw).__name__}"
|
||||
)
|
||||
try:
|
||||
text = raw.decode("utf-8", errors="surrogatepass")
|
||||
except UnicodeDecodeError as exc: # pragma: no cover
|
||||
raise PiStarError(f"input not valid UTF-8: {exc}") from exc
|
||||
|
||||
try:
|
||||
obj = json.loads(text)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise PiStarError(
|
||||
f"calculus-series@v1 input is not valid JSON: {exc}"
|
||||
) from exc
|
||||
if not isinstance(obj, dict):
|
||||
raise PiStarError(
|
||||
"calculus-series@v1 input must be a JSON object"
|
||||
)
|
||||
for required in ("f", "x", "x0", "n"):
|
||||
if required not in obj:
|
||||
raise PiStarError(
|
||||
f"calculus-series@v1 missing required field "
|
||||
f"{required!r}"
|
||||
)
|
||||
|
||||
f_text = obj["f"]
|
||||
x_name = obj["x"]
|
||||
x0_text = obj["x0"]
|
||||
n_terms = obj["n"]
|
||||
timeout_s = float(obj.get("timeout_seconds", DEFAULT_TIMEOUT))
|
||||
|
||||
if not isinstance(f_text, str) or not f_text.strip():
|
||||
raise PiStarError("calculus-series@v1 f must be a non-empty string")
|
||||
if not isinstance(x_name, str) or not x_name.strip():
|
||||
raise PiStarError("calculus-series@v1 x must be a non-empty string")
|
||||
# JSON booleans are subclasses of int in Python; reject explicitly.
|
||||
if isinstance(n_terms, bool) or not isinstance(n_terms, int) or n_terms < 1:
|
||||
raise PiStarError(
|
||||
f"calculus-series@v1 n must be a positive integer; got "
|
||||
f"{n_terms!r}"
|
||||
)
|
||||
|
||||
try:
|
||||
x_sym = sp.Symbol(x_name)
|
||||
f_expr = sp.sympify(f_text, locals={x_name: x_sym})
|
||||
x0_expr = sp.sympify(x0_text)
|
||||
except (sp.SympifyError, SyntaxError, TypeError) as exc:
|
||||
raise PiStarError(
|
||||
f"calculus-series@v1 cannot parse: {exc}"
|
||||
) from exc
|
||||
|
||||
def _compute():
|
||||
return sp.series(f_expr, x_sym, x0_expr, n_terms).removeO()
|
||||
|
||||
try:
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as ex:
|
||||
future = ex.submit(_compute)
|
||||
try:
|
||||
truncated = future.result(timeout=timeout_s)
|
||||
except concurrent.futures.TimeoutError:
|
||||
return _sentinel(f_expr, x_sym, x0_expr, n_terms)
|
||||
except (sp.SympifyError, ValueError, TypeError) as exc:
|
||||
raise PiStarError(
|
||||
f"calculus-series@v1 evaluation failed: {exc}"
|
||||
) from exc
|
||||
|
||||
try:
|
||||
expanded = sp.expand(truncated)
|
||||
except Exception as exc: # pragma: no cover
|
||||
raise PiStarError(
|
||||
f"calculus-series@v1 cannot expand result: {exc}"
|
||||
) from exc
|
||||
return sp.srepr(expanded).encode("utf-8")
|
||||
|
||||
|
||||
if sp is not None:
|
||||
register(CalculusSeriesV1())
|
||||
236
arborist/pi_star/function_sampled.py
Normal file
236
arborist/pi_star/function_sampled.py
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
"""``function-sampled@v1`` π* — symbolic function sampler.
|
||||
|
||||
Domain: ``function-sampled``. Bridges symbolic SymPy expressions
|
||||
to the existing ``time-series-quantized@v1`` integer-vector
|
||||
canonical form. Two functions that render identically (within
|
||||
sample-grid tolerance) collapse to the same canonical bytes.
|
||||
|
||||
Why this matters
|
||||
----------------
|
||||
Ticket #000030 §2.2 Phase 7. This is what plotting CAN become in
|
||||
π* terms: a function is identified by its quantized samples on a
|
||||
declared grid. Different libraries / DPIs / palettes don't matter
|
||||
— the canonical bytes pin the value-at-each-x. The PNG render is
|
||||
a downstream view of the same canonical evidence.
|
||||
|
||||
Input shape (JSON, UTF-8)
|
||||
-------------------------
|
||||
::
|
||||
|
||||
{
|
||||
"f": "<expression-text>",
|
||||
"x": "<variable-name>",
|
||||
"x_min": <number>,
|
||||
"x_max": <number>,
|
||||
"n_samples": <positive int>,
|
||||
"dv": <positive number>
|
||||
}
|
||||
|
||||
- ``f`` — expression to sample (SymPy parsable).
|
||||
- ``x`` — independent-variable name.
|
||||
- ``x_min``, ``x_max`` — sample-grid endpoints (inclusive).
|
||||
- ``n_samples`` — number of points on the grid (linspace).
|
||||
- ``dv`` — value-quantization step. Same role as
|
||||
``time-series-quantized@v1`` ``dv``: each y-sample is rounded to
|
||||
``round(y / dv)`` (banker's rounding).
|
||||
|
||||
Output canonical bytes
|
||||
----------------------
|
||||
Reuses ``time-series-quantized@v1`` exactly::
|
||||
|
||||
dt=<dt>;dv=<dv>;n=<n>;t0=<t0>:v0|v1|v2|...
|
||||
|
||||
Where ``dt = (x_max - x_min) / (n_samples - 1)``, ``t0 = 0`` (the
|
||||
linspace index), and each ``v_i = round(f(x_i) / dv)``. This
|
||||
makes the sampled signature byte-compatible with
|
||||
``time-series-quantized@v1`` storage and inspection.
|
||||
|
||||
Equivalence classes preserved
|
||||
-----------------------------
|
||||
- Two expressions that evaluate to the same numeric values on the
|
||||
same grid → same bytes. ``sin(x)`` and ``2*sin(x)/2`` collapse
|
||||
trivially; the harder cases (``sin(x)**2 + cos(x)**2`` ≡ ``1``)
|
||||
don't go through SymPy `simplify` here, so they only collapse
|
||||
if the sampler observes the same numeric values within the dv
|
||||
tolerance — which they will.
|
||||
- Different but equivalent JSON whitespace / formatting: same.
|
||||
|
||||
Equivalence classes kept distinct
|
||||
---------------------------------
|
||||
- Different grid (x_min, x_max, n_samples) → different canonical.
|
||||
The sample-grid IS part of identity.
|
||||
- Different ``dv`` (quantization tightness) → different canonical.
|
||||
- Different expression that diverges at any sample → different.
|
||||
|
||||
Round-trip property
|
||||
-------------------
|
||||
Projective. The canonical text isn't valid input JSON; rerunning
|
||||
on the canonical bytes raises :class:`PiStarError`.
|
||||
|
||||
Optional dependency
|
||||
-------------------
|
||||
SymPy via the ``[math]`` extras (lambdify needs it). NumPy is
|
||||
required transitively but ships standalone via SymPy's lambdify
|
||||
backend; tests use ``pytest.importorskip("sympy")``.
|
||||
|
||||
Source: ticket #000030 §2.2 Phase 7 — landed 2026-05-09.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
from arborist.pi_star.protocol import PiStarError
|
||||
from arborist.pi_star.registry import register
|
||||
|
||||
|
||||
try:
|
||||
import sympy as sp # type: ignore[import-not-found]
|
||||
except ImportError: # pragma: no cover — optional extra
|
||||
sp = None # type: ignore[assignment]
|
||||
|
||||
|
||||
_SYMPY_REQUIRED_MSG = (
|
||||
"function-sampled@v1 requires sympy; install with "
|
||||
"`pip install 'arborist[math]'`"
|
||||
)
|
||||
|
||||
|
||||
def _format_num(x) -> str:
|
||||
"""Mirror time_series._num: int → 'N'; integer-valued float → 'N';
|
||||
otherwise repr (shortest round-trip)."""
|
||||
if isinstance(x, int) or (isinstance(x, float) and x.is_integer()):
|
||||
return str(int(x))
|
||||
return repr(x)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FunctionSampledV1:
|
||||
name: str = "function-sampled"
|
||||
version: str = "v1"
|
||||
domain: str = "function-sampled"
|
||||
|
||||
def canonicalize(self, raw: bytes) -> bytes:
|
||||
if sp is None:
|
||||
raise PiStarError(_SYMPY_REQUIRED_MSG)
|
||||
if not isinstance(raw, (bytes, bytearray)):
|
||||
raise PiStarError(
|
||||
"function-sampled@v1 expects bytes; got "
|
||||
f"{type(raw).__name__}"
|
||||
)
|
||||
try:
|
||||
text = raw.decode("utf-8", errors="surrogatepass")
|
||||
except UnicodeDecodeError as exc: # pragma: no cover
|
||||
raise PiStarError(f"input not valid UTF-8: {exc}") from exc
|
||||
|
||||
try:
|
||||
obj = json.loads(text)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise PiStarError(
|
||||
f"function-sampled@v1 input is not valid JSON: {exc}"
|
||||
) from exc
|
||||
if not isinstance(obj, dict):
|
||||
raise PiStarError(
|
||||
"function-sampled@v1 input must be a JSON object"
|
||||
)
|
||||
for required in ("f", "x", "x_min", "x_max", "n_samples", "dv"):
|
||||
if required not in obj:
|
||||
raise PiStarError(
|
||||
f"function-sampled@v1 missing required field "
|
||||
f"{required!r}"
|
||||
)
|
||||
|
||||
f_text = obj["f"]
|
||||
x_name = obj["x"]
|
||||
x_min = obj["x_min"]
|
||||
x_max = obj["x_max"]
|
||||
n_samples = obj["n_samples"]
|
||||
dv = obj["dv"]
|
||||
|
||||
if not isinstance(f_text, str) or not f_text.strip():
|
||||
raise PiStarError("function-sampled@v1 f must be non-empty string")
|
||||
if not isinstance(x_name, str) or not x_name.strip():
|
||||
raise PiStarError("function-sampled@v1 x must be non-empty string")
|
||||
if not isinstance(x_min, (int, float)) or isinstance(x_min, bool):
|
||||
raise PiStarError("function-sampled@v1 x_min must be number")
|
||||
if not isinstance(x_max, (int, float)) or isinstance(x_max, bool):
|
||||
raise PiStarError("function-sampled@v1 x_max must be number")
|
||||
if x_max <= x_min:
|
||||
raise PiStarError(
|
||||
f"function-sampled@v1 x_max must be > x_min; got "
|
||||
f"x_min={x_min!r}, x_max={x_max!r}"
|
||||
)
|
||||
if isinstance(n_samples, bool) or not isinstance(n_samples, int) or n_samples < 2:
|
||||
raise PiStarError(
|
||||
"function-sampled@v1 n_samples must be int >= 2"
|
||||
)
|
||||
if isinstance(dv, bool) or not isinstance(dv, (int, float)) or dv <= 0:
|
||||
raise PiStarError(
|
||||
f"function-sampled@v1 dv must be positive number; got {dv!r}"
|
||||
)
|
||||
|
||||
try:
|
||||
x_sym = sp.Symbol(x_name)
|
||||
f_expr = sp.sympify(f_text, locals={x_name: x_sym})
|
||||
except (sp.SympifyError, SyntaxError, TypeError) as exc:
|
||||
raise PiStarError(
|
||||
f"function-sampled@v1 cannot parse f: {exc}"
|
||||
) from exc
|
||||
|
||||
# Sample without numpy — keep dependency surface small. The
|
||||
# output is byte-identical to time-series-quantized@v1's
|
||||
# rounding (Python's round() is banker's-rounding, matching
|
||||
# numpy's default).
|
||||
try:
|
||||
f_callable = sp.lambdify(x_sym, f_expr, "math")
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise PiStarError(
|
||||
f"function-sampled@v1 cannot lambdify f: {exc}"
|
||||
) from exc
|
||||
|
||||
dt_value = (x_max - x_min) / (n_samples - 1)
|
||||
values: list[int] = []
|
||||
for i in range(n_samples):
|
||||
xi = x_min + i * dt_value
|
||||
try:
|
||||
yi = f_callable(xi)
|
||||
except (ValueError, ZeroDivisionError, OverflowError) as exc:
|
||||
raise PiStarError(
|
||||
f"function-sampled@v1 evaluation failed at x={xi!r}: "
|
||||
f"{exc}"
|
||||
) from exc
|
||||
if isinstance(yi, complex):
|
||||
if abs(yi.imag) > 1e-12:
|
||||
raise PiStarError(
|
||||
f"function-sampled@v1 got complex value at "
|
||||
f"x={xi!r}: {yi!r}"
|
||||
)
|
||||
yi = yi.real
|
||||
if not isinstance(yi, (int, float)):
|
||||
raise PiStarError(
|
||||
f"function-sampled@v1 non-numeric sample at x={xi!r}: "
|
||||
f"{yi!r} (type {type(yi).__name__})"
|
||||
)
|
||||
if math.isnan(yi) or math.isinf(yi):
|
||||
raise PiStarError(
|
||||
f"function-sampled@v1 non-finite sample at x={xi!r}: "
|
||||
f"{yi!r}"
|
||||
)
|
||||
values.append(int(round(yi / dv)))
|
||||
|
||||
# Serialize in time-series-quantized@v1 format so the bytes
|
||||
# compose identically (function-sampled output IS a quantized
|
||||
# series, just one derived from a closed-form expression
|
||||
# rather than an array of samples).
|
||||
dt_s = _format_num(dt_value)
|
||||
dv_s = _format_num(dv)
|
||||
n = len(values)
|
||||
body = "|".join(str(v) for v in values)
|
||||
out = f"dt={dt_s};dv={dv_s};n={n};t0=0:{body}"
|
||||
return out.encode("utf-8")
|
||||
|
||||
|
||||
if sp is not None:
|
||||
register(FunctionSampledV1())
|
||||
284
arborist/pi_star/linear_algebra.py
Normal file
284
arborist/pi_star/linear_algebra.py
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
"""``linear-algebra@v1`` π* — matrix-operation canonicalizer.
|
||||
|
||||
Domain: ``linear-algebra``. One π* covers the whole linear-algebra
|
||||
surface via an ``op`` field in the input JSON. Matrix entries are
|
||||
folded through SymPy's exact-rational arithmetic so
|
||||
``[[1, 2], [3, 4]]`` and ``[[1.0, 2.0], [3.0, 4.0]]`` collapse.
|
||||
|
||||
Why this matters
|
||||
----------------
|
||||
Ticket #000030 §2.2 Phase 6. Linear algebra rounds out the math
|
||||
substrate alongside the calculus quartet. Same ``[math]`` extras
|
||||
gate; same `srepr`-based canonical-bytes discipline as the rest
|
||||
of the SymPy substrate.
|
||||
|
||||
Input shape (JSON, UTF-8)
|
||||
-------------------------
|
||||
::
|
||||
|
||||
{
|
||||
"op": "rref" | "det" | "eigenvalues" | "inverse",
|
||||
"matrix": [[<cell>, <cell>, ...], ...]
|
||||
}
|
||||
|
||||
- ``op`` selects the operation. Each operation has its own canonical
|
||||
output shape; see below.
|
||||
- ``matrix`` is a row-major 2D list. Cells go through
|
||||
:func:`sympy.sympify` so ``"1/2"``, ``1`` and ``1.0`` all work
|
||||
and fold to the same rational (see ``arithmetic@v1`` discipline).
|
||||
|
||||
Canonical output by op
|
||||
----------------------
|
||||
|
||||
``op = "rref"`` — Reduced Row Echelon Form. Output:
|
||||
|
||||
rref;rows=<r>;cols=<c>:cell00|cell01|...||cell10|...
|
||||
|
||||
Cells are printed as ``num/den`` (rational lowest-terms) for
|
||||
integer + rational entries; symbolic entries fall through
|
||||
``sp.srepr``.
|
||||
|
||||
``op = "det"`` — determinant. Output:
|
||||
|
||||
det:<num/den-or-srepr>
|
||||
|
||||
``op = "eigenvalues"`` — eigenvalues with multiplicity. Output:
|
||||
|
||||
eigenvalues:<value-1>x<mult-1>|<value-2>x<mult-2>|...
|
||||
|
||||
Sorted by value's `srepr` for determinism (eigenvalues come back
|
||||
as a dict from SymPy and would otherwise depend on insertion
|
||||
order).
|
||||
|
||||
``op = "inverse"`` — matrix inverse, same shape as ``rref``:
|
||||
|
||||
inverse;rows=<r>;cols=<c>:cell00|...
|
||||
|
||||
Equivalence classes preserved
|
||||
-----------------------------
|
||||
- Cell-formatting variations (``1`` ≡ ``1.0`` ≡ ``"1.0"``) collapse.
|
||||
- Eigenvalue ordering is canonicalized (sorted lexically by
|
||||
``srepr``); two equivalent eigen-decompositions canonicalize
|
||||
identically.
|
||||
|
||||
Equivalence classes kept distinct
|
||||
---------------------------------
|
||||
- Different ``op`` for same matrix → different canonical (the op
|
||||
is part of identity).
|
||||
- Different matrix entries → different canonical.
|
||||
- Different matrix shape (rows/cols) → different canonical.
|
||||
|
||||
Errors
|
||||
------
|
||||
- Singular matrix on ``inverse`` → :class:`PiStarError`.
|
||||
- Non-square matrix on ``det`` / ``inverse`` / ``eigenvalues`` →
|
||||
:class:`PiStarError`.
|
||||
- Unknown ``op`` → :class:`PiStarError`.
|
||||
|
||||
Optional dependency
|
||||
-------------------
|
||||
Same as ``algebra-symbolic@v1``: SymPy via the ``[math]`` extras.
|
||||
|
||||
Source: ticket #000030 §2.2 Phase 6 — landed 2026-05-09.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
|
||||
from arborist.pi_star.protocol import PiStarError
|
||||
from arborist.pi_star.registry import register
|
||||
|
||||
|
||||
try:
|
||||
import sympy as sp # type: ignore[import-not-found]
|
||||
except ImportError: # pragma: no cover — optional extra
|
||||
sp = None # type: ignore[assignment]
|
||||
|
||||
|
||||
_VALID_OPS = ("rref", "det", "eigenvalues", "inverse")
|
||||
|
||||
|
||||
_SYMPY_REQUIRED_MSG = (
|
||||
"linear-algebra@v1 requires sympy; install with "
|
||||
"`pip install 'arborist[math]'`"
|
||||
)
|
||||
|
||||
|
||||
def _format_cell(cell) -> str:
|
||||
"""Render a SymPy entry as canonical text. Rationals get
|
||||
``num/den``; symbolic values fall through ``srepr``."""
|
||||
if isinstance(cell, sp.Rational):
|
||||
return f"{cell.p}/{cell.q}"
|
||||
return sp.srepr(cell)
|
||||
|
||||
|
||||
def _format_matrix(M) -> str:
|
||||
rows = []
|
||||
for i in range(M.rows):
|
||||
cells = [_format_cell(M[i, j]) for j in range(M.cols)]
|
||||
rows.append("|".join(cells))
|
||||
return "||".join(rows)
|
||||
|
||||
|
||||
@dataclass
|
||||
class LinearAlgebraV1:
|
||||
name: str = "linear-algebra"
|
||||
version: str = "v1"
|
||||
domain: str = "linear-algebra"
|
||||
|
||||
def canonicalize(self, raw: bytes) -> bytes:
|
||||
if sp is None:
|
||||
raise PiStarError(_SYMPY_REQUIRED_MSG)
|
||||
if not isinstance(raw, (bytes, bytearray)):
|
||||
raise PiStarError(
|
||||
"linear-algebra@v1 expects bytes; got "
|
||||
f"{type(raw).__name__}"
|
||||
)
|
||||
try:
|
||||
text = raw.decode("utf-8", errors="surrogatepass")
|
||||
except UnicodeDecodeError as exc: # pragma: no cover
|
||||
raise PiStarError(f"input not valid UTF-8: {exc}") from exc
|
||||
|
||||
try:
|
||||
obj = json.loads(text)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise PiStarError(
|
||||
f"linear-algebra@v1 input is not valid JSON: {exc}"
|
||||
) from exc
|
||||
if not isinstance(obj, dict):
|
||||
raise PiStarError(
|
||||
"linear-algebra@v1 input must be a JSON object"
|
||||
)
|
||||
for required in ("op", "matrix"):
|
||||
if required not in obj:
|
||||
raise PiStarError(
|
||||
f"linear-algebra@v1 missing required field "
|
||||
f"{required!r}"
|
||||
)
|
||||
|
||||
op = obj["op"]
|
||||
matrix_raw = obj["matrix"]
|
||||
if op not in _VALID_OPS:
|
||||
raise PiStarError(
|
||||
f"linear-algebra@v1 op must be one of {_VALID_OPS}; "
|
||||
f"got {op!r}"
|
||||
)
|
||||
if not isinstance(matrix_raw, list) or not matrix_raw:
|
||||
raise PiStarError(
|
||||
"linear-algebra@v1 matrix must be a non-empty 2D array"
|
||||
)
|
||||
|
||||
# Sympify each cell. Floats are folded to exact Rational via
|
||||
# Decimal(str(...)) so 1.0 ≡ 1 ≡ "1.0" all collapse to the
|
||||
# same Rational(1, 1) — matching arithmetic@v1's discipline.
|
||||
# Without this, sympy keeps floats as Float (separate type),
|
||||
# and downstream det()/inverse() return Float-shaped bytes
|
||||
# that don't match the int-input bytes.
|
||||
from decimal import Decimal
|
||||
from fractions import Fraction
|
||||
sym_rows: list[list] = []
|
||||
n_cols = None
|
||||
for ri, row in enumerate(matrix_raw):
|
||||
if not isinstance(row, list):
|
||||
raise PiStarError(
|
||||
f"matrix row[{ri}] must be a JSON array"
|
||||
)
|
||||
if n_cols is None:
|
||||
n_cols = len(row)
|
||||
elif len(row) != n_cols:
|
||||
raise PiStarError(
|
||||
f"matrix row[{ri}] has {len(row)} cells; "
|
||||
f"row[0] has {n_cols} (jagged matrix)"
|
||||
)
|
||||
sym_row = []
|
||||
for ci, c in enumerate(row):
|
||||
try:
|
||||
if isinstance(c, bool):
|
||||
raise PiStarError(
|
||||
f"matrix[{ri}][{ci}] is bool; not a number"
|
||||
)
|
||||
if isinstance(c, float):
|
||||
# Float → exact Fraction → SymPy Rational. Avoids
|
||||
# sympy.Rational(Decimal) signature drift across
|
||||
# versions and matches arithmetic@v1's discipline.
|
||||
f = Fraction(Decimal(str(c)))
|
||||
sym_row.append(sp.Rational(f.numerator, f.denominator))
|
||||
elif isinstance(c, int):
|
||||
sym_row.append(sp.Integer(c))
|
||||
else:
|
||||
sym_row.append(sp.sympify(c, rational=True))
|
||||
except (sp.SympifyError, SyntaxError, TypeError, ValueError) as exc:
|
||||
raise PiStarError(
|
||||
f"matrix[{ri}][{ci}] cannot sympify: {exc}"
|
||||
) from exc
|
||||
sym_rows.append(sym_row)
|
||||
|
||||
try:
|
||||
M = sp.Matrix(sym_rows)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise PiStarError(
|
||||
f"linear-algebra@v1 cannot build Matrix: {exc}"
|
||||
) from exc
|
||||
|
||||
# Dispatch by op.
|
||||
if op == "rref":
|
||||
R, _pivots = M.rref()
|
||||
body = (
|
||||
f"rref;rows={R.rows};cols={R.cols}:"
|
||||
f"{_format_matrix(R)}"
|
||||
)
|
||||
return body.encode("utf-8")
|
||||
|
||||
if op == "det":
|
||||
if M.rows != M.cols:
|
||||
raise PiStarError(
|
||||
f"linear-algebra@v1 det requires square matrix; "
|
||||
f"got {M.rows}x{M.cols}"
|
||||
)
|
||||
d = M.det()
|
||||
return f"det:{_format_cell(d)}".encode("utf-8")
|
||||
|
||||
if op == "inverse":
|
||||
if M.rows != M.cols:
|
||||
raise PiStarError(
|
||||
f"linear-algebra@v1 inverse requires square matrix; "
|
||||
f"got {M.rows}x{M.cols}"
|
||||
)
|
||||
try:
|
||||
Inv = M.inv()
|
||||
except Exception as exc:
|
||||
# SymPy raises NonInvertibleMatrixError on singular
|
||||
# matrices; the import path drifts across versions
|
||||
# (sympy.matrices.common in 1.13, sympy.matrices.exceptions
|
||||
# in 1.14). Catch broadly + re-raise as PiStarError so
|
||||
# we don't pin the import path.
|
||||
raise PiStarError(
|
||||
f"linear-algebra@v1 inverse failed: {exc}"
|
||||
) from exc
|
||||
body = (
|
||||
f"inverse;rows={Inv.rows};cols={Inv.cols}:"
|
||||
f"{_format_matrix(Inv)}"
|
||||
)
|
||||
return body.encode("utf-8")
|
||||
|
||||
if op == "eigenvalues":
|
||||
if M.rows != M.cols:
|
||||
raise PiStarError(
|
||||
f"linear-algebra@v1 eigenvalues requires square matrix; "
|
||||
f"got {M.rows}x{M.cols}"
|
||||
)
|
||||
evs = M.eigenvals() # dict {value: multiplicity}
|
||||
# Sort by srepr of the value for determinism.
|
||||
ordered = sorted(evs.items(), key=lambda kv: sp.srepr(kv[0]))
|
||||
parts = [f"{_format_cell(v)}x{m}" for v, m in ordered]
|
||||
return f"eigenvalues:{'|'.join(parts)}".encode("utf-8")
|
||||
|
||||
raise PiStarError( # pragma: no cover — _VALID_OPS exhausts
|
||||
f"linear-algebra@v1 unknown op {op!r}"
|
||||
)
|
||||
|
||||
|
||||
if sp is not None:
|
||||
register(LinearAlgebraV1())
|
||||
|
|
@ -1,17 +1,99 @@
|
|||
"""``tabular-pinned@v1`` π* (stub).
|
||||
"""``tabular-pinned@v1`` π* — declared-schema tabular canonicalizer.
|
||||
|
||||
Domain: ``tabular``. Planned semantics: declared schema (column order
|
||||
+ types) + canonical row encoding (sorted by primary key, type-pinned
|
||||
cells, normalized text).
|
||||
Domain: ``tabular``. The last reserved-stub π* graduates: structured
|
||||
2D data (rows × columns) gets first-class equivalence-class identity.
|
||||
|
||||
This closes the π* registry chapter — every modality the substrate
|
||||
paper reserved (text, claim_lattice, code, arithmetic, logic,
|
||||
time-series, tabular, plus the math-substrate extras
|
||||
algebra-symbolic, calculus-derivative) is now real.
|
||||
|
||||
Input format
|
||||
------------
|
||||
UTF-8 JSON object::
|
||||
|
||||
{
|
||||
"schema": [
|
||||
{"name": "<col_name>", "type": "str|int|rational|bool"},
|
||||
...
|
||||
],
|
||||
"key_columns": ["<col_name>", ...],
|
||||
"rows": [
|
||||
[<cell>, <cell>, ...],
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
- ``schema`` declares column order + per-cell type. Types fold cell
|
||||
values: ``int`` and ``rational`` go through SQD §14.1 rational
|
||||
arithmetic (1, 1.0, "1.0" all collapse to ``1/1``). ``str`` keeps
|
||||
the value verbatim; ``bool`` normalizes to ``true``/``false``.
|
||||
- ``key_columns`` names the primary-key columns. Rows are sorted by
|
||||
the (key_column_1, key_column_2, ...) tuple before serialization;
|
||||
ties within key are stable.
|
||||
- Empty ``rows`` is valid — canonicalizes to an empty body.
|
||||
|
||||
Canonical output
|
||||
----------------
|
||||
UTF-8 text of shape::
|
||||
|
||||
schema=col1:type1,col2:type2,...
|
||||
key=col1,col2,...
|
||||
n=<row_count>
|
||||
rows:
|
||||
cell11|cell12|...
|
||||
cell21|cell22|...
|
||||
...
|
||||
|
||||
Type codes are the schema's declared types (``str`` / ``int`` /
|
||||
``rational`` / ``bool``). Cells are joined with ``|``; rows are
|
||||
joined with ``\n``. Header always present even on empty tables.
|
||||
|
||||
Equivalence classes preserved
|
||||
-----------------------------
|
||||
- Row order: rows are sorted by ``key_columns``; differently-ordered
|
||||
inputs collapse.
|
||||
- Numeric formatting: ``1`` ≡ ``1.0`` ≡ ``"1.0"`` for ``int`` / ``rational``
|
||||
columns (same rational identity through arithmetic@v1).
|
||||
- JSON whitespace / formatting: same logical content, same canonical.
|
||||
- Boolean spelling: ``true``/``True``/``"true"`` all normalize for
|
||||
``bool`` columns.
|
||||
|
||||
Equivalence classes kept distinct
|
||||
---------------------------------
|
||||
- Schema (column order, types, key_columns) is part of identity.
|
||||
Two tables with different declared schema → different canonical.
|
||||
- Different cell values → different canonical.
|
||||
- Different ``key_columns`` → different canonical (changes the sort).
|
||||
- Header case: pinned exact. ``Name`` ≠ ``name``. PostgreSQL +
|
||||
Excel both care about case; defaulting to lowercase-fold would
|
||||
break operator expectations.
|
||||
|
||||
Round-trip property
|
||||
-------------------
|
||||
Projective. The canonical text form is not valid JSON input —
|
||||
re-canonicalizing the canonical bytes raises PiStarError.
|
||||
|
||||
Versioning
|
||||
----------
|
||||
``tabular-pinned@v1`` pins this serialization, sort policy, and
|
||||
type-folding rules. Any change requires a new version.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from fractions import Fraction
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from arborist.pi_star.protocol import PiStarError
|
||||
from arborist.pi_star.registry import register
|
||||
|
||||
|
||||
_VALID_TYPES = ("str", "int", "rational", "bool")
|
||||
|
||||
|
||||
@dataclass
|
||||
class TabularPinnedV1:
|
||||
name: str = "tabular-pinned"
|
||||
|
|
@ -19,9 +101,224 @@ class TabularPinnedV1:
|
|||
domain: str = "tabular"
|
||||
|
||||
def canonicalize(self, raw: bytes) -> bytes:
|
||||
raise NotImplementedError(
|
||||
"tabular-pinned@v1 is a stub; implementation ticket pending."
|
||||
if not isinstance(raw, (bytes, bytearray)):
|
||||
raise PiStarError(
|
||||
"tabular-pinned@v1 expects bytes; got "
|
||||
f"{type(raw).__name__}"
|
||||
)
|
||||
try:
|
||||
text = raw.decode("utf-8", errors="surrogatepass")
|
||||
except UnicodeDecodeError as exc: # pragma: no cover
|
||||
raise PiStarError(f"input not valid UTF-8: {exc}") from exc
|
||||
|
||||
try:
|
||||
obj = json.loads(text)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise PiStarError(
|
||||
f"tabular-pinned@v1 input is not valid JSON: {exc}"
|
||||
) from exc
|
||||
|
||||
if not isinstance(obj, dict):
|
||||
raise PiStarError(
|
||||
"tabular-pinned@v1 input must be a JSON object"
|
||||
)
|
||||
for required in ("schema", "key_columns", "rows"):
|
||||
if required not in obj:
|
||||
raise PiStarError(
|
||||
f"tabular-pinned@v1 missing required field "
|
||||
f"{required!r}"
|
||||
)
|
||||
|
||||
schema = obj["schema"]
|
||||
key_columns = obj["key_columns"]
|
||||
rows = obj["rows"]
|
||||
|
||||
if not isinstance(schema, list) or not schema:
|
||||
raise PiStarError(
|
||||
"tabular-pinned@v1 schema must be a non-empty JSON array"
|
||||
)
|
||||
if not isinstance(key_columns, list):
|
||||
raise PiStarError(
|
||||
"tabular-pinned@v1 key_columns must be a JSON array"
|
||||
)
|
||||
if not isinstance(rows, list):
|
||||
raise PiStarError(
|
||||
"tabular-pinned@v1 rows must be a JSON array"
|
||||
)
|
||||
|
||||
# Validate schema entries.
|
||||
col_names: list[str] = []
|
||||
col_types: list[str] = []
|
||||
for i, entry in enumerate(schema):
|
||||
if not isinstance(entry, dict):
|
||||
raise PiStarError(
|
||||
f"schema[{i}] must be a JSON object with name + type"
|
||||
)
|
||||
name = entry.get("name")
|
||||
ctype = entry.get("type")
|
||||
if not isinstance(name, str) or not name:
|
||||
raise PiStarError(
|
||||
f"schema[{i}].name must be a non-empty string"
|
||||
)
|
||||
if ctype not in _VALID_TYPES:
|
||||
raise PiStarError(
|
||||
f"schema[{i}].type must be one of "
|
||||
f"{_VALID_TYPES}; got {ctype!r}"
|
||||
)
|
||||
col_names.append(name)
|
||||
col_types.append(ctype)
|
||||
|
||||
# Validate key_columns reference real schema columns.
|
||||
col_index = {n: i for i, n in enumerate(col_names)}
|
||||
if len(set(col_names)) != len(col_names):
|
||||
raise PiStarError(
|
||||
"tabular-pinned@v1 schema column names must be unique"
|
||||
)
|
||||
key_indices: list[int] = []
|
||||
for k in key_columns:
|
||||
if not isinstance(k, str):
|
||||
raise PiStarError("key_columns entries must be strings")
|
||||
if k not in col_index:
|
||||
raise PiStarError(
|
||||
f"key_column {k!r} not in schema columns "
|
||||
f"{col_names!r}"
|
||||
)
|
||||
key_indices.append(col_index[k])
|
||||
|
||||
# Validate + fold each row.
|
||||
n_cols = len(col_names)
|
||||
folded: list[list[str]] = []
|
||||
for ri, row in enumerate(rows):
|
||||
if not isinstance(row, list):
|
||||
raise PiStarError(
|
||||
f"rows[{ri}] must be a JSON array"
|
||||
)
|
||||
if len(row) != n_cols:
|
||||
raise PiStarError(
|
||||
f"rows[{ri}] has {len(row)} cells; schema declares "
|
||||
f"{n_cols}"
|
||||
)
|
||||
folded_row: list[str] = []
|
||||
for ci, cell in enumerate(row):
|
||||
folded_row.append(_fold_cell(cell, col_types[ci], ri, ci))
|
||||
folded.append(folded_row)
|
||||
|
||||
# Sort by key tuple (stable). Empty key_columns → preserve
|
||||
# input order (some tables have no PK; the operator declares
|
||||
# that explicitly).
|
||||
if key_indices:
|
||||
folded.sort(
|
||||
key=lambda r: tuple(r[i] for i in key_indices)
|
||||
)
|
||||
|
||||
# Serialize.
|
||||
schema_str = ",".join(
|
||||
f"{n}:{t}" for n, t in zip(col_names, col_types)
|
||||
)
|
||||
key_str = ",".join(key_columns)
|
||||
body = "\n".join("|".join(r) for r in folded)
|
||||
n = len(folded)
|
||||
out = (
|
||||
f"schema={schema_str}\n"
|
||||
f"key={key_str}\n"
|
||||
f"n={n}\n"
|
||||
f"rows:\n"
|
||||
f"{body}"
|
||||
)
|
||||
# Trailing newline only when rows non-empty so the empty-table
|
||||
# canonical is exactly: "schema=...\nkey=...\nn=0\nrows:".
|
||||
return out.encode("utf-8")
|
||||
|
||||
|
||||
def _fold_cell(cell, col_type: str, ri: int, ci: int) -> str:
|
||||
"""Fold one cell value into its canonical string form per type."""
|
||||
if col_type == "str":
|
||||
if not isinstance(cell, str):
|
||||
raise PiStarError(
|
||||
f"rows[{ri}][{ci}] expected str cell; got "
|
||||
f"{type(cell).__name__}"
|
||||
)
|
||||
return cell
|
||||
|
||||
if col_type == "bool":
|
||||
if isinstance(cell, bool):
|
||||
return "true" if cell else "false"
|
||||
if isinstance(cell, str):
|
||||
normalized = cell.strip().lower()
|
||||
if normalized in ("true", "1", "yes"):
|
||||
return "true"
|
||||
if normalized in ("false", "0", "no"):
|
||||
return "false"
|
||||
raise PiStarError(
|
||||
f"rows[{ri}][{ci}] expected bool-shape cell; got {cell!r}"
|
||||
)
|
||||
|
||||
if col_type == "int":
|
||||
# Accept int, float that's integer-valued, or string that parses.
|
||||
if isinstance(cell, bool):
|
||||
# bool is a subtype of int in Python; reject to avoid
|
||||
# silently treating True as 1.
|
||||
raise PiStarError(
|
||||
f"rows[{ri}][{ci}] expected int cell; got bool"
|
||||
)
|
||||
if isinstance(cell, int):
|
||||
return str(cell)
|
||||
if isinstance(cell, float):
|
||||
if not cell.is_integer():
|
||||
raise PiStarError(
|
||||
f"rows[{ri}][{ci}] expected int cell; got "
|
||||
f"non-integer float {cell!r}"
|
||||
)
|
||||
return str(int(cell))
|
||||
if isinstance(cell, str):
|
||||
try:
|
||||
f = Fraction(Decimal(cell.strip()))
|
||||
except (ValueError, InvalidOperation) as exc:
|
||||
raise PiStarError(
|
||||
f"rows[{ri}][{ci}] cannot parse int from {cell!r}"
|
||||
) from exc
|
||||
if f.denominator != 1:
|
||||
raise PiStarError(
|
||||
f"rows[{ri}][{ci}] expected int cell; got "
|
||||
f"non-integer rational {cell!r}"
|
||||
)
|
||||
return str(f.numerator)
|
||||
raise PiStarError(
|
||||
f"rows[{ri}][{ci}] cannot fold {cell!r} as int"
|
||||
)
|
||||
|
||||
if col_type == "rational":
|
||||
# Fold to num/den lowest-terms form, sharing arithmetic@v1's
|
||||
# contract (Decimal(str(...)) for floats avoids drift).
|
||||
if isinstance(cell, bool):
|
||||
raise PiStarError(
|
||||
f"rows[{ri}][{ci}] expected rational cell; got bool"
|
||||
)
|
||||
try:
|
||||
if isinstance(cell, int):
|
||||
f = Fraction(cell, 1)
|
||||
elif isinstance(cell, float):
|
||||
f = Fraction(Decimal(str(cell)))
|
||||
elif isinstance(cell, str):
|
||||
stripped = cell.strip()
|
||||
if "/" in stripped:
|
||||
f = Fraction(stripped)
|
||||
else:
|
||||
f = Fraction(Decimal(stripped))
|
||||
else:
|
||||
raise PiStarError(
|
||||
f"rows[{ri}][{ci}] cannot fold {cell!r} as rational"
|
||||
)
|
||||
except (ValueError, InvalidOperation, ZeroDivisionError) as exc:
|
||||
raise PiStarError(
|
||||
f"rows[{ri}][{ci}] cannot parse rational from {cell!r}: "
|
||||
f"{exc}"
|
||||
) from exc
|
||||
return f"{f.numerator}/{f.denominator}"
|
||||
|
||||
raise PiStarError( # pragma: no cover — _VALID_TYPES exhausts above
|
||||
f"rows[{ri}][{ci}] unknown type {col_type!r}"
|
||||
)
|
||||
|
||||
|
||||
register(TabularPinnedV1())
|
||||
|
|
|
|||
|
|
@ -122,6 +122,13 @@ PHASE_1_CARRIERS = frozenset({
|
|||
# Calculus-derivative shares this carrier (its output is itself an
|
||||
# algebraic expression).
|
||||
"symbolic_algebra",
|
||||
# Calculus / linear-algebra / function-sampled / tabular — ticket
|
||||
# #000030 Phases 4-7 + last-stub graduation. Each closes one more
|
||||
# modality the substrate paper reserved.
|
||||
"calculus",
|
||||
"linear-algebra",
|
||||
"function-sampled",
|
||||
"tabular",
|
||||
})
|
||||
|
||||
|
||||
|
|
|
|||
13
bench/fixtures/5s/semantics-calculus-limit-v1.jsonl
Normal file
13
bench/fixtures/5s/semantics-calculus-limit-v1.jsonl
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{"_meta": {"battery": "5s", "sub_battery": "semantics", "version": "v1", "task_count": 12, "notes": "calculus-limit@v1 semantics — same limit collapses regardless of f spelling; oo-direction-distinct."}}
|
||||
{"id": "5s-sem-lim-001", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-limit@v1", "input_a": "{\"f\": \"sin(x)/x\", \"x\": \"x\", \"point\": \"0\"}", "input_b": "{\"f\": \"sin(x)*1/x\", \"x\": \"x\", \"point\": \"0\"}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-lim-002", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-limit@v1", "input_a": "{\"f\": \"2*sin(x)/x\", \"x\": \"x\", \"point\": \"0\"}", "input_b": "{\"f\": \"2*sin(x)/x\", \"x\": \"x\", \"point\": \"0\"}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-lim-003", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-limit@v1", "input_a": "{\"f\": \"x\", \"x\": \"x\", \"point\": \"0\"}", "input_b": "{\"f\": \"x\", \"x\": \"x\", \"point\": \"1\"}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-lim-004", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-limit@v1", "input_a": "{\"f\": \"sin(x)/x\", \"x\": \"x\", \"point\": \"0\"}", "input_b": "{\"f\": \"cos(x)/x\", \"x\": \"x\", \"point\": \"0\"}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-lim-005", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-limit@v1", "input_a": "{\"f\": \"1/x\", \"x\": \"x\", \"point\": \"0\", \"dir\": \"+\"}", "input_b": "{\"f\": \"1/x\", \"x\": \"x\", \"point\": \"0\", \"dir\": \"+\"}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-lim-006", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-limit@v1", "input_a": "{\"f\": \"1/x\", \"x\": \"x\", \"point\": \"0\", \"dir\": \"+\"}", "input_b": "{\"f\": \"1/x\", \"x\": \"x\", \"point\": \"0\", \"dir\": \"-\"}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-lim-007", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-limit@v1", "input_a": "{\"f\": \"x**2\", \"x\": \"x\", \"point\": \"3\"}", "input_b": "{\"f\": \"x*x\", \"x\": \"x\", \"point\": \"3\"}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-lim-008", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-limit@v1", "input_a": "{\"f\": \"x\", \"x\": \"x\", \"point\": \"oo\"}", "input_b": "{\"f\": \"x\", \"x\": \"x\", \"point\": \"-oo\"}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-lim-009", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-limit@v1", "input_a": "{\"f\": \"0\", \"x\": \"x\", \"point\": \"0\"}", "input_b": "{\"f\": \"0\", \"x\": \"x\", \"point\": \"5\"}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-lim-010", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-limit@v1", "input_a": "{\"f\": \"y**2\", \"x\": \"y\", \"point\": \"2\"}", "input_b": "{\"f\": \"x**2\", \"x\": \"x\", \"point\": \"2\"}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-lim-011", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-limit@v1", "input_a": "{\"f\": \"sin(x)/x\", \"x\": \"x\", \"point\": \"0\"}", "input_b": "{\"f\": \"1/x\", \"x\": \"x\", \"point\": \"oo\"}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-lim-012", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-limit@v1", "input_a": "{\"f\": \"log(x)\", \"x\": \"x\", \"point\": \"1\"}", "input_b": "{\"f\": \"0\", \"x\": \"x\", \"point\": \"5\"}", "expected_equivalent": true}
|
||||
13
bench/fixtures/5s/semantics-calculus-series-v1.jsonl
Normal file
13
bench/fixtures/5s/semantics-calculus-series-v1.jsonl
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{"_meta": {"battery": "5s", "sub_battery": "semantics", "version": "v1", "task_count": 12, "notes": "calculus-series@v1 semantics — truncation identity; algebraic fold; n/x0/f distinct."}}
|
||||
{"id": "5s-sem-ser-001", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-series@v1", "input_a": "{\"f\": \"sin(x)\", \"x\": \"x\", \"x0\": \"0\", \"n\": 4}", "input_b": "{\"f\": \"sin(x)\", \"x\": \"x\", \"x0\": \"0\", \"n\": 4}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-ser-002", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-series@v1", "input_a": "{\"f\": \"sin(x)\", \"x\": \"x\", \"x0\": \"0\", \"n\": 4}", "input_b": "{\"f\": \"sin(x)\", \"x\": \"x\", \"x0\": \"0\", \"n\": 6}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-ser-003", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-series@v1", "input_a": "{\"f\": \"sin(x)\", \"x\": \"x\", \"x0\": \"0\", \"n\": 4}", "input_b": "{\"f\": \"cos(x)\", \"x\": \"x\", \"x0\": \"0\", \"n\": 4}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-ser-004", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-series@v1", "input_a": "{\"f\": \"sin(x)\", \"x\": \"x\", \"x0\": \"0\", \"n\": 4}", "input_b": "{\"f\": \"sin(x)\", \"x\": \"x\", \"x0\": \"pi\", \"n\": 4}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-ser-005", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-series@v1", "input_a": "{\"f\": \"2*x\", \"x\": \"x\", \"x0\": \"0\", \"n\": 3}", "input_b": "{\"f\": \"x+x\", \"x\": \"x\", \"x0\": \"0\", \"n\": 3}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-ser-006", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-series@v1", "input_a": "{\"f\": \"x**3\", \"x\": \"x\", \"x0\": \"0\", \"n\": 5}", "input_b": "{\"f\": \"x**3\", \"x\": \"x\", \"x0\": \"0\", \"n\": 5}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-ser-007", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-series@v1", "input_a": "{\"f\": \"x**3\", \"x\": \"x\", \"x0\": \"0\", \"n\": 3}", "input_b": "{\"f\": \"x**3\", \"x\": \"x\", \"x0\": \"0\", \"n\": 5}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-ser-008", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-series@v1", "input_a": "{\"f\": \"sin(x)\", \"x\": \"x\", \"x0\": \"0\", \"n\": 4}", "input_b": "{\"f\": \"sin(y)\", \"x\": \"y\", \"x0\": \"0\", \"n\": 4}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-ser-009", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-series@v1", "input_a": "{\"f\": \"exp(x)\", \"x\": \"x\", \"x0\": \"0\", \"n\": 3}", "input_b": "{\"f\": \"exp(2*x)\", \"x\": \"x\", \"x0\": \"0\", \"n\": 3}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-ser-010", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-series@v1", "input_a": "{\"f\": \"5\", \"x\": \"x\", \"x0\": \"0\", \"n\": 3}", "input_b": "{\"f\": \"5\", \"x\": \"x\", \"x0\": \"0\", \"n\": 3}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-ser-011", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-series@v1", "input_a": "{\"f\": \"0\", \"x\": \"x\", \"x0\": \"0\", \"n\": 3}", "input_b": "{\"f\": \"0\", \"x\": \"x\", \"x0\": \"5\", \"n\": 7}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-ser-012", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-series@v1", "input_a": "{\"f\": \"1/(1-x)\", \"x\": \"x\", \"x0\": \"0\", \"n\": 3}", "input_b": "{\"f\": \"1/(1-x)\", \"x\": \"x\", \"x0\": \"1/2\", \"n\": 3}", "expected_equivalent": false}
|
||||
13
bench/fixtures/5s/semantics-function-sampled-v1.jsonl
Normal file
13
bench/fixtures/5s/semantics-function-sampled-v1.jsonl
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{"_meta": {"battery": "5s", "sub_battery": "semantics", "version": "v1", "task_count": 12, "notes": "function-sampled@v1 semantics — same numeric profile collapses; grid/dv/f distinct."}}
|
||||
{"id": "5s-sem-fs-001", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "function-sampled", "domain": "json_object", "pi_star_ref": "function-sampled@v1", "input_a": "{\"f\": \"sin(x)\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 1, \"n_samples\": 5, \"dv\": 0.01}", "input_b": "{\"f\": \"2*sin(x)/2\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 1, \"n_samples\": 5, \"dv\": 0.01}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-fs-002", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "function-sampled", "domain": "json_object", "pi_star_ref": "function-sampled@v1", "input_a": "{\"f\": \"x\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 4, \"n_samples\": 5, \"dv\": 1}", "input_b": "{\"f\": \"x*1\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 4, \"n_samples\": 5, \"dv\": 1}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-fs-003", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "function-sampled", "domain": "json_object", "pi_star_ref": "function-sampled@v1", "input_a": "{\"f\": \"x\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 4, \"n_samples\": 5, \"dv\": 1}", "input_b": "{\"f\": \"x\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 5, \"n_samples\": 5, \"dv\": 1}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-fs-004", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "function-sampled", "domain": "json_object", "pi_star_ref": "function-sampled@v1", "input_a": "{\"f\": \"x\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 4, \"n_samples\": 5, \"dv\": 1}", "input_b": "{\"f\": \"x\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 4, \"n_samples\": 9, \"dv\": 1}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-fs-005", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "function-sampled", "domain": "json_object", "pi_star_ref": "function-sampled@v1", "input_a": "{\"f\": \"x\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 4, \"n_samples\": 5, \"dv\": 1}", "input_b": "{\"f\": \"x\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 4, \"n_samples\": 5, \"dv\": 0.5}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-fs-006", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "function-sampled", "domain": "json_object", "pi_star_ref": "function-sampled@v1", "input_a": "{\"f\": \"x**2\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 4, \"n_samples\": 5, \"dv\": 1}", "input_b": "{\"f\": \"x*x\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 4, \"n_samples\": 5, \"dv\": 1}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-fs-007", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "function-sampled", "domain": "json_object", "pi_star_ref": "function-sampled@v1", "input_a": "{\"f\": \"x\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 4, \"n_samples\": 5, \"dv\": 1}", "input_b": "{\"f\": \"x+1\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 4, \"n_samples\": 5, \"dv\": 1}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-fs-008", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "function-sampled", "domain": "json_object", "pi_star_ref": "function-sampled@v1", "input_a": "{\"f\": \"y\", \"x\": \"y\", \"x_min\": 0, \"x_max\": 4, \"n_samples\": 5, \"dv\": 1}", "input_b": "{\"f\": \"x\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 4, \"n_samples\": 5, \"dv\": 1}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-fs-009", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "function-sampled", "domain": "json_object", "pi_star_ref": "function-sampled@v1", "input_a": "{\"f\": \"x\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 4, \"n_samples\": 5, \"dv\": 1}", "input_b": "{\"f\": \"x\", \"x\": \"x\", \"x_min\": -4, \"x_max\": 0, \"n_samples\": 5, \"dv\": 1}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-fs-010", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "function-sampled", "domain": "json_object", "pi_star_ref": "function-sampled@v1", "input_a": "{\"f\": \"0\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 4, \"n_samples\": 5, \"dv\": 1}", "input_b": "{\"f\": \"0\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 4, \"n_samples\": 5, \"dv\": 1}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-fs-011", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "function-sampled", "domain": "json_object", "pi_star_ref": "function-sampled@v1", "input_a": "{\"f\": \"0\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 4, \"n_samples\": 5, \"dv\": 1}", "input_b": "{\"f\": \"1\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 4, \"n_samples\": 5, \"dv\": 1}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-fs-012", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "function-sampled", "domain": "json_object", "pi_star_ref": "function-sampled@v1", "input_a": "{\"f\": \"sin(x)+sin(x)\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 1, \"n_samples\": 11, \"dv\": 0.01}", "input_b": "{\"f\": \"2*sin(x)\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 1, \"n_samples\": 11, \"dv\": 0.01}", "expected_equivalent": true}
|
||||
13
bench/fixtures/5s/semantics-linear-algebra-v1.jsonl
Normal file
13
bench/fixtures/5s/semantics-linear-algebra-v1.jsonl
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{"_meta": {"battery": "5s", "sub_battery": "semantics", "version": "v1", "task_count": 12, "notes": "linear-algebra@v1 semantics — int/float/string cell fold; eigenvalue ordering; op-distinct."}}
|
||||
{"id": "5s-sem-la-001", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "linear-algebra", "domain": "json_object", "pi_star_ref": "linear-algebra@v1", "input_a": "{\"op\": \"det\", \"matrix\": [[1, 2], [3, 4]]}", "input_b": "{\"op\": \"det\", \"matrix\": [[1.0, 2.0], [3.0, 4.0]]}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-la-002", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "linear-algebra", "domain": "json_object", "pi_star_ref": "linear-algebra@v1", "input_a": "{\"op\": \"det\", \"matrix\": [[1, 2], [3, 4]]}", "input_b": "{\"op\": \"det\", \"matrix\": [[\"1\", \"2\"], [\"3\", \"4\"]]}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-la-003", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "linear-algebra", "domain": "json_object", "pi_star_ref": "linear-algebra@v1", "input_a": "{\"op\": \"det\", \"matrix\": [[2, 0], [0, 2]]}", "input_b": "{\"op\": \"rref\", \"matrix\": [[2, 0], [0, 2]]}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-la-004", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "linear-algebra", "domain": "json_object", "pi_star_ref": "linear-algebra@v1", "input_a": "{\"op\": \"det\", \"matrix\": [[1, 2], [3, 4]]}", "input_b": "{\"op\": \"det\", \"matrix\": [[1, 2], [3, 5]]}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-la-005", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "linear-algebra", "domain": "json_object", "pi_star_ref": "linear-algebra@v1", "input_a": "{\"op\": \"det\", \"matrix\": [[1, 0], [0, 1]]}", "input_b": "{\"op\": \"det\", \"matrix\": [[\"1\", \"0\"], [\"0\", \"1.0\"]]}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-la-006", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "linear-algebra", "domain": "json_object", "pi_star_ref": "linear-algebra@v1", "input_a": "{\"op\": \"det\", \"matrix\": [[1, 0], [0, 1]]}", "input_b": "{\"op\": \"det\", \"matrix\": [[1, 0, 0], [0, 1, 0], [0, 0, 1]]}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-la-007", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "linear-algebra", "domain": "json_object", "pi_star_ref": "linear-algebra@v1", "input_a": "{\"op\": \"rref\", \"matrix\": [[2, 4], [1, 2]]}", "input_b": "{\"op\": \"rref\", \"matrix\": [[1, 2], [2, 4]]}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-la-008", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "linear-algebra", "domain": "json_object", "pi_star_ref": "linear-algebra@v1", "input_a": "{\"op\": \"eigenvalues\", \"matrix\": [[2, 0], [0, 3]]}", "input_b": "{\"op\": \"eigenvalues\", \"matrix\": [[3, 0], [0, 2]]}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-la-009", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "linear-algebra", "domain": "json_object", "pi_star_ref": "linear-algebra@v1", "input_a": "{\"op\": \"eigenvalues\", \"matrix\": [[1, 0], [0, 2]]}", "input_b": "{\"op\": \"eigenvalues\", \"matrix\": [[1, 0], [0, 3]]}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-la-010", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "linear-algebra", "domain": "json_object", "pi_star_ref": "linear-algebra@v1", "input_a": "{\"op\": \"inverse\", \"matrix\": [[1, 0], [0, 1]]}", "input_b": "{\"op\": \"inverse\", \"matrix\": [[1, 0], [0, 1]]}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-la-011", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "linear-algebra", "domain": "json_object", "pi_star_ref": "linear-algebra@v1", "input_a": "{\"op\": \"inverse\", \"matrix\": [[2, 0], [0, 2]]}", "input_b": "{\"op\": \"inverse\", \"matrix\": [[1, 0], [0, 1]]}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-la-012", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "linear-algebra", "domain": "json_object", "pi_star_ref": "linear-algebra@v1", "input_a": "{\"op\": \"det\", \"matrix\": [[\"1/2\", 0], [0, \"1/2\"]]}", "input_b": "{\"op\": \"det\", \"matrix\": [[0.5, 0], [0, 0.5]]}", "expected_equivalent": true}
|
||||
13
bench/fixtures/5s/semantics-tabular-v1.jsonl
Normal file
13
bench/fixtures/5s/semantics-tabular-v1.jsonl
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{"_meta": {"battery": "5s", "sub_battery": "semantics", "version": "v1", "task_count": 12, "notes": "tabular-pinned@v1 semantics — row-order/cell-fold collapses; schema/key/case distinct."}}
|
||||
{"id": "5s-sem-tab-001", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "tabular", "domain": "json_object", "pi_star_ref": "tabular-pinned@v1", "input_a": "{\"schema\": [{\"name\": \"k\", \"type\": \"int\"}], \"key_columns\": [\"k\"], \"rows\": [[2], [1]]}", "input_b": "{\"schema\": [{\"name\": \"k\", \"type\": \"int\"}], \"key_columns\": [\"k\"], \"rows\": [[1], [2]]}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-tab-002", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "tabular", "domain": "json_object", "pi_star_ref": "tabular-pinned@v1", "input_a": "{\"schema\": [{\"name\": \"v\", \"type\": \"int\"}], \"key_columns\": [], \"rows\": [[1]]}", "input_b": "{\"schema\": [{\"name\": \"v\", \"type\": \"int\"}], \"key_columns\": [], \"rows\": [[1.0]]}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-tab-003", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "tabular", "domain": "json_object", "pi_star_ref": "tabular-pinned@v1", "input_a": "{\"schema\": [{\"name\": \"v\", \"type\": \"int\"}], \"key_columns\": [], \"rows\": [[1]]}", "input_b": "{\"schema\": [{\"name\": \"v\", \"type\": \"int\"}], \"key_columns\": [], \"rows\": [[\"1.0\"]]}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-tab-004", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "tabular", "domain": "json_object", "pi_star_ref": "tabular-pinned@v1", "input_a": "{\"schema\": [{\"name\": \"r\", \"type\": \"rational\"}], \"key_columns\": [], \"rows\": [[0.5]]}", "input_b": "{\"schema\": [{\"name\": \"r\", \"type\": \"rational\"}], \"key_columns\": [], \"rows\": [[\"1/2\"]]}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-tab-005", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "tabular", "domain": "json_object", "pi_star_ref": "tabular-pinned@v1", "input_a": "{\"schema\": [{\"name\": \"f\", \"type\": \"bool\"}], \"key_columns\": [], \"rows\": [[true]]}", "input_b": "{\"schema\": [{\"name\": \"f\", \"type\": \"bool\"}], \"key_columns\": [], \"rows\": [[\"true\"]]}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-tab-006", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "tabular", "domain": "json_object", "pi_star_ref": "tabular-pinned@v1", "input_a": "{\"schema\": [{\"name\": \"Name\", \"type\": \"str\"}], \"key_columns\": [], \"rows\": [[\"x\"]]}", "input_b": "{\"schema\": [{\"name\": \"name\", \"type\": \"str\"}], \"key_columns\": [], \"rows\": [[\"x\"]]}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-tab-007", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "tabular", "domain": "json_object", "pi_star_ref": "tabular-pinned@v1", "input_a": "{\"schema\": [{\"name\": \"v\", \"type\": \"int\"}], \"key_columns\": [], \"rows\": [[1]]}", "input_b": "{\"schema\": [{\"name\": \"v\", \"type\": \"int\"}], \"key_columns\": [], \"rows\": [[2]]}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-tab-008", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "tabular", "domain": "json_object", "pi_star_ref": "tabular-pinned@v1", "input_a": "{\"schema\": [{\"name\": \"a\", \"type\": \"int\"}], \"key_columns\": [], \"rows\": [[1]]}", "input_b": "{\"schema\": [{\"name\": \"b\", \"type\": \"int\"}], \"key_columns\": [], \"rows\": [[1]]}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-tab-009", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "tabular", "domain": "json_object", "pi_star_ref": "tabular-pinned@v1", "input_a": "{\"schema\": [{\"name\": \"a\", \"type\": \"int\"}, {\"name\": \"b\", \"type\": \"int\"}], \"key_columns\": [\"a\"], \"rows\": [[2, 1], [1, 2]]}", "input_b": "{\"schema\": [{\"name\": \"a\", \"type\": \"int\"}, {\"name\": \"b\", \"type\": \"int\"}], \"key_columns\": [\"b\"], \"rows\": [[2, 1], [1, 2]]}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-tab-010", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "tabular", "domain": "json_object", "pi_star_ref": "tabular-pinned@v1", "input_a": "{\"schema\": [{\"name\": \"x\", \"type\": \"int\"}], \"key_columns\": [], \"rows\": []}", "input_b": "{\"schema\": [{\"name\": \"x\", \"type\": \"int\"}], \"key_columns\": [], \"rows\": []}", "expected_equivalent": true}
|
||||
{"id": "5s-sem-tab-011", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "tabular", "domain": "json_object", "pi_star_ref": "tabular-pinned@v1", "input_a": "{\"schema\": [{\"name\": \"x\", \"type\": \"int\"}], \"key_columns\": [\"x\"], \"rows\": [[1]]}", "input_b": "{\"schema\": [{\"name\": \"x\", \"type\": \"int\"}], \"key_columns\": [\"x\"], \"rows\": [[1], [2]]}", "expected_equivalent": false}
|
||||
{"id": "5s-sem-tab-012", "battery": "5s", "sub_battery": "semantics", "version": "v1", "carrier": "tabular", "domain": "json_object", "pi_star_ref": "tabular-pinned@v1", "input_a": "{\"schema\": [{\"name\": \"r\", \"type\": \"rational\"}], \"key_columns\": [], \"rows\": [[\"1/2\"]]}", "input_b": "{\"schema\": [{\"name\": \"r\", \"type\": \"rational\"}], \"key_columns\": [], \"rows\": [[\"2/4\"]]}", "expected_equivalent": true}
|
||||
11
bench/fixtures/5s/syntax-calculus-limit-v1.jsonl
Normal file
11
bench/fixtures/5s/syntax-calculus-limit-v1.jsonl
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{"_meta": {"battery": "5s", "sub_battery": "syntax", "version": "v1", "task_count": 10, "notes": "calculus-limit@v1 syntax — sin(x)/x at 0; 1/x at 0±; e at oo."}}
|
||||
{"id": "5s-syn-lim-001", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-limit@v1", "input": "{\"f\": \"sin(x)/x\", \"x\": \"x\", \"point\": \"0\"}"}
|
||||
{"id": "5s-syn-lim-002", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-limit@v1", "input": "{\"f\": \"x**2 + 1\", \"x\": \"x\", \"point\": \"3\"}"}
|
||||
{"id": "5s-syn-lim-003", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-limit@v1", "input": "{\"f\": \"1/x\", \"x\": \"x\", \"point\": \"oo\"}"}
|
||||
{"id": "5s-syn-lim-004", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-limit@v1", "input": "{\"f\": \"1/x\", \"x\": \"x\", \"point\": \"0\", \"dir\": \"+\"}"}
|
||||
{"id": "5s-syn-lim-005", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-limit@v1", "input": "{\"f\": \"1/x\", \"x\": \"x\", \"point\": \"0\", \"dir\": \"-\"}"}
|
||||
{"id": "5s-syn-lim-006", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-limit@v1", "input": "{\"f\": \"exp(-x)\", \"x\": \"x\", \"point\": \"oo\"}"}
|
||||
{"id": "5s-syn-lim-007", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-limit@v1", "input": "{\"f\": \"log(x)\", \"x\": \"x\", \"point\": \"1\"}"}
|
||||
{"id": "5s-syn-lim-008", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-limit@v1", "input": "{\"f\": \"(1+1/x)**x\", \"x\": \"x\", \"point\": \"oo\"}"}
|
||||
{"id": "5s-syn-lim-009", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-limit@v1", "input": "{\"f\": \"tan(x)\", \"x\": \"x\", \"point\": \"0\"}"}
|
||||
{"id": "5s-syn-lim-010", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-limit@v1", "input": "{\"f\": \"x*sin(1/x)\", \"x\": \"x\", \"point\": \"0\"}"}
|
||||
11
bench/fixtures/5s/syntax-calculus-series-v1.jsonl
Normal file
11
bench/fixtures/5s/syntax-calculus-series-v1.jsonl
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{"_meta": {"battery": "5s", "sub_battery": "syntax", "version": "v1", "task_count": 10, "notes": "calculus-series@v1 syntax — Maclaurin/Taylor truncation."}}
|
||||
{"id": "5s-syn-ser-001", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-series@v1", "input": "{\"f\": \"sin(x)\", \"x\": \"x\", \"x0\": \"0\", \"n\": 4}"}
|
||||
{"id": "5s-syn-ser-002", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-series@v1", "input": "{\"f\": \"cos(x)\", \"x\": \"x\", \"x0\": \"0\", \"n\": 4}"}
|
||||
{"id": "5s-syn-ser-003", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-series@v1", "input": "{\"f\": \"exp(x)\", \"x\": \"x\", \"x0\": \"0\", \"n\": 3}"}
|
||||
{"id": "5s-syn-ser-004", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-series@v1", "input": "{\"f\": \"1/(1-x)\", \"x\": \"x\", \"x0\": \"0\", \"n\": 4}"}
|
||||
{"id": "5s-syn-ser-005", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-series@v1", "input": "{\"f\": \"log(1+x)\", \"x\": \"x\", \"x0\": \"0\", \"n\": 4}"}
|
||||
{"id": "5s-syn-ser-006", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-series@v1", "input": "{\"f\": \"sin(x)\", \"x\": \"x\", \"x0\": \"pi\", \"n\": 3}"}
|
||||
{"id": "5s-syn-ser-007", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-series@v1", "input": "{\"f\": \"x**3 + 2*x\", \"x\": \"x\", \"x0\": \"0\", \"n\": 5}"}
|
||||
{"id": "5s-syn-ser-008", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-series@v1", "input": "{\"f\": \"1/(1+x**2)\", \"x\": \"x\", \"x0\": \"0\", \"n\": 4}"}
|
||||
{"id": "5s-syn-ser-009", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-series@v1", "input": "{\"f\": \"tan(x)\", \"x\": \"x\", \"x0\": \"0\", \"n\": 3}"}
|
||||
{"id": "5s-syn-ser-010", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "calculus", "domain": "json_object", "pi_star_ref": "calculus-series@v1", "input": "{\"f\": \"sqrt(1+x)\", \"x\": \"x\", \"x0\": \"0\", \"n\": 3}"}
|
||||
11
bench/fixtures/5s/syntax-function-sampled-v1.jsonl
Normal file
11
bench/fixtures/5s/syntax-function-sampled-v1.jsonl
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{"_meta": {"battery": "5s", "sub_battery": "syntax", "version": "v1", "task_count": 10, "notes": "function-sampled@v1 syntax — SymPy expr → quantized sample grid."}}
|
||||
{"id": "5s-syn-fs-001", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "function-sampled", "domain": "json_object", "pi_star_ref": "function-sampled@v1", "input": "{\"f\": \"x\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 4, \"n_samples\": 5, \"dv\": 1}"}
|
||||
{"id": "5s-syn-fs-002", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "function-sampled", "domain": "json_object", "pi_star_ref": "function-sampled@v1", "input": "{\"f\": \"x**2\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 4, \"n_samples\": 5, \"dv\": 1}"}
|
||||
{"id": "5s-syn-fs-003", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "function-sampled", "domain": "json_object", "pi_star_ref": "function-sampled@v1", "input": "{\"f\": \"sin(x)\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 1, \"n_samples\": 11, \"dv\": 0.01}"}
|
||||
{"id": "5s-syn-fs-004", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "function-sampled", "domain": "json_object", "pi_star_ref": "function-sampled@v1", "input": "{\"f\": \"2*x + 1\", \"x\": \"x\", \"x_min\": -2, \"x_max\": 2, \"n_samples\": 5, \"dv\": 1}"}
|
||||
{"id": "5s-syn-fs-005", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "function-sampled", "domain": "json_object", "pi_star_ref": "function-sampled@v1", "input": "{\"f\": \"exp(x)\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 1, \"n_samples\": 11, \"dv\": 0.1}"}
|
||||
{"id": "5s-syn-fs-006", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "function-sampled", "domain": "json_object", "pi_star_ref": "function-sampled@v1", "input": "{\"f\": \"cos(x)\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 3.14159, \"n_samples\": 4, \"dv\": 0.1}"}
|
||||
{"id": "5s-syn-fs-007", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "function-sampled", "domain": "json_object", "pi_star_ref": "function-sampled@v1", "input": "{\"f\": \"x**3\", \"x\": \"x\", \"x_min\": -1, \"x_max\": 1, \"n_samples\": 5, \"dv\": 0.1}"}
|
||||
{"id": "5s-syn-fs-008", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "function-sampled", "domain": "json_object", "pi_star_ref": "function-sampled@v1", "input": "{\"f\": \"abs(x)\", \"x\": \"x\", \"x_min\": -1, \"x_max\": 1, \"n_samples\": 5, \"dv\": 0.1}"}
|
||||
{"id": "5s-syn-fs-009", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "function-sampled", "domain": "json_object", "pi_star_ref": "function-sampled@v1", "input": "{\"f\": \"x\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 10, \"n_samples\": 11, \"dv\": 1}"}
|
||||
{"id": "5s-syn-fs-010", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "function-sampled", "domain": "json_object", "pi_star_ref": "function-sampled@v1", "input": "{\"f\": \"sin(x)*cos(x)\", \"x\": \"x\", \"x_min\": 0, \"x_max\": 1, \"n_samples\": 11, \"dv\": 0.01}"}
|
||||
11
bench/fixtures/5s/syntax-linear-algebra-v1.jsonl
Normal file
11
bench/fixtures/5s/syntax-linear-algebra-v1.jsonl
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{"_meta": {"battery": "5s", "sub_battery": "syntax", "version": "v1", "task_count": 10, "notes": "linear-algebra@v1 syntax — det/rref/inverse/eigenvalues."}}
|
||||
{"id": "5s-syn-la-001", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "linear-algebra", "domain": "json_object", "pi_star_ref": "linear-algebra@v1", "input": "{\"op\": \"det\", \"matrix\": [[1, 2], [3, 4]]}"}
|
||||
{"id": "5s-syn-la-002", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "linear-algebra", "domain": "json_object", "pi_star_ref": "linear-algebra@v1", "input": "{\"op\": \"det\", \"matrix\": [[1, 0, 0], [0, 1, 0], [0, 0, 1]]}"}
|
||||
{"id": "5s-syn-la-003", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "linear-algebra", "domain": "json_object", "pi_star_ref": "linear-algebra@v1", "input": "{\"op\": \"rref\", \"matrix\": [[2, 4], [1, 2]]}"}
|
||||
{"id": "5s-syn-la-004", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "linear-algebra", "domain": "json_object", "pi_star_ref": "linear-algebra@v1", "input": "{\"op\": \"rref\", \"matrix\": [[1, 2, 3], [0, 1, 1]]}"}
|
||||
{"id": "5s-syn-la-005", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "linear-algebra", "domain": "json_object", "pi_star_ref": "linear-algebra@v1", "input": "{\"op\": \"inverse\", \"matrix\": [[2, 0], [0, 2]]}"}
|
||||
{"id": "5s-syn-la-006", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "linear-algebra", "domain": "json_object", "pi_star_ref": "linear-algebra@v1", "input": "{\"op\": \"inverse\", \"matrix\": [[1, 1], [0, 1]]}"}
|
||||
{"id": "5s-syn-la-007", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "linear-algebra", "domain": "json_object", "pi_star_ref": "linear-algebra@v1", "input": "{\"op\": \"eigenvalues\", \"matrix\": [[2, 0], [0, 3]]}"}
|
||||
{"id": "5s-syn-la-008", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "linear-algebra", "domain": "json_object", "pi_star_ref": "linear-algebra@v1", "input": "{\"op\": \"eigenvalues\", \"matrix\": [[1, 1], [0, 1]]}"}
|
||||
{"id": "5s-syn-la-009", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "linear-algebra", "domain": "json_object", "pi_star_ref": "linear-algebra@v1", "input": "{\"op\": \"det\", \"matrix\": [[\"1/2\", \"1/3\"], [\"1\", \"1\"]]}"}
|
||||
{"id": "5s-syn-la-010", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "linear-algebra", "domain": "json_object", "pi_star_ref": "linear-algebra@v1", "input": "{\"op\": \"rref\", \"matrix\": [[1, 2], [2, 4], [3, 6]]}"}
|
||||
11
bench/fixtures/5s/syntax-tabular-v1.jsonl
Normal file
11
bench/fixtures/5s/syntax-tabular-v1.jsonl
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{"_meta": {"battery": "5s", "sub_battery": "syntax", "version": "v1", "task_count": 10, "notes": "tabular-pinned@v1 syntax — last reserved-stub π* graduates."}}
|
||||
{"id": "5s-syn-tab-001", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "tabular", "domain": "json_object", "pi_star_ref": "tabular-pinned@v1", "input": "{\"schema\": [{\"name\": \"id\", \"type\": \"int\"}], \"key_columns\": [\"id\"], \"rows\": [[1], [2]]}"}
|
||||
{"id": "5s-syn-tab-002", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "tabular", "domain": "json_object", "pi_star_ref": "tabular-pinned@v1", "input": "{\"schema\": [{\"name\": \"k\", \"type\": \"int\"}, {\"name\": \"v\", \"type\": \"str\"}], \"key_columns\": [\"k\"], \"rows\": [[1, \"a\"], [2, \"b\"]]}"}
|
||||
{"id": "5s-syn-tab-003", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "tabular", "domain": "json_object", "pi_star_ref": "tabular-pinned@v1", "input": "{\"schema\": [{\"name\": \"r\", \"type\": \"rational\"}], \"key_columns\": [], \"rows\": [[0.5], [0.25]]}"}
|
||||
{"id": "5s-syn-tab-004", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "tabular", "domain": "json_object", "pi_star_ref": "tabular-pinned@v1", "input": "{\"schema\": [{\"name\": \"f\", \"type\": \"bool\"}], \"key_columns\": [], \"rows\": [[true], [false]]}"}
|
||||
{"id": "5s-syn-tab-005", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "tabular", "domain": "json_object", "pi_star_ref": "tabular-pinned@v1", "input": "{\"schema\": [{\"name\": \"id\", \"type\": \"int\"}], \"key_columns\": [\"id\"], \"rows\": []}"}
|
||||
{"id": "5s-syn-tab-006", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "tabular", "domain": "json_object", "pi_star_ref": "tabular-pinned@v1", "input": "{\"schema\": [{\"name\": \"a\", \"type\": \"int\"}, {\"name\": \"b\", \"type\": \"int\"}], \"key_columns\": [\"a\", \"b\"], \"rows\": [[1, 2], [3, 4]]}"}
|
||||
{"id": "5s-syn-tab-007", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "tabular", "domain": "json_object", "pi_star_ref": "tabular-pinned@v1", "input": "{\"schema\": [{\"name\": \"name\", \"type\": \"str\"}], \"key_columns\": [\"name\"], \"rows\": [[\"alice\"], [\"bob\"]]}"}
|
||||
{"id": "5s-syn-tab-008", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "tabular", "domain": "json_object", "pi_star_ref": "tabular-pinned@v1", "input": "{\"schema\": [{\"name\": \"x\", \"type\": \"rational\"}], \"key_columns\": [\"x\"], \"rows\": [[\"1/3\"], [\"2/3\"]]}"}
|
||||
{"id": "5s-syn-tab-009", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "tabular", "domain": "json_object", "pi_star_ref": "tabular-pinned@v1", "input": "{\"schema\": [{\"name\": \"flag\", \"type\": \"bool\"}, {\"name\": \"id\", \"type\": \"int\"}], \"key_columns\": [\"id\"], \"rows\": [[true, 1], [false, 2]]}"}
|
||||
{"id": "5s-syn-tab-010", "battery": "5s", "sub_battery": "syntax", "version": "v1", "carrier": "tabular", "domain": "json_object", "pi_star_ref": "tabular-pinned@v1", "input": "{\"schema\": [{\"name\": \"id\", \"type\": \"int\"}], \"key_columns\": [\"id\"], \"rows\": [[100], [200], [300], [400]]}"}
|
||||
|
|
@ -11,20 +11,34 @@ the equivalence-class identity.
|
|||
Registry overview
|
||||
-----------------
|
||||
|
||||
Lookup is by ``name@version`` key. Six concrete π*'s + one stub
|
||||
ship today:
|
||||
Lookup is by ``name@version`` key. **Fifteen** concrete π*'s ship
|
||||
today; the registry has no remaining reserved stubs.
|
||||
|
||||
========================== =========== ===========================================================
|
||||
Key Domain Status
|
||||
========================== =========== ===========================================================
|
||||
``wikitext-base@v1`` text Wikitext → plain prose (Phase 1a)
|
||||
``claim-lattice@v1`` text Claim lines → JSON parsed-claim list (Phase 1a)
|
||||
``code-py-ast@v1`` code Python source → canonical AST S-expression
|
||||
``arithmetic@v1`` arithmetic Expression → exact rational ``num/den`` (SQD §14.1)
|
||||
``logic-kernel@v1`` logic Boolean expression → canonical CNF (SQD §14.3)
|
||||
``time-series-quantized@v1`` time-series JSON sample array → quantized integer vector
|
||||
``tabular-pinned@v1`` tabular reserved (stub)
|
||||
========================== =========== ===========================================================
|
||||
================================ ================= =====================================================
|
||||
Key Domain Status
|
||||
================================ ================= =====================================================
|
||||
``wikitext-base@v1`` text Wikitext → plain prose
|
||||
``claim-lattice@v1`` text Claim lines → JSON parsed-claim list
|
||||
``code-py-ast@v1`` code Python source → canonical AST S-expression
|
||||
``arithmetic@v1`` arithmetic Expression → exact rational ``num/den`` (SQD §14.1)
|
||||
``logic-kernel@v1`` logic Boolean expression → canonical CNF (SQD §14.3)
|
||||
``time-series-quantized@v1`` time-series JSON sample array → quantized integer vector (SQD §13.5)
|
||||
``tabular-pinned@v1`` tabular JSON-rows → pinned-schema canonical bytes
|
||||
``algebra-symbolic@v1`` symbolic-algebra SymPy ``expand + srepr`` (#000030 Phase 1)
|
||||
``algebra-symbolic-simplified@v1`` symbolic-algebra SymPy ``simplify + srepr`` — collapses trig identities
|
||||
``calculus-derivative@v1`` calculus ``sp.diff`` re-canonicalized through algebra-symbolic
|
||||
``calculus-integral@v1`` calculus ``sp.integrate`` + unevaluated-Integral sentinel
|
||||
``calculus-limit@v1`` calculus ``sp.limit`` + ±∞/complex-infinity sentinels
|
||||
``calculus-series@v1`` calculus Truncated Taylor / Maclaurin (drops ``O(x**n)``)
|
||||
``linear-algebra@v1`` linear-algebra RREF / det / eigenvalues / inverse via ``{op, matrix}``
|
||||
``function-sampled@v1`` function-sampled SymPy expr → quantized integer vector (bridge to time-series)
|
||||
================================ ================= =====================================================
|
||||
|
||||
The math π*'s (algebra-symbolic / calculus-* / linear-algebra /
|
||||
function-sampled) gate on the optional ``[math]`` extra:
|
||||
``pip install 'arborist[math]'`` installs SymPy. A fresh checkout
|
||||
without the extra still passes the test suite — the modules
|
||||
self-skip registration when ``import sympy`` fails.
|
||||
|
||||
Cross-modality discipline
|
||||
-------------------------
|
||||
|
|
|
|||
|
|
@ -124,19 +124,38 @@ def test_claim_lattice_empty_input_returns_empty_array():
|
|||
# --- stubs ---------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"key",
|
||||
[
|
||||
# code-py-ast@v1, logic-kernel@v1, arithmetic@v1, and
|
||||
# time-series-quantized@v1 all graduated to real
|
||||
# implementations. Only tabular-pinned@v1 remains a stub.
|
||||
"tabular-pinned@v1",
|
||||
],
|
||||
)
|
||||
def test_stubs_raise_not_implemented(key):
|
||||
pi_star = get(key)
|
||||
with pytest.raises(NotImplementedError):
|
||||
pi_star.canonicalize(b"anything")
|
||||
def test_no_stub_pi_stars_remain():
|
||||
"""Closure-criterion guard: every reserved-stub π* has graduated.
|
||||
|
||||
History:
|
||||
- 2026-05-07 (#000015): six concrete + four reserved stubs
|
||||
(``code-py-ast``, ``logic-kernel``, ``time-series-quantized``,
|
||||
``tabular-pinned``).
|
||||
- 2026-05-08: arithmetic / logic-kernel / code-py-ast /
|
||||
time-series-quantized graduated.
|
||||
- 2026-05-09 (#000030 + tabular phase): algebra-symbolic /
|
||||
calculus-derivative / -integral / -limit / -series /
|
||||
linear-algebra / function-sampled / tabular-pinned all real.
|
||||
|
||||
Adding a new reserved stub re-opens this list; that's the
|
||||
governance event this test pins."""
|
||||
from arborist.pi_star import REGISTRY
|
||||
for key, pi_star in REGISTRY.items():
|
||||
# A stub raises NotImplementedError on any input; a real
|
||||
# canonicalizer either succeeds or raises PiStarError on
|
||||
# bad input. We probe with empty bytes — most real kernels
|
||||
# raise PiStarError; none should raise NotImplementedError.
|
||||
try:
|
||||
pi_star.canonicalize(b"")
|
||||
except NotImplementedError:
|
||||
raise AssertionError(
|
||||
f"π* {key!r} still raises NotImplementedError — "
|
||||
f"reserved stub not yet graduated"
|
||||
)
|
||||
except Exception:
|
||||
# Any other exception (PiStarError, ValueError, etc.)
|
||||
# means the kernel is real.
|
||||
pass
|
||||
|
||||
|
||||
# --- time-series-quantized@v1 (graduated from stub) ------------------
|
||||
|
|
|
|||
272
tests/test_pi_star_phase_3_to_7.py
Normal file
272
tests/test_pi_star_phase_3_to_7.py
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
"""Tests for #000030 Phase 4-7 π* graduations.
|
||||
|
||||
Phase 4 — calculus-limit@v1
|
||||
Phase 5 — calculus-series@v1
|
||||
Phase 6 — linear-algebra@v1
|
||||
Phase 7 — function-sampled@v1
|
||||
|
||||
All four gate on SymPy via the [math] extra; tests skip cleanly
|
||||
when sympy is absent (mirrors the algebra-symbolic / calculus-
|
||||
derivative pattern).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from arborist.pi_star import PiStarError, get
|
||||
|
||||
|
||||
sympy = pytest.importorskip("sympy")
|
||||
|
||||
|
||||
# ===== calculus-limit@v1 (Phase 4) ========================================
|
||||
|
||||
|
||||
def test_limit_sinx_over_x_at_zero_is_one():
|
||||
ps = get("calculus-limit@v1")
|
||||
out = ps.canonicalize(b'{"f":"sin(x)/x","x":"x","point":"0"}')
|
||||
assert out == sympy.srepr(sympy.Integer(1)).encode("utf-8")
|
||||
|
||||
|
||||
def test_limit_polynomial_at_finite_point():
|
||||
ps = get("calculus-limit@v1")
|
||||
out = ps.canonicalize(b'{"f":"x**2 + 1","x":"x","point":"3"}')
|
||||
# 9 + 1 = 10
|
||||
assert out == sympy.srepr(sympy.Integer(10)).encode("utf-8")
|
||||
|
||||
|
||||
def test_limit_one_over_x_at_zero_plus_is_oo():
|
||||
ps = get("calculus-limit@v1")
|
||||
out = ps.canonicalize(b'{"f":"1/x","x":"x","point":"0","dir":"+"}')
|
||||
assert out == b"+oo"
|
||||
|
||||
|
||||
def test_limit_one_over_x_at_zero_minus_is_minus_oo():
|
||||
ps = get("calculus-limit@v1")
|
||||
out = ps.canonicalize(b'{"f":"1/x","x":"x","point":"0","dir":"-"}')
|
||||
assert out == b"-oo"
|
||||
|
||||
|
||||
def test_limit_at_infinity():
|
||||
ps = get("calculus-limit@v1")
|
||||
out = ps.canonicalize(b'{"f":"1/x","x":"x","point":"oo"}')
|
||||
assert out == sympy.srepr(sympy.Integer(0)).encode("utf-8")
|
||||
|
||||
|
||||
def test_limit_invalid_dir_raises():
|
||||
ps = get("calculus-limit@v1")
|
||||
with pytest.raises(PiStarError):
|
||||
ps.canonicalize(b'{"f":"x","x":"x","point":"0","dir":"invalid"}')
|
||||
|
||||
|
||||
def test_limit_missing_f_raises():
|
||||
ps = get("calculus-limit@v1")
|
||||
with pytest.raises(PiStarError):
|
||||
ps.canonicalize(b'{"x":"x","point":"0"}')
|
||||
|
||||
|
||||
# ===== calculus-series@v1 (Phase 5) =======================================
|
||||
|
||||
|
||||
def test_series_sinx_maclaurin_n4():
|
||||
"""Taylor series of sin(x) at x=0 to 4 terms is x - x³/6."""
|
||||
ps = get("calculus-series@v1")
|
||||
out = ps.canonicalize(b'{"f":"sin(x)","x":"x","x0":"0","n":4}')
|
||||
# Should canonicalize to x - x³/6 via expand+srepr.
|
||||
expected = sympy.srepr(sympy.expand(
|
||||
sympy.Symbol("x") - sympy.Rational(1, 6) * sympy.Symbol("x") ** 3
|
||||
)).encode("utf-8")
|
||||
assert out == expected
|
||||
|
||||
|
||||
def test_series_exp_maclaurin_n3():
|
||||
"""exp(x) Taylor at 0, n=3 → 1 + x + x²/2."""
|
||||
ps = get("calculus-series@v1")
|
||||
out = ps.canonicalize(b'{"f":"exp(x)","x":"x","x0":"0","n":3}')
|
||||
x = sympy.Symbol("x")
|
||||
expected = sympy.srepr(sympy.expand(
|
||||
1 + x + sympy.Rational(1, 2) * x ** 2
|
||||
)).encode("utf-8")
|
||||
assert out == expected
|
||||
|
||||
|
||||
def test_series_n_must_be_positive():
|
||||
ps = get("calculus-series@v1")
|
||||
with pytest.raises(PiStarError):
|
||||
ps.canonicalize(b'{"f":"sin(x)","x":"x","x0":"0","n":0}')
|
||||
|
||||
|
||||
def test_series_n_must_be_int():
|
||||
ps = get("calculus-series@v1")
|
||||
with pytest.raises(PiStarError):
|
||||
ps.canonicalize(b'{"f":"sin(x)","x":"x","x0":"0","n":3.5}')
|
||||
|
||||
|
||||
def test_series_missing_field_raises():
|
||||
ps = get("calculus-series@v1")
|
||||
with pytest.raises(PiStarError):
|
||||
ps.canonicalize(b'{"f":"sin(x)","x":"x","n":4}')
|
||||
|
||||
|
||||
# ===== linear-algebra@v1 (Phase 6) ========================================
|
||||
|
||||
|
||||
def test_linalg_det_2x2():
|
||||
ps = get("linear-algebra@v1")
|
||||
# det [[1,2],[3,4]] = 1*4 - 2*3 = -2
|
||||
out = ps.canonicalize(b'{"op":"det","matrix":[[1,2],[3,4]]}')
|
||||
assert out == b"det:-2/1"
|
||||
|
||||
|
||||
def test_linalg_det_3x3():
|
||||
ps = get("linear-algebra@v1")
|
||||
# Identity 3x3 has det 1.
|
||||
out = ps.canonicalize(
|
||||
b'{"op":"det","matrix":[[1,0,0],[0,1,0],[0,0,1]]}'
|
||||
)
|
||||
assert out == b"det:1/1"
|
||||
|
||||
|
||||
def test_linalg_rref_collapses_dependent_rows():
|
||||
"""RREF of [[2,4],[1,2]] is [[1,2],[0,0]] — one pivot."""
|
||||
ps = get("linear-algebra@v1")
|
||||
out = ps.canonicalize(b'{"op":"rref","matrix":[[2,4],[1,2]]}')
|
||||
assert out == b"rref;rows=2;cols=2:1/1|2/1||0/1|0/1"
|
||||
|
||||
|
||||
def test_linalg_inverse_2x2():
|
||||
ps = get("linear-algebra@v1")
|
||||
# [[2,0],[0,2]]^-1 = [[1/2,0],[0,1/2]]
|
||||
out = ps.canonicalize(b'{"op":"inverse","matrix":[[2,0],[0,2]]}')
|
||||
assert out == b"inverse;rows=2;cols=2:1/2|0/1||0/1|1/2"
|
||||
|
||||
|
||||
def test_linalg_eigenvalues_diagonal():
|
||||
"""Diagonal matrix has its diagonal entries as eigenvalues."""
|
||||
ps = get("linear-algebra@v1")
|
||||
out = ps.canonicalize(
|
||||
b'{"op":"eigenvalues","matrix":[[3,0],[0,2]]}'
|
||||
)
|
||||
# Sorted by srepr → 2 first then 3.
|
||||
assert out == b"eigenvalues:2/1x1|3/1x1"
|
||||
|
||||
|
||||
def test_linalg_inverse_singular_raises():
|
||||
"""Singular matrix has no inverse."""
|
||||
ps = get("linear-algebra@v1")
|
||||
with pytest.raises(PiStarError):
|
||||
ps.canonicalize(b'{"op":"inverse","matrix":[[1,1],[1,1]]}')
|
||||
|
||||
|
||||
def test_linalg_det_non_square_raises():
|
||||
ps = get("linear-algebra@v1")
|
||||
with pytest.raises(PiStarError):
|
||||
ps.canonicalize(b'{"op":"det","matrix":[[1,2,3],[4,5,6]]}')
|
||||
|
||||
|
||||
def test_linalg_unknown_op_raises():
|
||||
ps = get("linear-algebra@v1")
|
||||
with pytest.raises(PiStarError):
|
||||
ps.canonicalize(b'{"op":"transpose","matrix":[[1,2],[3,4]]}')
|
||||
|
||||
|
||||
def test_linalg_jagged_matrix_raises():
|
||||
ps = get("linear-algebra@v1")
|
||||
with pytest.raises(PiStarError):
|
||||
ps.canonicalize(b'{"op":"det","matrix":[[1,2],[3,4,5]]}')
|
||||
|
||||
|
||||
def test_linalg_cell_format_folds():
|
||||
"""1, 1.0, '1.0', '1/1' all sympify to the same SymPy Integer/Rational
|
||||
so the canonical bytes for det should match."""
|
||||
ps = get("linear-algebra@v1")
|
||||
a = ps.canonicalize(b'{"op":"det","matrix":[[1,2],[3,4]]}')
|
||||
b = ps.canonicalize(b'{"op":"det","matrix":[[1.0,2.0],[3.0,4.0]]}')
|
||||
c = ps.canonicalize(b'{"op":"det","matrix":[["1","2"],["3","4"]]}')
|
||||
assert a == b == c
|
||||
|
||||
|
||||
# ===== function-sampled@v1 (Phase 7) ======================================
|
||||
|
||||
|
||||
def test_function_sampled_basic_polynomial():
|
||||
"""x² sampled at 0,1,2,3,4 with dv=1 → 0|1|4|9|16."""
|
||||
ps = get("function-sampled@v1")
|
||||
out = ps.canonicalize(
|
||||
b'{"f":"x**2","x":"x","x_min":0,"x_max":4,"n_samples":5,"dv":1}'
|
||||
)
|
||||
assert out == b"dt=1;dv=1;n=5;t0=0:0|1|4|9|16"
|
||||
|
||||
|
||||
def test_function_sampled_equivalent_expressions_collapse():
|
||||
"""sin(x) and 2*sin(x)/2 are textually different but evaluate
|
||||
identically. Same canonical bytes."""
|
||||
ps = get("function-sampled@v1")
|
||||
a = ps.canonicalize(
|
||||
b'{"f":"sin(x)","x":"x","x_min":0,"x_max":1,"n_samples":11,"dv":0.01}'
|
||||
)
|
||||
b = ps.canonicalize(
|
||||
b'{"f":"2*sin(x)/2","x":"x","x_min":0,"x_max":1,"n_samples":11,"dv":0.01}'
|
||||
)
|
||||
assert a == b
|
||||
|
||||
|
||||
def test_function_sampled_different_grid_distinct():
|
||||
"""Different sample grid → different canonical."""
|
||||
ps = get("function-sampled@v1")
|
||||
a = ps.canonicalize(
|
||||
b'{"f":"x","x":"x","x_min":0,"x_max":4,"n_samples":5,"dv":1}'
|
||||
)
|
||||
b = ps.canonicalize(
|
||||
b'{"f":"x","x":"x","x_min":0,"x_max":4,"n_samples":9,"dv":1}'
|
||||
)
|
||||
assert a != b
|
||||
|
||||
|
||||
def test_function_sampled_output_format_byte_compatible_with_time_series():
|
||||
"""Output starts with the same 'dt=...;dv=...;n=...;t0=...:' header
|
||||
as time-series-quantized@v1 so storage paths can treat both
|
||||
interchangeably."""
|
||||
ps = get("function-sampled@v1")
|
||||
out = ps.canonicalize(
|
||||
b'{"f":"x","x":"x","x_min":0,"x_max":2,"n_samples":3,"dv":1}'
|
||||
).decode("utf-8")
|
||||
assert out.startswith("dt=")
|
||||
assert ";dv=" in out
|
||||
assert ";n=" in out
|
||||
assert ";t0=0:" in out
|
||||
|
||||
|
||||
def test_function_sampled_x_max_must_exceed_x_min():
|
||||
ps = get("function-sampled@v1")
|
||||
with pytest.raises(PiStarError):
|
||||
ps.canonicalize(
|
||||
b'{"f":"x","x":"x","x_min":1,"x_max":1,"n_samples":3,"dv":1}'
|
||||
)
|
||||
|
||||
|
||||
def test_function_sampled_n_samples_minimum_two():
|
||||
ps = get("function-sampled@v1")
|
||||
with pytest.raises(PiStarError):
|
||||
ps.canonicalize(
|
||||
b'{"f":"x","x":"x","x_min":0,"x_max":1,"n_samples":1,"dv":1}'
|
||||
)
|
||||
|
||||
|
||||
def test_function_sampled_dv_must_be_positive():
|
||||
ps = get("function-sampled@v1")
|
||||
with pytest.raises(PiStarError):
|
||||
ps.canonicalize(
|
||||
b'{"f":"x","x":"x","x_min":0,"x_max":1,"n_samples":3,"dv":0}'
|
||||
)
|
||||
|
||||
|
||||
def test_function_sampled_complex_value_raises():
|
||||
"""sqrt(x) at negative x is complex; should raise rather than
|
||||
silently drop the imaginary part."""
|
||||
ps = get("function-sampled@v1")
|
||||
with pytest.raises(PiStarError):
|
||||
ps.canonicalize(
|
||||
b'{"f":"sqrt(x)","x":"x","x_min":-1,"x_max":1,"n_samples":3,"dv":0.1}'
|
||||
)
|
||||
233
tests/test_pi_star_tabular.py
Normal file
233
tests/test_pi_star_tabular.py
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
"""Tests for ``tabular-pinned@v1`` (the last reserved-stub π*).
|
||||
|
||||
Closes the registry chapter — every modality the substrate paper
|
||||
reserved (text, claim_lattice, code, arithmetic, logic, time-series,
|
||||
tabular) is now real. Plus the math substrate extras (algebra-symbolic,
|
||||
calculus-derivative/integral/limit/series, linear-algebra,
|
||||
function-sampled).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from arborist.pi_star import PiStarError, get
|
||||
|
||||
|
||||
# ----- basic round-trips -------------------------------------------------
|
||||
|
||||
|
||||
def test_tabular_basic_int_table():
|
||||
ps = get("tabular-pinned@v1")
|
||||
out = ps.canonicalize(
|
||||
b'{"schema":[{"name":"id","type":"int"}],"key_columns":["id"],'
|
||||
b'"rows":[[1],[2],[3]]}'
|
||||
)
|
||||
assert out == b"schema=id:int\nkey=id\nn=3\nrows:\n1\n2\n3"
|
||||
|
||||
|
||||
def test_tabular_empty_rows():
|
||||
"""Empty body still emits header + n=0."""
|
||||
ps = get("tabular-pinned@v1")
|
||||
out = ps.canonicalize(
|
||||
b'{"schema":[{"name":"id","type":"int"}],"key_columns":["id"],'
|
||||
b'"rows":[]}'
|
||||
)
|
||||
assert out == b"schema=id:int\nkey=id\nn=0\nrows:\n"
|
||||
|
||||
|
||||
def test_tabular_schema_only_no_key_columns():
|
||||
"""key_columns=[] is valid: rows preserve input order."""
|
||||
ps = get("tabular-pinned@v1")
|
||||
out = ps.canonicalize(
|
||||
b'{"schema":[{"name":"x","type":"str"}],"key_columns":[],'
|
||||
b'"rows":[["b"],["a"],["c"]]}'
|
||||
)
|
||||
assert out == b"schema=x:str\nkey=\nn=3\nrows:\nb\na\nc"
|
||||
|
||||
|
||||
# ----- equivalence-class collapse ----------------------------------------
|
||||
|
||||
|
||||
def test_tabular_row_order_collapses_under_key_sort():
|
||||
"""Same data, different row order → same canonical (key-sorted)."""
|
||||
ps = get("tabular-pinned@v1")
|
||||
a = ps.canonicalize(
|
||||
b'{"schema":[{"name":"id","type":"int"},{"name":"name","type":"str"}],'
|
||||
b'"key_columns":["id"],"rows":[[2,"b"],[1,"a"],[3,"c"]]}'
|
||||
)
|
||||
b = ps.canonicalize(
|
||||
b'{"schema":[{"name":"id","type":"int"},{"name":"name","type":"str"}],'
|
||||
b'"key_columns":["id"],"rows":[[1,"a"],[2,"b"],[3,"c"]]}'
|
||||
)
|
||||
assert a == b
|
||||
|
||||
|
||||
def test_tabular_int_float_string_fold_equivalence():
|
||||
"""1 ≡ 1.0 ≡ "1.0" for int columns; same canonical."""
|
||||
ps = get("tabular-pinned@v1")
|
||||
a = ps.canonicalize(
|
||||
b'{"schema":[{"name":"v","type":"int"}],"key_columns":["v"],'
|
||||
b'"rows":[[1]]}'
|
||||
)
|
||||
b = ps.canonicalize(
|
||||
b'{"schema":[{"name":"v","type":"int"}],"key_columns":["v"],'
|
||||
b'"rows":[[1.0]]}'
|
||||
)
|
||||
c = ps.canonicalize(
|
||||
b'{"schema":[{"name":"v","type":"int"}],"key_columns":["v"],'
|
||||
b'"rows":[["1.0"]]}'
|
||||
)
|
||||
assert a == b == c
|
||||
|
||||
|
||||
def test_tabular_rational_fold():
|
||||
"""0.5 → 1/2; "1/2" → 1/2; same canonical."""
|
||||
ps = get("tabular-pinned@v1")
|
||||
a = ps.canonicalize(
|
||||
b'{"schema":[{"name":"r","type":"rational"}],"key_columns":[],'
|
||||
b'"rows":[[0.5]]}'
|
||||
)
|
||||
b = ps.canonicalize(
|
||||
b'{"schema":[{"name":"r","type":"rational"}],"key_columns":[],'
|
||||
b'"rows":[["1/2"]]}'
|
||||
)
|
||||
assert a == b
|
||||
assert b"1/2" in a
|
||||
|
||||
|
||||
def test_tabular_bool_normalization():
|
||||
"""true / True / "true" / 1 (when bool col) — fold to 'true'."""
|
||||
ps = get("tabular-pinned@v1")
|
||||
a = ps.canonicalize(
|
||||
b'{"schema":[{"name":"flag","type":"bool"}],"key_columns":[],'
|
||||
b'"rows":[[true]]}'
|
||||
)
|
||||
b = ps.canonicalize(
|
||||
b'{"schema":[{"name":"flag","type":"bool"}],"key_columns":[],'
|
||||
b'"rows":[["true"]]}'
|
||||
)
|
||||
c = ps.canonicalize(
|
||||
b'{"schema":[{"name":"flag","type":"bool"}],"key_columns":[],'
|
||||
b'"rows":[["yes"]]}'
|
||||
)
|
||||
assert a == b == c
|
||||
assert b"true" in a
|
||||
|
||||
|
||||
# ----- equivalence-class distinction -------------------------------------
|
||||
|
||||
|
||||
def test_tabular_different_schema_distinct():
|
||||
ps = get("tabular-pinned@v1")
|
||||
a = ps.canonicalize(
|
||||
b'{"schema":[{"name":"x","type":"int"}],"key_columns":[],'
|
||||
b'"rows":[[1]]}'
|
||||
)
|
||||
b = ps.canonicalize(
|
||||
b'{"schema":[{"name":"y","type":"int"}],"key_columns":[],'
|
||||
b'"rows":[[1]]}'
|
||||
)
|
||||
assert a != b
|
||||
|
||||
|
||||
def test_tabular_different_key_columns_distinct():
|
||||
"""Same data, different declared key → different canonical (sort changes)."""
|
||||
ps = get("tabular-pinned@v1")
|
||||
a = ps.canonicalize(
|
||||
b'{"schema":[{"name":"a","type":"int"},{"name":"b","type":"int"}],'
|
||||
b'"key_columns":["a"],"rows":[[2,1],[1,2]]}'
|
||||
)
|
||||
b = ps.canonicalize(
|
||||
b'{"schema":[{"name":"a","type":"int"},{"name":"b","type":"int"}],'
|
||||
b'"key_columns":["b"],"rows":[[2,1],[1,2]]}'
|
||||
)
|
||||
assert a != b
|
||||
|
||||
|
||||
def test_tabular_header_case_pinned():
|
||||
"""Header case is part of identity; 'Name' ≠ 'name'."""
|
||||
ps = get("tabular-pinned@v1")
|
||||
a = ps.canonicalize(
|
||||
b'{"schema":[{"name":"Name","type":"str"}],"key_columns":[],'
|
||||
b'"rows":[["x"]]}'
|
||||
)
|
||||
b = ps.canonicalize(
|
||||
b'{"schema":[{"name":"name","type":"str"}],"key_columns":[],'
|
||||
b'"rows":[["x"]]}'
|
||||
)
|
||||
assert a != b
|
||||
|
||||
|
||||
# ----- error paths -------------------------------------------------------
|
||||
|
||||
|
||||
def test_tabular_missing_schema_field_raises():
|
||||
ps = get("tabular-pinned@v1")
|
||||
with pytest.raises(PiStarError):
|
||||
ps.canonicalize(b'{"key_columns":[],"rows":[]}')
|
||||
|
||||
|
||||
def test_tabular_unknown_type_raises():
|
||||
ps = get("tabular-pinned@v1")
|
||||
with pytest.raises(PiStarError):
|
||||
ps.canonicalize(
|
||||
b'{"schema":[{"name":"x","type":"datetime"}],'
|
||||
b'"key_columns":[],"rows":[]}'
|
||||
)
|
||||
|
||||
|
||||
def test_tabular_key_column_not_in_schema_raises():
|
||||
ps = get("tabular-pinned@v1")
|
||||
with pytest.raises(PiStarError):
|
||||
ps.canonicalize(
|
||||
b'{"schema":[{"name":"a","type":"int"}],'
|
||||
b'"key_columns":["b"],"rows":[[1]]}'
|
||||
)
|
||||
|
||||
|
||||
def test_tabular_duplicate_column_names_raises():
|
||||
ps = get("tabular-pinned@v1")
|
||||
with pytest.raises(PiStarError):
|
||||
ps.canonicalize(
|
||||
b'{"schema":[{"name":"a","type":"int"},{"name":"a","type":"str"}],'
|
||||
b'"key_columns":[],"rows":[]}'
|
||||
)
|
||||
|
||||
|
||||
def test_tabular_row_arity_mismatch_raises():
|
||||
ps = get("tabular-pinned@v1")
|
||||
with pytest.raises(PiStarError):
|
||||
ps.canonicalize(
|
||||
b'{"schema":[{"name":"a","type":"int"},{"name":"b","type":"int"}],'
|
||||
b'"key_columns":[],"rows":[[1]]}'
|
||||
)
|
||||
|
||||
|
||||
def test_tabular_int_with_non_integer_float_raises():
|
||||
ps = get("tabular-pinned@v1")
|
||||
with pytest.raises(PiStarError):
|
||||
ps.canonicalize(
|
||||
b'{"schema":[{"name":"v","type":"int"}],"key_columns":[],'
|
||||
b'"rows":[[1.5]]}'
|
||||
)
|
||||
|
||||
|
||||
def test_tabular_bool_subtype_of_int_rejected_in_int_column():
|
||||
"""Python booleans are int-subtypes; we reject so True doesn't
|
||||
silently become 1 in an int column."""
|
||||
ps = get("tabular-pinned@v1")
|
||||
with pytest.raises(PiStarError):
|
||||
ps.canonicalize(
|
||||
b'{"schema":[{"name":"v","type":"int"}],"key_columns":[],'
|
||||
b'"rows":[[true]]}'
|
||||
)
|
||||
|
||||
|
||||
# ----- registry presence -------------------------------------------------
|
||||
|
||||
|
||||
def test_tabular_pinned_registered():
|
||||
"""Sanity: the kernel is in the registry under the expected key."""
|
||||
from arborist.pi_star import list_keys
|
||||
assert "tabular-pinned@v1" in list_keys()
|
||||
Loading…
Add table
Add a link
Reference in a new issue