From 7c58c57143cc322eb0d578162e6df87ac0f4cd1f Mon Sep 17 00:00:00 2001 From: Ali Romman <226877337+aliromman@users.noreply.github.com> Date: Mon, 26 Oct 2026 10:20:30 -0500 Subject: [PATCH] Certify common-base invoice order over every power exponent --- docs/research/power-polynomial-comparison.md | 127 ++++++++++++ docs/research/recursive-invoice-preflight.md | 5 + scripts/compare_power_polynomial.py | 192 +++++++++++++++++++ tests/test_research_power_polynomial.py | 171 +++++++++++++++++ 4 files changed, 495 insertions(+) create mode 100644 docs/research/power-polynomial-comparison.md create mode 100644 scripts/compare_power_polynomial.py create mode 100644 tests/test_research_power_polynomial.py diff --git a/docs/research/power-polynomial-comparison.md b/docs/research/power-polynomial-comparison.md new file mode 100644 index 0000000000..77820c52f4 --- /dev/null +++ b/docs/research/power-polynomial-comparison.md @@ -0,0 +1,127 @@ +# Compare a power invoice over the whole exponent interval + +An inconclusive concave-order result does not prove that a power cost changes +sign. When all changed ranks are powers of one integer, this tool gives an exact +answer for **every** exponent `0 < t < 1`: + +```sh +python3 -B scripts/compare_power_polynomial.py OLD.json NEW.json +``` + +The inputs use the [recursive invoice schema](recursive-invoice-preflight.md). +Their contracts must match. The tool first cancels equal rank terms in +`(new paid-new targets)-(old paid-old targets)`. It then finds an integer base +`b > 1` such that each remaining rank is `b^j`. Rank one has exponent zero. A +constant or zero difference uses base two by convention. Unsupported ranks fail +with exit code2; the tool does not round them to a common base. + +The report binds the exact input bytes by SHA256. It gives the signed polynomial +coefficients in ascending order, endpoint multiplicities, exact root counts and +a rational sample. All arithmetic uses integers and fractions. No grid sampling, +floating-point roots or precision choice determines the result. + +| Classification | Meaning for the new cost minus the old cost | +|---|---| +| `uniformly_lower` | Strictly negative for every `0 < t < 1`. | +| `uniformly_higher` | Strictly positive for every `0 < t < 1`. | +| `nonpositive_with_equality` | Never positive, with at least one interior zero. | +| `nonnegative_with_equality` | Never negative, with at least one interior zero. | +| `crossing` | Both signs occur in the open exponent interval. | +| `equal` | The signed rank difference is zero. | + +For example, old paid jobs `{"2":2}` and new jobs `{"1":1,"4":1}` give +`P(x)=(x-1)^2`. General concave order is inconclusive, but the new power cost is +strictly higher for every `0 < t < 1`. By contrast, old jobs `{"2":12}` and new +jobs `{"1":9,"4":4}` give `(2x-3)^2`: the result has one interior equality and +is not strict throughout the interval. Old jobs `{"1":3}` and new jobs `{"2":2}` +give `2x-3`, so the cost crosses zero. These are declared arithmetic examples, +not new legal circuit replacements. + +## Exact reduction and proof + +Let `d_r` be the signed difference after paid and target terms are combined. +Write each remaining rank as `r=b^j`, and set + +`P(x) = sum_j d_(b^j) x^j`. + +The map `x=b^t` is a continuous, strictly increasing bijection from `(0,1)` to +`(1,b)`. Thus `sum_r d_r r^t = P(b^t)`, with the same signs and zeros. Factoring +the first nonunit rank and dividing its prime exponents by their greatest common +divisor gives its primitive integer base. Repeated exact division checks every +other rank. If a common integer base exists, that primitive base also works. + +The zero polynomial gives `equal`. For a nonzero polynomial, first divide out +every factor `x-1` and `x-b`. These zeros lie outside the open domain. Record +their multiplicities; do not count them as interior equalities. All later GCDs +divide this reduced polynomial, so neither endpoint can become a root again. + +For any remaining nonconstant polynomial `G`, compute `D=gcd(G,G')` and the +square-free quotient `S=G/D`. Its roots are the distinct roots of `G`. Form the +Sturm sequence `S,S',-rem(S,S'),...`, stopping at a nonzero constant. At an +endpoint, omit zero values before counting adjacent sign changes. The difference +between the variation counts at1 and b is the number of distinct interior roots. +All divisions are exact operations in Q. Clearing denominators and dividing by +positive content preserve the signs needed by Sturm's theorem. + +To obtain multiplicity parity, repeat with `G <- gcd(G,G')`. Let `n_j` be the +distinct interior root count at step j, starting with j=0. A root of multiplicity +m appears in exactly the first m counts. Hence its contribution to + +`n_0 - n_1 + n_2 - n_3 + ...` + +is one if m is odd and zero if m is even. This sum counts odd-multiplicity +interior roots. Each such root changes the sign of a real polynomial. Its +presence proves `crossing`. If none exists, the sign is constant away from the +interior roots. One nonzero rational sample determines that sign. A polynomial +of degree d cannot vanish at d+1 distinct sample points, so the bounded sample +selection must succeed. With no interior roots the order is strict. With only +even-multiplicity interior roots the order is weak, with equality at those roots. + +This uses classical Sturm theory, not a new root-count theorem. See Philippe +Pebay, J. Maurice Rojas and David C. Thompson, +[Sturm's Theorem with Endpoints](https://arxiv.org/abs/2208.07904). This tool +removes endpoint factors before the count to make its open-domain convention +explicit. The small polynomial implementation uses standard-library fractions; +the shared invoice tools and CI do not require a computer-algebra installation. + +## Verification and limits + +The independent low-degree oracle checks all343 quadratics with coefficients in +`{-3,...,3}` by their exact extrema and monotone intervals. A second oracle checks +288 products with known endpoint, repeated interior and nonreal roots. Further +checks cover irrational interior roots, degree29, target credits, cancellation, +large coefficients, unsupported ranks and the isolated optimized CLI. +An optional independent check uses SymPy1.14.0 root isolation on64 further +polynomials, including24 dense degree29 cases with coefficients near the input +limit. That check is skipped when SymPy is not installed; the other tests and +the tool need only the standard library. + +```sh +python3 -B -m unittest discover -s tests -p 'test_research_power_polynomial.py' -v +``` + +The invoice rank limit10^9 bounds the polynomial degree by29. The existing count, +input-size and contract limits still apply. Only uncancelled ranks need a common +base. This can help reject or price a proposed rank exchange before a full +construction replay. It does not authenticate a circuit, detect an omitted +source charge, establish ownership or native legality, or admit a compiler or +kappa claim. Even `uniformly_lower` concerns only the declared common tariff. + +The [pointwise power comparator PR5732](https://github.com/CrocSwap/integer-mult-bounds/pull/5732) +handles arbitrary supported ranks at one rational exponent. The +[concave witnesses PR5731](https://github.com/CrocSwap/integer-mult-bounds/pull/5731) +explain the broader tariff screen. Those tools are complementary; this tool +does not depend on their unmerged code. The source invoice interface retains +zxv's comparator, mightyOil's T16 audit and all credited predecessors. + +## Open next questions + +- Can an actual proposed exchange with an inconclusive concave screen use this + certificate? Supply the complete changed paid and target terms and the same + consumer contract before applying the tool. +- Can noncommensurate rank differences receive a uniform sign certificate? + This polynomial reduction does not answer that question. A finite exponent + sample must not be treated as a proof over an interval. + +New reduction, implementation and checks: Ali Romman with substantial OpenAI +Codex assistance. Apache-2.0. No independent human or proof-kernel review is claimed. diff --git a/docs/research/recursive-invoice-preflight.md b/docs/research/recursive-invoice-preflight.md index d55f6c136c..66c191936c 100644 --- a/docs/research/recursive-invoice-preflight.md +++ b/docs/research/recursive-invoice-preflight.md @@ -106,3 +106,8 @@ fail with a diagnostic and exit code 2. Valid comparisons, including inconclusiv ones, exit 0: this is an arithmetic report, not an automatic acceptance gate. Prepared for zxv with substantial OpenAI Codex assistance. Apache-2.0. + +For a difference whose uncancelled ranks are powers of one integer, the +[polynomial comparison](power-polynomial-comparison.md) classifies the power +cost on the whole open interval `0 < t < 1`. It distinguishes strict order, +interior equality and actual crossings without a numerical exponent grid. diff --git a/scripts/compare_power_polynomial.py b/scripts/compare_power_polynomial.py new file mode 100644 index 0000000000..54689dede4 --- /dev/null +++ b/scripts/compare_power_polynomial.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +"""Classify a common-base invoice difference for every power 0 < t < 1. + +Use classical Euclid and Sturm arithmetic over Q. No numerical roots are used. +The invoice interface retains zxv's comparator and predecessor credits. +Ali Romman with substantial OpenAI Codex assistance. See the linked proof guide. +""" + +import argparse +from fractions import Fraction as Q +from functools import reduce +import json +from math import gcd, lcm +from pathlib import Path +import runpy +import sys + +INVOICES = runpy.run_path(str(Path(__file__).with_name("compare_recursive_invoices.py"))) +require = INVOICES["require"] + + +def trim(poly): + values = list(poly) + while values and not values[-1]: + values.pop() + return tuple(values) + + +def primitive(poly): + """Remove positive rational content; retain the sign at every real point.""" + poly = trim(poly) + if not poly: + return () + denominator = lcm(*(Q(c).denominator for c in poly)) + integers = [int(c * denominator) for c in poly] + content = gcd(*integers) + return tuple(c // content for c in integers) + + +def divide(a, b): + a, b = list(map(Q, trim(a))), trim(b) + require(bool(b), "Polynomial division by zero") + quotient = [Q(0)] * max(0, len(a) - len(b) + 1) + while a and len(a) >= len(b): + offset, factor = len(a) - len(b), a[-1] / b[-1] + quotient[offset] = factor + for i, coefficient in enumerate(b): + a[offset + i] -= factor * coefficient + a = list(trim(a)) + return trim(quotient), trim(a) + + +def derivative(poly): + return trim(i * c for i, c in enumerate(poly) if i) + + +def value(poly, x): + result = Q(0) + for coefficient in reversed(poly): + result = result * x + coefficient + return result + + +def poly_gcd(a, b): + a, b = primitive(a), primitive(b) + while b: + a, b = b, primitive(divide(a, b)[1]) + return a + + +def variations(sequence, x): + signs = [1 if v > 0 else -1 for p in sequence if (v := value(p, x))] + return sum(a != b for a, b in zip(signs, signs[1:])) + + +def distinct_roots(poly, left, right): + """Count roots after square-free reduction; endpoints must not be roots.""" + common = poly_gcd(poly, derivative(poly)) + square_free, remainder = divide(poly, common) + require(not remainder, "Inexact square-free quotient") + square_free = primitive(square_free) + require(value(square_free, left) and value(square_free, right), "Root at Sturm endpoint") + sequence = [square_free] + if len(square_free) > 1: + sequence.append(derivative(square_free)) + while len(sequence[-1]) > 1: + remainder = divide(sequence[-2], sequence[-1])[1] + require(bool(remainder), "Unexpected repeated root after square-free reduction") + sequence.append(primitive(-c for c in remainder)) + a, b = variations(sequence, left), variations(sequence, right) + return {"square_free": list(square_free), "left_variations": a, + "right_variations": b, "distinct_roots": a - b} + + +def common_base(ranks): + """Find the primitive integer base of the first rank, then check all ranks.""" + number = next((r for r in ranks if r > 1), 2) + factors, divisor = {}, 2 + while divisor * divisor <= number: + while number % divisor == 0: + factors[divisor] = factors.get(divisor, 0) + 1 + number //= divisor + divisor = 3 if divisor == 2 else divisor + 2 + if number > 1: + factors[number] = 1 + divisor = gcd(*factors.values()) + base = reduce(lambda a, item: a * item[0] ** (item[1] // divisor), factors.items(), 1) + exponents = {} + for rank in ranks: + remaining, power = rank, 0 + while remaining > 1 and remaining % base == 0: + remaining //= base + power += 1 + require(remaining == 1, "Uncancelled ranks are not powers of one integer base") + exponents[rank] = power + return base, exponents + + +def classify_polynomial(poly, base): + """Keep endpoint zeros separate from the open-interval order.""" + original = trim(poly) + if not original: + return {"classification": "equal", "interior_roots": 0, "odd_interior_roots": 0, + "endpoint_multiplicities": {"1": 0, str(base): 0}, "root_counts": []} + current = primitive(original) + endpoints = {} + for endpoint in (1, base): + multiplicity = 0 + while not value(current, endpoint): + current, remainder = divide(current, (-endpoint, 1)) + require(not remainder, "Inexact endpoint quotient") + current = primitive(current) + multiplicity += 1 + endpoints[str(endpoint)] = multiplicity + counts = [] + while len(current) > 1: + counts.append(distinct_roots(current, 1, base)) + current = poly_gcd(current, derivative(current)) + roots = counts[0]["distinct_roots"] if counts else 0 + odd = sum((-1) ** j * row["distinct_roots"] for j, row in enumerate(counts)) + require(0 <= odd <= roots, "Invalid root multiplicity count") + # A degree-d nonzero polynomial cannot vanish at d+1 distinct sample points. + for denominator in range(2, len(original) + 2): + sample = 1 + Q(base - 1, denominator) + sample_value = value(original, sample) + if sample_value: + break + require(bool(sample_value), "No nonzero polynomial sample") + classification = ("crossing" if odd else + "nonnegative_with_equality" if roots and sample_value > 0 else + "nonpositive_with_equality" if roots else + "uniformly_higher" if sample_value > 0 else "uniformly_lower") + return {"classification": classification, "interior_roots": roots, + "odd_interior_roots": odd, "endpoint_multiplicities": endpoints, + "root_counts": counts, "sample": {"x": str(sample), "value": str(sample_value)}} + + +def compare(old, new): + invoice = INVOICES["compare_invoices"](old, new) + delta = {int(r): n for r, n in invoice["signed_net_delta"].items()} + base, exponents = common_base(delta) + poly = [0] * (max(exponents.values(), default=0) + 1) + for rank, count in delta.items(): + poly[exponents[rank]] = count + require(len(poly) <= 30, "Polynomial degree exceeds the invoice rank limit") + return {"schema": "integer-mult-power-polynomial/v1", "contract": invoice["contract"], + "quantity": invoice["quantity"], "signed_net_delta": invoice["signed_net_delta"], + "base": base, "polynomial_ascending": poly, "exponent_domain": "0 < t < 1", + "substitution": "x = base^t; 1 < x < base", **classify_polynomial(poly, base), + "scope": "Declared invoice arithmetic only. No circuit, ownership, compiler or kappa admission."} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("old") + parser.add_argument("new") + args = parser.parse_args() + try: + old, old_pin = INVOICES["load_invoice"](args.old) + new, new_pin = INVOICES["load_invoice"](args.new) + report = compare(old, new) + report["inputs"] = {"old": old_pin, "new": new_pin} + print(json.dumps(report, indent=2, sort_keys=True)) + except (OSError, ValueError, TypeError, RecursionError) as error: + print("Polynomial comparison failed: " + str(error), file=sys.stderr) + return 2 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_research_power_polynomial.py b/tests/test_research_power_polynomial.py new file mode 100644 index 0000000000..0adf7d3980 --- /dev/null +++ b/tests/test_research_power_polynomial.py @@ -0,0 +1,171 @@ +# SPDX-License-Identifier: Apache-2.0 +"""Independent range and factor oracles for uniform power invoice order. + +Ali Romman with substantial OpenAI Codex assistance. +""" + +from fractions import Fraction as Q +from hashlib import sha256 +import importlib.util +from itertools import product +import json +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts/compare_power_polynomial.py" +SPEC = importlib.util.spec_from_file_location("power_polynomial", SCRIPT) +M = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(M) + + +def invoice(paid=None, targets=None): + return {"schema": "integer-mult-recursive-invoice/v1", "contract": { + "model": "test-common-base", "rank_semantics": "r^t", + "ambient_rank": 10**9, "boundary": "same declared unit"}, + "paid": {str(r): n for r, n in (paid or {}).items()}, + "targets": {str(r): n for r, n in (targets or {}).items()}} + + +def times(a, b): + out = [0] * (len(a) + len(b) - 1) + for i, x in enumerate(a): + for j, y in enumerate(b): + out[i + j] += x * y + return out + + +class PowerPolynomialTests(unittest.TestCase): + def test_inconclusive_hinge_has_strict_uniform_power_order(self): + old, new = invoice({2: 2}), invoice({1: 1, 4: 1}) + self.assertEqual(M.INVOICES["compare_invoices"](old, new)["classification"], "inconclusive") + result = M.compare(old, new) + self.assertEqual(result["classification"], "uniformly_higher") + self.assertEqual(result["polynomial_ascending"], [1, -2, 1]) + self.assertEqual(result["endpoint_multiplicities"], {"1": 2, "2": 0}) + self.assertEqual(M.compare(new, old)["classification"], "uniformly_lower") + + def test_interior_equality_and_crossing_are_different(self): + for coefficients, expected, odd in (([9, -12, 4], "nonnegative_with_equality", 0), + ([-9, 12, -4], "nonpositive_with_equality", 0), + ([-3, 2], "crossing", 1)): + result = M.classify_polynomial(coefficients, 2) + self.assertEqual(result["classification"], expected) + self.assertEqual((result["interior_roots"], result["odd_interior_roots"]), (1, odd)) + + def test_all_343_small_quadratics_against_extrema_and_monotone_intervals(self): + for c, b, a in product(range(-3, 4), repeat=3): + poly = [c, b, a] + if not any(poly): + expected, roots = "equal", 0 + else: + # An independent quadratic range oracle uses the vertex and + # monotone pieces. It does not use a GCD or Sturm sequence. + points = [Q(1), Q(2)] + if a and 1 < Q(-b, 2 * a) < 2: + points.insert(1, Q(-b, 2 * a)) + values = [a * x * x + b * x + c for x in points] + roots = sum(v == 0 for v in values[1:-1]) + roots += sum(x * y < 0 for x, y in zip(values, values[1:])) + low, high = min(values), max(values) + expected = ("crossing" if low < 0 < high else + "nonnegative_with_equality" if low >= 0 and roots else + "nonpositive_with_equality" if high <= 0 and roots else + "uniformly_higher" if low >= 0 else "uniformly_lower") + result = M.classify_polynomial(poly, 2) + self.assertEqual((result["classification"], result["interior_roots"]), + (expected, roots), poly) + + def test_288_factored_polynomials_with_repeated_and_endpoint_roots(self): + for middle, left, right, nonreal in product(range(6), range(4), range(4), range(3)): + poly = [1] + for count, factor in ((middle, [-3, 2]), (left, [-1, 1]), + (right, [-2, 1]), (nonreal, [1, 0, 1])): + for _ in range(count): + poly = times(poly, factor) + expected = ("crossing" if middle % 2 else + "nonpositive_with_equality" if middle and right % 2 else + "nonnegative_with_equality" if middle else + "uniformly_lower" if right % 2 else "uniformly_higher") + result = M.classify_polynomial(poly, 2) + self.assertEqual(result["classification"], expected, (middle, left, right, nonreal)) + self.assertEqual(result["interior_roots"], int(middle > 0)) + self.assertEqual(result["odd_interior_roots"], middle % 2) + self.assertEqual(result["endpoint_multiplicities"], {"1": left, "2": right}) + + def test_irrational_roots_and_maximum_degree(self): + square = times([-3, 0, 1], [-3, 0, 1]) + result = M.classify_polynomial(square, 2) + self.assertEqual((result["classification"], result["interior_roots"]), + ("nonnegative_with_equality", 1)) + poly = [1] + for _ in range(29): + poly = times(poly, [-3, 2]) + old = invoice({2**i: -v for i, v in enumerate(poly) if v < 0}) + new = invoice({2**i: v for i, v in enumerate(poly) if v > 0}) + result = M.compare(old, new) + self.assertEqual((result["classification"], result["interior_roots"], + result["odd_interior_roots"]), ("crossing", 1, 1)) + + def test_common_base_targets_cancellation_and_large_coefficients(self): + self.assertEqual(M.common_base([1, 36, 216]), (6, {1: 0, 36: 2, 216: 3})) + self.assertEqual(M.common_base([16, 64])[0], 2) + with self.assertRaisesRegex(ValueError, "not powers"): + M.compare(invoice({2: 1}), invoice({3: 1})) + old = invoice({3: 10**60, 2: 2}, {4: 1}) + new = invoice({3: 10**60, 1: 1}) + self.assertEqual(M.compare(old, new)["classification"], "uniformly_higher") + self.assertEqual(M.compare(invoice({3: 1}), invoice({3: 1}))["classification"], "equal") + self.assertEqual(M.compare(invoice(), invoice({1: 10**60}))["classification"], "uniformly_higher") + + def test_real_cli_optimized_byte_binding_and_domain_failures(self): + with tempfile.TemporaryDirectory() as tmp: + old, new = Path(tmp) / "old.json", Path(tmp) / "new.json" + old.write_text(json.dumps(invoice({2: 2})), encoding="utf-8") + new.write_text(json.dumps(invoice({1: 1, 4: 1})), encoding="utf-8") + command = [sys.executable, "-I", "-O", "-B", str(SCRIPT), str(old), str(new)] + run = subprocess.run(command, text=True, capture_output=True) + self.assertEqual(run.returncode, 0, run.stderr) + report = json.loads(run.stdout) + self.assertEqual(report["classification"], "uniformly_higher") + self.assertEqual(report["inputs"]["old"]["sha256"], sha256(old.read_bytes()).hexdigest()) + for bad in (invoice({3: 1}), invoice({2: True}), dict(invoice(), schema="unsupported")): + new.write_text(json.dumps(bad), encoding="utf-8") + run = subprocess.run(command, text=True, capture_output=True) + self.assertEqual(run.returncode, 2) + self.assertEqual(run.stdout, "") + + def test_optional_independent_sympy_root_isolation(self): + try: + import sympy + except ImportError: + self.skipTest("Optional independent oracle: install sympy==1.14.0") + x = sympy.Symbol("x") + fixtures = [] + for seed in range(48): + degree = 29 if seed < 24 else 1 + seed % 15 + poly = [int.from_bytes(sha256(f"{seed}:{i}".encode()).digest(), "big") + % (2 * 10**60) - 10**60 for i in range(degree + 1)] + fixtures.append(poly) + for multiplicity in range(1, 17): + poly = [1] + for _ in range(multiplicity): + poly = times(poly, [-3, 2]) + fixtures.append(times(times(poly, [-1, 1]), [-2, 1])) + for poly in fixtures: + source = sympy.Poly(sum(c * x**i for i, c in enumerate(poly)), x) + intervals = source.intervals(inf=1, sup=2) + interior = [(ab, multiplicity) for ab, multiplicity in intervals + if ab not in ((1, 1), (2, 2))] + odd = sum(m % 2 for _, m in interior) + result = M.classify_polynomial(poly, 2) + self.assertEqual((result["interior_roots"], result["odd_interior_roots"]), + (len(interior), odd), poly) + self.assertEqual(result["classification"] == "crossing", odd > 0) + + +if __name__ == "__main__": + unittest.main()