arborist/tests/test_pi_star_tabular.py
russell@unturf.com abe5988bef
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.
2026-05-09 13:04:43 -04:00

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()