Research source · python

check_relative_degree_exhaustive.py

site/public/research-artifacts/relative-degree-rational-series/check_relative_degree_exhaustive.py

177 lines. Source is displayed for inspection; it is not executed by this page.

File fingerprint

SHA-256: 1a1ced31f490916cc4bf2f2d5d7034d9a50fd040831b3bafa22f3f2ec3e6fc82

#!/usr/bin/env python3
"""Exhaustive exact check for the relative-degree criterion in dimensions 1 and 2.

The domain is every integer linear representation over {a,b} whose entries lie
in {-1,0,1}.  The direct oracle tests the definition using all suffix words of
length below d; this is exact because those columns span the reachable space.
"""

from fractions import Fraction
from itertools import product
import hashlib
import json
import sys


VALUES = (-1, 0, 1)


def dot(row, col):
    return sum(x * y for x, y in zip(row, col))


def row_times(row, matrix):
    d = len(row)
    return tuple(sum(row[i] * matrix[i][j] for i in range(d)) for j in range(d))


def matrix_times(matrix, col):
    return tuple(dot(row, col) for row in matrix)


def add_basis(basis, vector):
    """Append vector iff it increases row rank; dimensions here are at most two."""
    rows = [[Fraction(x) for x in row] for row in (*basis, vector)]
    rank = 0
    columns = len(vector)
    for column in range(columns):
        pivot = next((i for i in range(rank, len(rows)) if rows[i][column]), None)
        if pivot is None:
            continue
        rows[rank], rows[pivot] = rows[pivot], rows[rank]
        scale = rows[rank][column]
        rows[rank] = [x / scale for x in rows[rank]]
        for i in range(len(rows)):
            if i != rank and rows[i][column]:
                factor = rows[i][column]
                rows[i] = [x - factor * y for x, y in zip(rows[i], rows[rank])]
        rank += 1
    if rank > len(basis):
        basis.append(vector)
        return True
    return False


def reachable_basis(gamma, matrices):
    basis = []
    add_basis(basis, gamma)
    cursor = 0
    while cursor < len(basis):
        vector = basis[cursor]
        cursor += 1
        for matrix in matrices:
            add_basis(basis, matrix_times(matrix, vector))
    return basis


def safe_row(row, gamma, b_matrix, basis):
    row_b = row_times(row, b_matrix)
    return dot(row, gamma) == 0 and all(dot(row_b, col) == 0 for col in basis)


def krylov_algorithm(lam, gamma, a_matrix, b_matrix):
    d = len(lam)
    basis = reachable_basis(gamma, (a_matrix, b_matrix))
    row = lam
    earlier_safe = True
    for degree in range(d):
        if earlier_safe and dot(row_times(row, b_matrix), gamma) != 0:
            return degree
        earlier_safe = earlier_safe and safe_row(row, gamma, b_matrix, basis)
        row = row_times(row, a_matrix)
    return None


def suffix_columns(gamma, matrices, max_length):
    levels = [(gamma,)]
    for _ in range(max_length):
        levels.append(tuple(matrix_times(matrix, col) for col in levels[-1] for matrix in matrices))
    return tuple(col for level in levels for col in level)


def direct_candidates(lam, gamma, a_matrix, b_matrix):
    """Test the definition independently for every candidate 0 <= r < d."""
    d = len(lam)
    suffixes = suffix_columns(gamma, (a_matrix, b_matrix), d - 1)
    candidates = []
    row = lam
    earlier_ok = True
    for degree in range(d):
        witness = dot(row_times(row, b_matrix), gamma) != 0
        if earlier_ok and witness:
            candidates.append(degree)
        row_b = row_times(row, b_matrix)
        stop_zero = dot(row, gamma) == 0
        deviation_zero = all(dot(row_b, suffix) == 0 for suffix in suffixes)
        earlier_ok = earlier_ok and stop_zero and deviation_zero
        row = row_times(row, a_matrix)
    return tuple(candidates)


def matrices(d):
    for entries in product(VALUES, repeat=d * d):
        yield tuple(tuple(entries[i * d : (i + 1) * d]) for i in range(d))


def representations(d):
    vectors = tuple(product(VALUES, repeat=d))
    mats = tuple(matrices(d))
    for lam in vectors:
        for gamma in vectors:
            for a_matrix in mats:
                for b_matrix in mats:
                    yield lam, gamma, a_matrix, b_matrix


def run():
    by_dimension = {}
    total = 0
    degrees = {}
    transcript = hashlib.sha256()
    for d in (1, 2):
        count = 0
        with_degree = 0
        local_degrees = {str(r): 0 for r in range(d)}
        for index, representation in enumerate(representations(d)):
            algorithm = krylov_algorithm(*representation)
            direct = direct_candidates(*representation)
            if len(direct) > 1:
                raise AssertionError(("nonunique", d, index, direct, representation))
            expected = direct[0] if direct else None
            if algorithm != expected:
                raise AssertionError(("mismatch", d, index, algorithm, direct, representation))
            transcript.update(f"{d}:{index}:{algorithm}\n".encode())
            count += 1
            if algorithm is not None:
                with_degree += 1
                local_degrees[str(algorithm)] += 1
        by_dimension[str(d)] = {
            "representations": count,
            "with_relative_degree": with_degree,
            "degree_counts": local_degrees,
        }
        total += count
        degrees.update({f"d{d}_r{r}": n for r, n in local_degrees.items()})

    return {
        "protocol": "fprd-relative-degree-exhaustive.v1",
        "domain": {
            "dimensions": [1, 2],
            "entry_set": list(VALUES),
            "alphabet": ["a", "b"],
            "enumeration": "all lambda, gamma, M_a, M_b over the entry set",
        },
        "oracle": "direct definition test for every r<d using every suffix word of length <d",
        "result": "pass",
        "total_representations": total,
        "by_dimension": by_dimension,
        "degree_counts": degrees,
        "mismatches": 0,
        "nonunique_candidate_sets": 0,
        "transcript_sha256": transcript.hexdigest(),
        "python": sys.version.split()[0],
    }


if __name__ == "__main__":
    print(json.dumps(run(), indent=2, sort_keys=True))