Skip to content

coxeter_spectral

quiverlab.invariants.coxeter_spectral

Plan 58 (record R20): the certified Coxeter spectral report on an Algebra.

Assembles the shipped, exact spectral primitives -- coxeter_polynomial / coxeter_matrix (invariants/cartan.py), spectral_radius / mahler_measure / is_cyclotomic_product and the Plan-58 certify_real_algebraic / cyclotomic_factorization / off_circle_root_count helpers (invariants/spectral.py) -- into a single report:

  • the exact ZZ[x] cyclotomic factorization with Phi_n labels;
  • a cyclotomic / quasi_unipotent verdict and the finite coxeter_order (Phi^m = I, verified by EXACT matrix power) or None with an honest reason;
  • the exact count of roots outside the unit circle;
  • the spectral radius rho and Mahler measure M as CERTIFIED ALGEBRAIC NUMBERS (minimal polynomial + rational isolating interval + root index) -- never a float -- OR a loud per-field refusal on a complex-dominant spectrum;
  • the class-conditional Lehmer note (documentation only).

P58 adds NO new root-finding or spectral machinery: it is a certification + labelling + assembly layer. See docs/plans/2026-08-07-plan-58-coxeter-spectral.md.

THE COMPLEX-DOMINANT GATE (§D3, load-bearing). rho / M are always non-negative reals, EXACTLY computed by the shipped spectral_radius / mahler_measure (sound for complex off-circle roots). But sympy.minimal_polynomial HANGS (~121 s, measured on a dim-5 algebra) -- it does NOT raise -- on the complex-modulus form sqrt(CRootOf*CRootOf) that a complex-dominant spectrum yields, so an exception-based guard never fires and the offline tier (no wall-clock kill) hangs on a SMALL quiver. Instead we decide real-dominance up front, deterministically and cheaply (~0.4 ms), with the shipped predicate _real_roots_suffice(_noncyclotomic_part(chi)), and only then call certify_real_algebraic. All-cyclotomic short-circuits to rho = M = 1. The outside-circle count is reported in BOTH branches.

coxeter_spectral

coxeter_spectral(A) -> dict

The certified Coxeter spectral report dict (no kind/references/ citations/scope/lehmer_class_note -- the raw analysis; see :func:coxeter_spectral_block for the runner block). Any field that raises on this input (singular / absent Cartan -> coxeter_polynomial; complex-dominant or otherwise uncertifiable rho / M -> §D3) is captured as {"error": <the loud message>} per field -- never a crash, never a silent default (the Plan-30 tau-block / P43 fingerprint precedent).

Source code in src/quiverlab/invariants/coxeter_spectral.py
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
def coxeter_spectral(A) -> dict:
    """The certified Coxeter spectral report dict (no ``kind``/``references``/
    ``citations``/``scope``/``lehmer_class_note`` -- the raw analysis; see
    :func:`coxeter_spectral_block` for the runner block). Any field that raises on
    this input (singular / absent Cartan -> ``coxeter_polynomial``; complex-dominant
    or otherwise uncertifiable ``rho`` / ``M`` -> §D3) is captured as
    ``{"error": <the loud message>}`` per field -- never a crash, never a silent
    default (the Plan-30 tau-block / P43 fingerprint precedent)."""
    from quiverlab.invariants.spectral import (
        spectral_radius, mahler_measure, certify_real_algebraic,
        cyclotomic_factorization, off_circle_root_count,
        _noncyclotomic_part, _real_roots_suffice)
    from quiverlab.engine.coxeter_spectrum import is_cyclotomic_product

    report = {}
    # 1. Coxeter polynomial (guarded: singular / absent Cartan refuses loudly).
    try:
        chi = A.coxeter_polynomial().as_expr()
    except QuiverlabError as exc:
        msg = str(exc)
        for key in ("coxeter_polynomial", "coxeter_latex", "cyclotomic",
                    "factorization", "quasi_unipotent", "coxeter_order",
                    "coxeter_order_reason", "outside_unit_circle_count",
                    "spectral_radius", "mahler_measure"):
            report[key] = {"error": msg}
        return report
    report["coxeter_polynomial"] = str(chi)
    report["coxeter_latex"] = sp.latex(chi)

    # 2. cyclotomic verdict + Phi_n-labelled factorization.
    cyclotomic = bool(is_cyclotomic_product(chi))
    report["cyclotomic"] = cyclotomic
    report["quasi_unipotent"] = cyclotomic          # eigenvalue-level; n&s from factors
    report["factorization"] = cyclotomic_factorization(chi)

    # 3. outside-unit-circle root count (always reported, exact).
    report["outside_unit_circle_count"] = off_circle_root_count(chi)

    # 4. rho + M via the deterministic real-dominant gate (§D3).
    q = _noncyclotomic_part(chi)
    if q is None or _real_roots_suffice(q):
        # all-cyclotomic (rho = M = 1) or real-dominant: minimal_polynomial is
        # sub-second; certify. (certify_real_algebraic is guarded per field.)
        for key, alpha in (("spectral_radius", spectral_radius(chi)),
                           ("mahler_measure", mahler_measure(chi))):
            try:
                report[key] = certify_real_algebraic(alpha)
            except QuiverlabError as exc:
                report[key] = {"error": str(exc)}
    else:
        report["spectral_radius"] = {"error": _COMPLEX_DOMINANT_MSG}
        report["mahler_measure"] = {"error": _COMPLEX_DOMINANT_MSG}

    # 5. finite Coxeter order Phi^m = I (§D1): lcm of the recognised indices, VERIFIED
    #    by an exact matrix power (never is_diagonalizable).
    if not cyclotomic:
        report["coxeter_order"] = None
        report["coxeter_order_reason"] = (
            "not quasi-unipotent: some eigenvalue has modulus > 1 (ρ > 1), so the "
            "Coxeter transformation has infinite order")
    else:
        ns = [f["cyclotomic_index"] for f in report["factorization"]
              if isinstance(f.get("cyclotomic_index"), int)]
        m = math.lcm(*ns) if ns else 1
        try:
            Phi = sp.Matrix(A.coxeter_matrix())
            if Phi**m == sp.eye(Phi.rows):
                report["coxeter_order"] = m
                report["coxeter_order_reason"] = (
                    "Φ^%d = I (finite order %d; minimal by construction as the lcm "
                    "of the cyclotomic indices)" % (m, m))
            else:
                report["coxeter_order"] = None
                report["coxeter_order_reason"] = (
                    "quasi-unipotent but the Coxeter transformation has a nontrivial "
                    "Jordan block ⇒ infinite order (typical of tame/affine type)")
        except QuiverlabError as exc:
            report["coxeter_order"] = {"error": str(exc)}
            report["coxeter_order_reason"] = {"error": str(exc)}
    return report

coxeter_spectral_block

coxeter_spectral_block(A) -> dict

The no-code coxeter_spectral compute block (schema v1): the :func:coxeter_spectral report plus {"kind", "references", "scope", "lehmer_class_note", "latex"}. SHARED by both runners so their blocks are byte-identical; each runner adds citations from references.

Source code in src/quiverlab/invariants/coxeter_spectral.py
146
147
148
149
150
151
152
153
154
155
156
157
def coxeter_spectral_block(A) -> dict:
    """The no-code ``coxeter_spectral`` compute block (schema v1): the
    :func:`coxeter_spectral` report plus ``{"kind", "references", "scope",
    "lehmer_class_note", "latex"}``. SHARED by both runners so their blocks are
    byte-identical; each runner adds ``citations`` from ``references``."""
    block = dict(coxeter_spectral(A))
    block["kind"] = "coxeter_spectral"
    block["references"] = list(_REFERENCES)
    block["scope"] = _SCOPE
    block["lehmer_class_note"] = _LEHMER_NOTE
    block["latex"] = _summary_latex(block)
    return block