Jump to content

The Principia Machina/TPC-PY/gen units corpus.py

From The Invariance Itself
Revision as of 04:32, 28 September 2026 by BFP Rauch (talk | contribs) (1 revision imported)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
# -*- coding: utf-8 -*-
"""Generator for tpc/vectors/units_corpus.json (increment 6, SELECTED
D103.1, D104.1, D105.1). Expectations are written literally below and derived by
hand: canonical spellings and DIMENSIONs from the catalogue, scales from
the SI Brochure's exact values (degree = pi/180 rad), contradiction results
from the SR3 inequality worked on paper. The generator refuses to write the
corpus if the module disagrees with any of them.
Run: cd tpc && python3 gen_units_corpus.py (Windows: py ...).
Recovered 2026-09-27 (BASE-GEN; record 0.18.0 section 7) from the TIDY 3
archive, tidy3_2026-09-25/gen_units_corpus.py, SHA-256
0472369761784d030f718bf00f1f96917bd797a20361826c7e3b2318c5192860, where it
regenerates the archived corpus byte for byte. Changed in the tree: S15's
seven cases appended with corpus 0.1.1's meta (RD-C8-3); the writer writes
a hidden character as itself, as the published corpus carries it
(RD-C8-2)."""
import json, os, sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import tpc_strike as S
import tpc_units as U

cases = []
def case(cid, fn, inp, expect, **extra):
    c = {"id": cid, "fn": fn, "input": inp}
    c.update(extra)
    c["expect"] = expect
    cases.append(c)

P = "unit_parse"
# ---- grammar and canonical spelling -------------------------------------------------
case("parse/newton-base", P, {"u": "kg m s-2"}, "[OK,m kg s-2,L1 M1 T-2,none]",
     note="catalogue order: m before g; kg is k + g")
case("parse/any-order", P, {"u": "s-2 m kg"}, "[OK,m kg s-2,L1 M1 T-2,none]")
case("parse/named", P, {"u": "N"}, "[OK,N,L1 M1 T-2,none]")
case("parse/merge", P, {"u": "m s-1 m"}, "[OK,m2 s-1,L2 T-1,none]")
case("parse/cancel", P, {"u": "m m-1"}, "[OK,1,1,none]")
case("parse/exponent-one", P, {"u": "m1"}, "[OK,m,L1,none]")
case("parse/zero-exponent", P, {"u": "m0 s"}, "[OK,s,T1,none]")
case("parse/one", P, {"u": "1"}, "[OK,1,1,none]")
case("parse/spelling-only", P, {"u": "km m"}, "[OK,m km,L2,none]",
     note="only spelling is canonical: km m is not rewritten as m2")
case("parse/prefix-power-order", P, {"u": "km mm"}, "[OK,mm km,L2,none]")
case("parse/micro-ascii", P, {"u": "uL"}, "[OK,uL,L3,none]")
case("parse/deca", P, {"u": "dam"}, "[OK,dam,L1,none]")
case("parse/hecto", P, {"u": "hPa"}, "[OK,hPa,L-1 M1 T-2,none]")
case("parse/megagram", P, {"u": "Mg"}, "[OK,Mg,M1,none]")
case("parse/megatonne", P, {"u": "Mt"}, "[OK,Mt,M1,none]", note="the Brochure is silent on prefixed t")
case("parse/milliarcsecond", P, {"u": "marcsec"}, "[OK,marcsec,1,rad1]", note="mas in the Brochure")
case("parse/ohm", P, {"u": "kOhm"}, "[OK,kOhm,L2 M1 T-3 I-2,none]")
case("parse/tesla-vs-tera", P, {"u": "TT"}, "[OK,TT,M1 T-2 I-1,none]",
     note="tera-tesla; the tesla metre is written T m")
# ---- refusals: shape --------------------------------------------------------------
case("refused/empty", P, {"u": ""}, "[REFUSED,UNIT  is not a unit]")
case("refused/double-space", P, {"u": "m  s"}, "[REFUSED,UNIT m  s is not a unit]")
case("refused/caret", P, {"u": "m^2"}, "[REFUSED,UNIT m^2 is not a unit]")
case("refused/slash", P, {"u": "m/s"}, "[REFUSED,UNIT m/s is not a unit]")
case("refused/five-digit-exponent", P, {"u": "m10000"}, "[REFUSED,UNIT m10000 is not a unit]")
case("refused/bidi-control", P, {"u": "m\u202e"}, "[REFUSED,UNIT m<U+202E> is not a unit]",
     note="the quoted input is escaped (zone schemas 6.3)")
case("refused/not-a-string", P, {"u": 5}, "[REFUSED,UNIT ? is not a unit]")
case("refused/merged-range", P, {"u": "m9999 m9999"},
     "[REFUSED,UNIT m9999 m9999: an exponent is out of range]")
case("refused/dimension-range", P, {"u": "L3334"},
     "[REFUSED,UNIT L3334: an exponent is out of range]", note="L3334 is L10002 as a DIMENSION")
# ---- refusals: symbols ------------------------------------------------------------
case("refused/unknown", P, {"u": "furlong"}, "[REFUSED,UNIT furlong: furlong is not a unit symbol]")
case("refused/double-prefix", P, {"u": "kkg"}, "[REFUSED,UNIT kkg: kkg is not a unit symbol]")
case("refused/day-prefix", P, {"u": "kd"}, "[REFUSED,UNIT kd: d takes no prefix]",
     note="the SI Brochure forbids prefixes on the non-SI units of time")
case("refused/au-prefix", P, {"u": "kau"}, "[REFUSED,UNIT kau: au takes no prefix]")
case("refused/minute-prefix", P, {"u": "mmin"}, "[REFUSED,UNIT mmin: min takes no prefix]")
case("refused/hour-prefix", P, {"u": "hh"}, "[REFUSED,UNIT hh: h takes no prefix]")
case("refused/celsius", P, {"u": "degC"},
     "[REFUSED,UNIT degC: degC is refused, an offset scale, not a ratio]")
case("refused/dalton", P, {"u": "mDa"},
     "[REFUSED,UNIT mDa: Da is refused, a measured value; the conversion would carry an uncertainty]")
case("refused/decibel", P, {"u": "dB"}, "[REFUSED,UNIT dB: dB is refused, a logarithmic scale]")
case("refused/prefixed-decibel", P, {"u": "kdB"}, "[REFUSED,UNIT kdB: dB is refused, a logarithmic scale]")
case("refused/neper", P, {"u": "Np"}, "[REFUSED,UNIT Np: Np is refused, a logarithmic scale]")
# ---- KIND ---------------------------------------------------------------------------
case("kind/hertz", P, {"u": "Hz"}, "[OK,Hz,T-1,Hz1]")
case("kind/rad-per-second", P, {"u": "rad s-1"}, "[OK,s-1 rad,T-1,rad1]")
case("kind/per-second", P, {"u": "s-1"}, "[OK,s-1,T-1,none]")
case("kind/steradian", P, {"u": "sr"}, "[OK,sr,1,rad2]")
case("kind/square-degree", P, {"u": "deg2"}, "[OK,deg2,1,rad2]")
case("kind/lumen", P, {"u": "lm"}, "[OK,lm,J1,rad2]", note="lm = cd sr")
case("kind/becquerel", P, {"u": "Bq"}, "[OK,Bq,T-1,Bq1]")
case("kind/gray-sievert", P, {"u": "Gy Sv-1"}, "[OK,Gy Sv-1,1,Gy1 Sv-1]")
# ---- scale ----------------------------------------------------------------------------
SC = "unit_scale"
case("scale/one", SC, {"u": "1"}, "[OK,1]")
case("scale/square-cm", SC, {"u": "cm2"}, "[OK,0.0001]")
case("scale/per-minute", SC, {"u": "min-1"}, "[OK,0.0166666666667]")
case("scale/hectare", SC, {"u": "ha"}, "[OK,10000]")
case("scale/au", SC, {"u": "au"}, "[OK,149597870700]")
case("scale/kiloelectronvolt", SC, {"u": "keV"}, "[OK,1.602176634e-16]")
case("scale/square-degree", SC, {"u": "deg2"}, "[OK,0.000304617419787]", note="(pi/180)^2")
case("scale/arcsecond", SC, {"u": "arcsec"}, "[OK,4.8481368111e-06]", note="pi/648000")
case("scale/megatonne", SC, {"u": "Mt"}, "[OK,1000000000]")
case("scale/km-per-hour", SC, {"u": "km h-1"}, "[OK,0.277777777778]")
case("scale/overflow", SC, {"u": "km9999"}, "[REFUSED,UNIT km9999 has no finite scale]")
case("scale/underflow-reciprocal", SC, {"u": "qm-20"}, "[REFUSED,UNIT qm-20 has no finite scale]",
     note="(1e-30)^20 underflows to zero; the reciprocal is refused, never divided")
case("scale/partial-overflow", SC, {"u": "Qm11 qm11"}, "[REFUSED,UNIT Qm11 qm11 has no finite scale]",
     note="a recorded limit: the whole is 1 m22, but a partial product leaves the double range")
# ---- C-32 -----------------------------------------------------------------------------
MA = "unit_matches"
case("c32/match", MA, {"u": "kg m s-2", "dimension": "L1 M1 T-2"}, "[OK,true]")
case("c32/match-any-order", MA, {"u": "N", "dimension": "T-2 M1 L1"}, "[OK,true]")
case("c32/mismatch", MA, {"u": "J", "dimension": "L1"}, "[OK,false]")
case("c32/angle-is-one", MA, {"u": "rad", "dimension": "1"}, "[OK,true]")
case("c32/bad-dimension", MA, {"u": "m", "dimension": "X1"}, "[REFUSED,DIMENSION X1 is not a dimension]")
case("c32/bad-unit", MA, {"u": "furlong", "dimension": "L1"},
     "[REFUSED,UNIT furlong: furlong is not a unit symbol]")
# ---- D40 normalization -------------------------------------------------------------
NM = "normalize_measurement"
case("norm/cm", NM, {"value": "5", "uncertainty": "0.1", "u": "cm"}, "[OK,0.05,0.001,L1,none]")
case("norm/spring-x-max", NM, {"value": "0.05", "uncertainty": "0.001", "u": "m"}, "[OK,0.05,0.001,L1,none]")
case("norm/hours", NM, {"value": "1.5", "uncertainty": "0.01", "u": "h"}, "[OK,5400,36,T1,none]")
case("norm/negative-zero", NM, {"value": "-0", "uncertainty": "0", "u": "m"}, "[OK,0,0,L1,none]")
case("norm/not-decimal", NM, {"value": "x", "uncertainty": "0", "u": "m"}, "[REFUSED,VALUE x is not a decimal]")
case("norm/negative-u", NM, {"value": "1", "uncertainty": "-1", "u": "m"},
     "[REFUSED,UNCERTAINTY -1 is negative]")
case("norm/value-overflow", NM, {"value": "1e308", "uncertainty": "0", "u": "km"},
     "[REFUSED,VALUE 1e308 in UNIT km is not a finite double]")
# ---- SR3 after normalization ----------------------------------------------------------
MM = "mm_contradicts_units"
case("mm/agree-across-prefix", MM, {"x1": "100", "u1": "1", "unit1": "cm", "x2": "1.02", "u2": "0.01",
     "unit2": "m", "k": 2.0}, "[OK,false]", note="|1.00-1.02| = 0.02 <= 2 x 0.01414")
case("mm/contradict-across-prefix", MM, {"x1": "100", "u1": "1", "unit1": "cm", "x2": "1.05",
     "u2": "0.01", "unit2": "m", "k": 2.0}, "[OK,true]", note="0.05 > 0.0283")
case("mm/hz-vs-rad", MM, {"x1": "1", "u1": "0.01", "unit1": "Hz", "x2": "6.2832", "u2": "0.01",
     "unit2": "rad s-1", "k": 2.0}, "[CONVENTION,units of different kind: Hz1 and rad1]",
     note="the same oscillation; never compared, TPX opens an audit block")
case("mm/hz-vs-per-second", MM, {"x1": "50", "u1": "0.1", "unit1": "Hz", "x2": "50", "u2": "0.1",
     "unit2": "s-1", "k": 2.0}, "[CONVENTION,units of different kind: Hz1 and none]")
case("mm/gray-vs-sievert", MM, {"x1": "1", "u1": "0.1", "unit1": "Gy", "x2": "1", "u2": "0.1",
     "unit2": "Sv", "k": 2.0}, "[CONVENTION,units of different kind: Gy1 and Sv1]")
case("mm/cross-dimension", MM, {"x1": "1", "u1": "0.01", "unit1": "m", "x2": "1", "u2": "0.01",
     "unit2": "s", "k": 2.0}, "[REFUSED,units of different dimension: L1 and T1]")
LM = "lm_contradicts_units"
case("lm/degree-agrees", LM, {"x_l": "180", "unit_l": "deg", "x_m": "3.14159", "u_m": "0.0001",
     "unit_m": "rad", "k": 2.0}, "[OK,false]", note="pi - 3.14159 = 2.65e-6 <= 2e-4")
case("lm/degree-contradicts", LM, {"x_l": "180", "unit_l": "deg", "x_m": "3.1", "u_m": "0.0001",
     "unit_m": "rad", "k": 2.0}, "[OK,true]")
S8 = "sr8_scale_factor_units"
case("sr8/mixed-prefix", S8, {"measurements": [{"x": "1.00", "u": "0.01", "unit": "m"},
     {"x": "103", "u": "1", "unit": "cm"}, {"x": "0.99", "u": "0.01", "unit": "m"}]},
     "[OK,2.08166599947]", note="chi2/(N-1) = 4.3333 over 1.00, 1.03, 0.99 m")
case("sr8/convention", S8, {"measurements": [{"x": "1", "u": "0.1", "unit": "Hz"},
     {"x": "1", "u": "0.1", "unit": "s-1"}]}, "[CONVENTION,units of different kind: Hz1 and none]")
case("sr8/zero-u", S8, {"measurements": [{"x": "1", "u": "0", "unit": "m"},
     {"x": "1", "u": "0.1", "unit": "m"}]}, "[REFUSED,every M record needs an uncertainty above zero]")
case("sr8/refusal-first", S8, {"measurements": [{"x": "1", "u": "0", "unit": "m"},
     {"x": "1", "u": "0.1", "unit": "Hz"}, {"x": "1", "u": "0.1", "unit": "s"}]},
     "[REFUSED,units of different dimension: L1 and T-1]")

# ---- S15 (tpc_units 0.1.1, proposed; corpus 0.1.1): appended, no earlier case
# changed. Hand-derived: the DIMENSION grammar bounds each base's summed
# exponent to -9999..9999 (S15, D99, D103); tpc_units' own sums were audited
# and pinned. Reconstructed 2026-09-27 from the published corpus 0.1.1
# (BASE-GEN; append_units_s15.py, its seed, is not in the tree or the archive).
M = "unit_matches"
case("s15/matches-dimension-out-of-range", M, {"u": "m", "dimension": "L9999 L1"},
     "[REFUSED,DIMENSION L9999 L1: an exponent is out of range]",
     note="S15: the DIMENSION's bases sum to L10000, outside the grammar; tpc_units 0.1.0 answered [OK,false]")
case("s15/matches-dimension-bad-token", M, {"u": "m", "dimension": "L9999 L1 Q1"},
     "[REFUSED,DIMENSION L9999 L1 Q1 is not a dimension]")
case("s15/matches-sum-at-bound", M, {"u": "m9999", "dimension": "L5000 L4999"}, "[OK,true]")
case("s15/matches-sum-returns", M, {"u": "m", "dimension": "L5000 L-4999"}, "[OK,true]")
case("s15/audit-merged-symbol-sum", P, {"u": "m5000 m5000"},
     "[REFUSED,UNIT m5000 m5000: an exponent is out of range]",
     note="S15 audit: one symbol's exponents add and are bounded (tpc_units 0.1.0, unchanged)")
case("s15/audit-dimension-sum-across-symbols", P, {"u": "km5000 m5000"},
     "[REFUSED,UNIT km5000 m5000: an exponent is out of range]",
     note="S15 audit: the resulting DIMENSION L10000 is bounded (tpc_units 0.1.0, unchanged)")
case("s15/audit-kind-not-a-grammar-form", P, {"u": "sr5000"}, "[OK,sr5000,1,rad10000]",
     note=("S15 audit: sr counts as rad2, so the KIND is rad10000; KIND is derived, written by no "
           "author and entering no preimage, so it is not bounded here; tpc_form refuses it where "
           "it is read (S14)"))


def dispatch(fn, i):
    if fn == "unit_parse":
        return U.unit_parse(i["u"])
    if fn == "unit_scale":
        return U.unit_scale(i["u"])
    if fn == "unit_matches":
        return U.unit_matches(i["u"], i["dimension"])
    if fn == "normalize_measurement":
        return U.normalize_measurement(i["value"], i["uncertainty"], i["u"])
    if fn == "mm_contradicts_units":
        return U.mm_contradicts_units(i["x1"], i["u1"], i["unit1"], i["x2"], i["u2"], i["unit2"],
                                      i["k"], i.get("S", 1.0))
    if fn == "lm_contradicts_units":
        return U.lm_contradicts_units(i["x_l"], i["unit_l"], i["x_m"], i["u_m"], i["unit_m"],
                                      i["k"], i.get("S", 1.0))
    if fn == "sr8_scale_factor_units":
        return U.sr8_scale_factor_units(i["measurements"])
    raise KeyError(fn)


bad = 0
for c in cases:
    got = S.canon(dispatch(c["fn"], c["input"]))
    if got != c["expect"]:
        bad += 1
        print("MISMATCH", c["id"], "\n  expect", c["expect"], "\n  got   ", got)
if bad:
    sys.exit("refusing to write: %d mismatches" % bad)

corpus = {
  "meta": {
    "name": "tpc_units_corpus", "version": "0.1.1",
    "status": "SELECTED D103.1, D104.1, D105.1 (2026-09-22); VERIFIED (both residences)",
    "conforms_tps": ("0.1.0 revision 4: SR3 and SR8 as amended by D40.1 (TPS 4.4.4), C-32 (TPS 3.2.9); "
                     "unit grammar and KIND text (tpm_units 0.2.0) enters TPS at revision 5"),
    "module": "tpc_units 0.1.1 (proposed) (tpc_zone 0.4.1, tpc_strike 0.1.1)",
    "note": ("Expectations hand-derived: spellings and DIMENSIONs from the catalogue, scales from the SI "
             "Brochure's exact values, SR3 results worked on paper. Both residences MUST reproduce every "
             "expect. 0.1.1 (S15) appends the s15/ cases and changes no earlier case: a DIMENSION "
             "whose bases sum outside -9999..9999 is refused with the reason, and the audit of "
             "tpc_units' own sums is pinned.")
  },
  "cases": cases
}
with open(os.path.join(HERE, "vectors", "units_corpus.json"), "w", encoding="utf-8",
          newline="\n") as f:
    f.write(json.dumps(corpus, indent=1, ensure_ascii=False) + "\n")
print("wrote", len(cases), "units cases; all match literal expectations")