05 — Resolutions¶
The mathematics¶
Hochschild homology is Tor over the enveloping algebra: HH_n(A) = Tor_n^{A^e}(A, A), where A^e = A ⊗ A^op. To compute it you need a projective resolution of A as an A^e-bimodule, apply A ⊗_{A^e} (−), and take homology. Any resolution gives the same answer, so the game is to use the smallest one that a given algebra admits. quiverlab ships several: the bar complex (Chapter 04, huge but universal), the minimal A^e-resolution built by syzygies (works for every algebra, smallest possible terms), and Bardzell's resolution (closed-form and minimal for monomial algebras, reaching enormous depth).
How it is represented — the Resolution protocol¶
A resolution backend is a class implementing a fixed interface Resolution
(engine/resolutions.py). The two primitives every homology backend must expose:
term_basis(alg, n)— an ordered list of hashable generators of the n-th term; its length is dim P_n;differential_matrix(alg, n, basis_n, index_nm1)— the matrix of d_n as a numpy int64 array (a fixed-width-integer matrix) of shape(dim P_{n-1}, dim P_n).
Two firm contract rules: matrices are integer, never pre-reduced mod p (a small prime
p dividing an entry carries genuine signal — torsion — that reducing early would erase);
and the differential is expressed in exactly the ordering that term_basis returned. Two
more primitives (cochain_basis, coboundary_matrix) serve cohomology, and two
(sigma_chain_matrix, sigma_cochain_matrix) serve the Nakayama-action layer of Chapter
06. BarResolution is the reference backend that delegates to the bar-complex code and is
the oracle every other backend is checked against.
The minimal A^e-resolution (the syzygy stepper)¶
engine/resolutions_minimal.py builds the minimal projective bimodule resolution one
degree at a time, valid for any finite-dimensional path-basis A given by its structure
constants. The engine AeEngine precomputes, over F_p, the m^2 × m^2 matrices of
left-multiplication by each A^e basis element e_a ⊗ e_b (using
(e_a ⊗ e_b)·(e_p ⊗ e_q) = (e_a e_p) ⊗ (e_q e_b)).
Local vs multi-vertex terms (Plan 13). For a local algebra projective = free, so the
terms are free: ... -> (A^e)^{r_n} -> ... -> A^e -> A -> 0 (the original,
kernel-accelerated path). Over a multi-vertex algebra a minimal free resolution does
not exist — ker(A^e ↠ A) contains whole off-diagonal corner projectives Ae_v ⊗ e_wA, and
free covers of projectives spawn projective junk forever — so the engine builds
corner-typed terms P_n = ⊕_j A^e·(ε_{v_j} ⊗ ε_{w_j}) (_CornerContext, pure
Python): kernels are computed over corner coordinates, generator candidates are the
corner components (ε_i ⊗ ε_j)·k of kernel vectors, and each generator carries its corner
tag. The vertex idempotents are read off the unit's 1-coordinates (validated against T);
a nilpotent-closure guard in radical_basis refuses non-path-type bases loudly instead
of returning a silently wrong resolution (the pre-Plan-13 multi-vertex failure mode).
Validation: HH ≡ bar on kA_2 / the commutative square / kZ_3/rad², and on the monomial
line quiver kQ/(abc, cde) the corner Betti numbers equal Bardzell's chain counts
6, 5, 2, 1, 0 — with the 1 the straddling overlap abcde (an independent syzygy-side
re-derivation of the Plan-12 chain).
_advance_resolution computes the next degree n from the current differential cur:
- Syzygies.
nullspace_mod_p(cur, p)findsker= a basis of the kernel of the current map over F_p (these are the relations among the previous generators). If the kernel is empty the resolution terminates — A has finite Hochschild dimension. - Minimal generators. The kernel must be cut down to generators modulo the radical
of A^e (minimality).
_build_radKforms rad(A^e)·ker (the non-minimal part), and_independent_modulogreedily keeps exactly the kernel vectors that are independent of it — via one incremental row-reduction, not repeated rank tests. Those chosen vectorsgensare the columns of d_n and their count is r_n. rad(A) is the span of the non-idempotent basis vectors (vertex idempotents read off the unit; for a local algebra this degenerates to "all non-unit basis vectors") — computed characteristic-independently byradical_basis, with a nilpotent-closure guard. - Assemble d_n.
_build_Dnwrites each generator into a column block, giving the newcur; the loop repeats.
Then _contracted_complex applies A ⊗{A^e} (−) to every degree, and
minimal_homology_dims reads off dim HH_n = dim(A ⊗ P_n) − rank(dbar_n) − rank(dbar{n+1}).
Cohomology from the same resolution (Plan 16). minimal_cohomology_dims applies
Hom_{A^e}(−, A) instead: Hom_{A^e}((A^e)^{r_n}, A) ≅ A^{r_n}, and the coboundary
δ^{n−1} is precomposition with d_n, so a differential coefficient at e_u ⊗ e_v acts
two-sidedly the other way round than the homology collapse — α ↦ e_u·α·e_v (in the
Chouhy–Solotar code's vocabulary, a·w·b where homology is b·w·a). On the corner path
the cochain block of a generator tagged (v, w) is e_v A e_w — the opposite corner
of the homology target e_w A e_v, so the corner-basis dictionary is read with the tag
swapped (_corner_cohomology_degree; kA₂ makes the swap load-bearing: its coh corner is
1-dimensional exactly where the homology corner is 0). dim HH^n = dim C^n − rank δ^n −
rank δ^{n−1}, with the same per-prime rebuild and the same truncation rule (exact through
truncated_at − 1, since δ^t needs the unknown d_{t+1}). This gives deep HH^• for any
finite-dimensional algebra and a second deep oracle degreewise against the CS
resolution's side="coh" collapse.
Guards. The host is small, so two budgets stop the build gracefully instead of being
killed by the operating system: max_term_dim caps a term's k-dimension m^2·r_n, and
max_transient_bytes predicts the size of the large transient radK array before
allocating it and stops if it would blow the budget. Either way truncated_at records the
last fully-known degree, and the returned dimensions are exact up to truncated_at − 1.
Because a minimal resolution depends on the characteristic, the whole thing is rebuilt per
prime; the default large prime 32003 is the faithful char-0 proxy and small primes expose
torsion.
Checkpointed deepening¶
engine/deepen.py drives the same stepper for very deep runs on a cluster, one degree per
step, writing an atomic checkpoint after every degree so a timeout or node failure
costs at most one degree of recompute. A checkpoint is a pickle file (Python's binary
save format) whose payload is a dict containing literally: n (the last completed degree),
cur and cur_r (the current differential and its generator count), rks (the r_n so
far), last_gens (the previous degree's generator columns, needed to finalize the next
HH), HH (the dimensions computed so far), and per_degree (per-degree timing and memory
records). Multi-vertex algebras (the corner-typed Plan-13 stepper) checkpoint one extra
key: tags, the per-degree corner labels — every other piece of corner data
(_CornerContext, gens0, rad_ab_pairs) is deterministic from (A, prime) and is
rebuilt on resume, never pickled. A checkpoint directory belongs to one (algebra, prime)
run: resuming a corner algebra against a local checkpoint (or vice versa) refuses loudly
with QuiverlabError. Resuming reads the latest checkpoint and rebuilds only the cheap
per-(A, prime) state via _init_resolution. A
latest.txt pointer only ever advances (a stale second writer cannot regress it), and
deepen predicts the next degree's walltime as a multiple of the last one's, stopping
before a degree that would overrun the time budget rather than being SIGKILL'd mid-degree
with a stale result. finalize_only=True re-emits the summary from the newest checkpoint —
the recovery path when a job died before writing its JSON.
The Bardzell resolution (monomial algebras)¶
engine/resolutions_bardzell.py implements Bardzell's minimal projective bimodule
resolution for a monomial algebra kQ/I, exposed as a Resolution backend over a
MonomialPresentation (vertices, arrows as (id, source, target) tuples, relations as
path tuples). Its combinatorics are the graded associated paths AP^n (equivalently
Anick chains / n-ambiguities):
- AP^0 = vertices, AP^1 = arrows, AP^2 = the minimal relations;
- AP^n (n ≥ 2) = paths admitting a unique left decomposition p = u_0 u_1 ... u_{n−1}
(u_0 a single arrow): each next block is the shortest extension making the consecutive
pair reducible — the witness relation ends at the pair's end and straddles the block
boundary; with mixed-length relations it can be a proper suffix of the pair rather than
the pair itself (CS §3 / Anick minimality, corrected in Plan 12). Built recursively by
associated_paths. Example, relations{xx, yy, xyx}:xyxx = (x)(yx)(x)— the pairyx·xis not a relation but containsxx; the exact-pair shortcut missed this chain (andxyxyx), giving AP^3 = 3 instead of the true 5 = the minimal-A^e Betti number. - every associated path also has a unique right decomposition (
right_decomposition, last block a single arrow, mirror-greedy): same set (CS §3 Prop.), different blocks — forxyxxit is(xy)(x)(x).
The n-th term is P_n = ⊕{p ∈ AP^n} A e{o(p)} ⊗ e_{t(p)} A; after A ⊗{A^e} (−) each
summand collapses to the loop space e{o(p)} A e_{t(p)} (paths closing p), so a basis
element of the contracted term is a pair (p, w) — an associated path and a nonzero path
closing it (term_basis, loops). The differential differential_matrix alternates
between Bardzell's "big" map (even n: sum over (n−1)-associated subpaths) and "small" map
(odd n ≥ 3: (+1) drop the leftmost right-block v_top, (−1) drop the rightmost
left-block — CS §4 f_n even; using u_0 in the first slot is correct only for
quadratic/palindromic relation sets), with n = 1 the commutator/augmentation map.
Because the resolution is minimal and its terms are the small associated-path sets rather
than the bar complex's m·(m−1)^n, it runs far past the bar wall — the bank record is exact
homology to degree 1702 (a hanlab-bank record; not reproduced or verified in this
port). It is cross-checked entry-exact against the bar oracle on
k[x]/(x^a), cyclic Nakayama algebras, the radical-square-zero families, and (Plan 12)
the straddling-overlap presentation k⟨x,y⟩/(xx, yy, xyx) against bar and the
minimal-A^e engine simultaneously.
The periodicity detector, and eventually-periodic families¶
For self-injective and other eventually-periodic algebras the HH sequence eventually
repeats. Two homology-only family backends in engine/resolutions_periodic.py give a
caller the small resolution by name: CyclicNakayamaResolution(n, ell) (a thin verified
wrapper over Bardzell) and QuantumCIResolution(c) (a wrapper over the Chouhy–Solotar
closed form — dormant in this port; it lands with Plan 04). Separately, once a
sequence of dimensions is in hand, complexity_diagnostic (engine/scan2.py) is the
detector: it takes finite differences to spot polynomial growth, checks for trailing
zeros (eventual vanishing, the Han-counterexample signature, complexity 0), and tests for
eventual periodicity seq[i] == seq[i−p] returning a label like eventually_periodic_p2.
It is honest — it reports only what the computed degrees show, never a proof of the tail.
The fast GF(p) engine, and its pure-Python fallback¶
Over a prime field the engine replaces exact-domain rref with numpy int64 matrices and
rank mod p. rank_mod_p (engine/hh_engine.py) is dense Gaussian elimination over F_p
(modular inverse via pow(a, p−2, p)); sparse_rank_mod_p (engine/linalg_fast.py) does
the same on a dictionary-of-columns for the very sparse deep differentials, and
rank_mod_p_auto dispatches to whichever fits. The genuinely hot loops (nullspace, rank,
the radK and Dn matvecs — matrix-times-vector products) also have numba-compiled kernels (engine/_kernels.py) —
just-in-time-compiled machine code. The flag USE_KERNELS is true only when numba is
importable and not disabled; setting the environment variable QUIVERLAB_NO_NUMBA=1
forces the pure-Python twins. Those twins are not just a fallback — they are the permanent
oracle, and a parity test runs both. (Overflow is controlled: the matvec kernels
accumulate up to m^2 products before one reduction, safe while m^2·(p−1)^2 < 2^63, which
holds comfortably for p = 32003 at the target dimensions.)
The adapter — public Algebra into engine form¶
The engine speaks numpy int64, the public Algebra speaks field elements. to_engine
(engine/adapter.py) bridges them: it refuses any non-prime field loudly (a
FieldError — the fast engine is a GF(p) accelerator, the bar path serves all fields),
then extracts integer coefficients by looping T[i, j, t] = int(vec[t]) over the public
table and int(c) over the unit, producing an engine/hh_engine.py Algebra (which
additionally changes to its own unit-adapted basis so that A/k.1 is "drop coordinate t").
engine_cohomology_dims / engine_homology_dims guard the exponential bar-basis size
against max_cells exactly as the pure oracle does, then return plain list[int].
A worked micro-example — a Bardzell term for k[x]/(x^3)¶
MonomialPresentation.truncated_polynomial(3) has one vertex "v", one loop arrow 0,
and the single relation (0, 0, 0) = x^3. Running it: AP^1 = [(0,)] (the arrow x),
AP^2 = [(0, 0, 0)] (the relation x^3), AP^3 = [(0,0,0,0)]. The contracted degree-0
term basis is [(('v','v'), ()), (('v','v'), (0,)), (('v','v'), (0,0))] — the vertex loop
paired with the loops 1, x, x^2 closing it (dim 3 = dim A). Each higher term keeps this
constant size, which is exactly why Bardzell reached degree 1702 in the hanlab bank where the bar complex —
dim C_n = 3·2^n — chokes. (These bases were produced by running the code.)
Where to look in the code¶
| concept | file | function / class |
|---|---|---|
| the resolution interface | engine/resolutions.py |
Resolution, BarResolution, _default |
| minimal A^e engine, syzygy step | engine/resolutions_minimal.py |
AeEngine, _advance_resolution, minimal_homology_dims |
| minimal-resolution guards | engine/resolutions_minimal.py |
minimal_resolution (max_term_dim, max_transient_bytes) |
| checkpointed deepening | engine/deepen.py |
deepen, _save_ckpt, _load_ckpt |
| Bardzell resolution, associated paths | engine/resolutions_bardzell.py |
BardzellResolution, MonomialPresentation.associated_paths |
| eventually-periodic family backends | engine/resolutions_periodic.py |
CyclicNakayamaResolution, QuantumCIResolution |
| periodicity / complexity detector | engine/scan2.py, engine/scan3.py |
complexity_diagnostic, complexity_of |
| fast rank over F_p | engine/hh_engine.py, engine/linalg_fast.py |
rank_mod_p, sparse_rank_mod_p, rank_mod_p_auto |
| numba kernels + fallback flag | engine/_kernels.py |
USE_KERNELS, rank_mod_p_kernel, nullspace_kernel |
| public Algebra -> engine | engine/adapter.py |
to_engine, engine_cohomology_dims, engine_homology_dims |