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.
233 lines
7.1 KiB
Python
233 lines
7.1 KiB
Python
"""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()
|