quiverlab 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. quiverlab/__init__.py +42 -0
  2. quiverlab/batch/__init__.py +20 -0
  3. quiverlab/batch/builders.py +68 -0
  4. quiverlab/batch/db.py +157 -0
  5. quiverlab/batch/scan.py +257 -0
  6. quiverlab/citations/__init__.py +5 -0
  7. quiverlab/citations/bibliography.py +125 -0
  8. quiverlab/citations/references.bib +420 -0
  9. quiverlab/citations/registry.py +275 -0
  10. quiverlab/combinat/__init__.py +2 -0
  11. quiverlab/combinat/quiver.py +108 -0
  12. quiverlab/combinat/relations.py +199 -0
  13. quiverlab/core/__init__.py +2 -0
  14. quiverlab/core/algebra.py +678 -0
  15. quiverlab/core/monomial.py +158 -0
  16. quiverlab/engine/__init__.py +13 -0
  17. quiverlab/engine/_kernels.py +276 -0
  18. quiverlab/engine/adapter.py +84 -0
  19. quiverlab/engine/bimodule.py +297 -0
  20. quiverlab/engine/coxeter.py +373 -0
  21. quiverlab/engine/coxeter2.py +241 -0
  22. quiverlab/engine/coxeter_spectrum.py +259 -0
  23. quiverlab/engine/cyclic.py +271 -0
  24. quiverlab/engine/deepen.py +236 -0
  25. quiverlab/engine/hh_engine.py +323 -0
  26. quiverlab/engine/linalg_fast.py +102 -0
  27. quiverlab/engine/resolutions.py +112 -0
  28. quiverlab/engine/resolutions_bardzell.py +472 -0
  29. quiverlab/engine/resolutions_minimal.py +1029 -0
  30. quiverlab/engine/resolutions_periodic.py +96 -0
  31. quiverlab/engine/scan2.py +430 -0
  32. quiverlab/engine/scan3.py +136 -0
  33. quiverlab/engine/tt_calculus.py +339 -0
  34. quiverlab/errors.py +40 -0
  35. quiverlab/families/__init__.py +16 -0
  36. quiverlab/families/basic.py +22 -0
  37. quiverlab/families/discover.py +79 -0
  38. quiverlab/families/dynkin.py +58 -0
  39. quiverlab/families/exterior.py +16 -0
  40. quiverlab/families/incidence.py +35 -0
  41. quiverlab/families/nakayama.py +70 -0
  42. quiverlab/families/path_algebra.py +13 -0
  43. quiverlab/families/poset.py +51 -0
  44. quiverlab/families/preprojective.py +116 -0
  45. quiverlab/families/quantum.py +67 -0
  46. quiverlab/families/radical_square_zero.py +9 -0
  47. quiverlab/families/tensor.py +52 -0
  48. quiverlab/families/trivial_extension.py +327 -0
  49. quiverlab/families/truncated.py +30 -0
  50. quiverlab/families/zoo.py +72 -0
  51. quiverlab/families/zoo_catalog.json +6280 -0
  52. quiverlab/fields/__init__.py +6 -0
  53. quiverlab/fields/complexfield.py +137 -0
  54. quiverlab/fields/conway.py +32 -0
  55. quiverlab/fields/domain.py +132 -0
  56. quiverlab/fields/finitefield.py +149 -0
  57. quiverlab/fields/gaussian.py +50 -0
  58. quiverlab/fields/linalg.py +87 -0
  59. quiverlab/fields/primefield.py +51 -0
  60. quiverlab/fields/rationals.py +38 -0
  61. quiverlab/groebner/__init__.py +13 -0
  62. quiverlab/groebner/certificate.py +42 -0
  63. quiverlab/groebner/complete.py +80 -0
  64. quiverlab/groebner/events.py +26 -0
  65. quiverlab/groebner/lower.py +52 -0
  66. quiverlab/groebner/order.py +74 -0
  67. quiverlab/groebner/overlap.py +63 -0
  68. quiverlab/groebner/reduction.py +99 -0
  69. quiverlab/groebner/system.py +90 -0
  70. quiverlab/hochschild/__init__.py +3 -0
  71. quiverlab/hochschild/bar.py +152 -0
  72. quiverlab/hochschild/basis_reps.py +165 -0
  73. quiverlab/hochschild/cyclic.py +165 -0
  74. quiverlab/hochschild/cyclic_reps.py +174 -0
  75. quiverlab/hochschild/hh_reps.py +193 -0
  76. quiverlab/hochschild/products.py +454 -0
  77. quiverlab/hochschild/table.py +31 -0
  78. quiverlab/hpc/__init__.py +24 -0
  79. quiverlab/hpc/__main__.py +8 -0
  80. quiverlab/hpc/cli.py +456 -0
  81. quiverlab/hpc/deepen_runner.py +62 -0
  82. quiverlab/hpc/report.py +682 -0
  83. quiverlab/hpc/resources.py +296 -0
  84. quiverlab/hpc/spec.py +1745 -0
  85. quiverlab/invariants/__init__.py +1 -0
  86. quiverlab/invariants/betti.py +71 -0
  87. quiverlab/invariants/cartan.py +71 -0
  88. quiverlab/invariants/frobenius.py +314 -0
  89. quiverlab/invariants/pathbasis.py +58 -0
  90. quiverlab/invariants/scalar.py +119 -0
  91. quiverlab/invariants/spectral.py +149 -0
  92. quiverlab/invariants/sweep.py +65 -0
  93. quiverlab/modules/__init__.py +6 -0
  94. quiverlab/modules/builders.py +92 -0
  95. quiverlab/modules/complex_reps.py +542 -0
  96. quiverlab/modules/decompose.py +396 -0
  97. quiverlab/modules/duality.py +117 -0
  98. quiverlab/modules/ext.py +115 -0
  99. quiverlab/modules/ext_algebra.py +666 -0
  100. quiverlab/modules/hom.py +212 -0
  101. quiverlab/modules/injective.py +83 -0
  102. quiverlab/modules/koszul.py +505 -0
  103. quiverlab/modules/linalg_mod.py +139 -0
  104. quiverlab/modules/module.py +327 -0
  105. quiverlab/modules/opposite.py +67 -0
  106. quiverlab/modules/qpa_module.py +186 -0
  107. quiverlab/modules/radtopsoc.py +121 -0
  108. quiverlab/modules/resolution.py +172 -0
  109. quiverlab/modules/tor.py +181 -0
  110. quiverlab/modules/yoneda.py +373 -0
  111. quiverlab/qpa/__init__.py +7 -0
  112. quiverlab/qpa/crosscheck.py +377 -0
  113. quiverlab/qpa/scripts.py +262 -0
  114. quiverlab/qpa/session.py +108 -0
  115. quiverlab/resolutions_cs/__init__.py +33 -0
  116. quiverlab/resolutions_cs/_fieldshim.py +22 -0
  117. quiverlab/resolutions_cs/aarith.py +49 -0
  118. quiverlab/resolutions_cs/ambiguities.py +59 -0
  119. quiverlab/resolutions_cs/build.py +17 -0
  120. quiverlab/resolutions_cs/cap.py +108 -0
  121. quiverlab/resolutions_cs/comparison.py +833 -0
  122. quiverlab/resolutions_cs/cup.py +106 -0
  123. quiverlab/resolutions_cs/diagonal.py +325 -0
  124. quiverlab/resolutions_cs/engine_facade.py +79 -0
  125. quiverlab/resolutions_cs/homology.py +173 -0
  126. quiverlab/resolutions_cs/pelt.py +69 -0
  127. quiverlab/resolutions_cs/products.py +113 -0
  128. quiverlab/resolutions_cs/resolution.py +247 -0
  129. quiverlab/resolutions_cs/terms.py +19 -0
  130. quiverlab/resolutions_cs/trace.py +42 -0
  131. quiverlab/trace/__init__.py +17 -0
  132. quiverlab/trace/events.py +249 -0
  133. quiverlab/trace/interpretations.py +292 -0
  134. quiverlab/trace/json_guide.py +221 -0
  135. quiverlab/trace/modules.py +876 -0
  136. quiverlab/trace/products.py +619 -0
  137. quiverlab/trace/provenance.py +88 -0
  138. quiverlab/trace/recorder.py +163 -0
  139. quiverlab/trace/render_html.py +2200 -0
  140. quiverlab/trace/render_json.py +123 -0
  141. quiverlab/trace/render_text.py +286 -0
  142. quiverlab/trace/results_html.py +661 -0
  143. quiverlab/trace/writer.py +176 -0
  144. quiverlab/viz/__init__.py +16 -0
  145. quiverlab/viz/draw.py +77 -0
  146. quiverlab/viz/layout.py +159 -0
  147. quiverlab/viz/tikz.py +40 -0
  148. quiverlab-0.1.0.dist-info/METADATA +603 -0
  149. quiverlab-0.1.0.dist-info/RECORD +153 -0
  150. quiverlab-0.1.0.dist-info/WHEEL +5 -0
  151. quiverlab-0.1.0.dist-info/entry_points.txt +2 -0
  152. quiverlab-0.1.0.dist-info/licenses/LICENSE +21 -0
  153. quiverlab-0.1.0.dist-info/top_level.txt +1 -0
quiverlab/__init__.py ADDED
@@ -0,0 +1,42 @@
1
+ """quiverlab: quivers with relations and Hochschild theory, exactly."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ # Worked-steps traces are ON by default (spec D9). Flip per-call via
6
+ # A.hochschild_cohomology(..., verbose=False) or globally via quiverlab.verbose.
7
+ verbose = True
8
+
9
+ from quiverlab.errors import ( # noqa: E402,F401
10
+ QuiverlabError, ExactnessError, FieldError, RelationError,
11
+ AdmissibilityError, NotFiniteDimensionalError, DepthLimitError,
12
+ QpaUnavailableError,
13
+ )
14
+ from quiverlab.fields import GF # noqa: E402,F401
15
+ from quiverlab.fields import CC, E # noqa: E402,F401
16
+ from quiverlab.fields import QQi # noqa: E402,F401
17
+ from quiverlab.combinat import Quiver # noqa: E402,F401
18
+ from quiverlab.core import Algebra # noqa: E402,F401
19
+ from quiverlab.families import linear_path_algebra, truncated_polynomial # noqa: E402,F401
20
+ from quiverlab.families import ( # noqa: E402,F401
21
+ NakayamaAlgebra, PathAlgebra, TruncatedPathAlgebra, RadicalSquareZero,
22
+ IncidenceAlgebra, QuantumCI, ExteriorAlgebra, PreprojectiveAlgebra,
23
+ TrivialExtension, TensorProduct, zoo, families,
24
+ )
25
+ from quiverlab.citations import bibliography # noqa: E402,F401
26
+ from quiverlab.invariants.sweep import sweep # noqa: E402,F401
27
+
28
+ __all__ = [
29
+ "__version__",
30
+ "verbose",
31
+ "QuiverlabError", "ExactnessError", "FieldError", "RelationError",
32
+ "AdmissibilityError", "NotFiniteDimensionalError", "DepthLimitError",
33
+ "QpaUnavailableError",
34
+ "GF", "CC", "E", "QQi",
35
+ "Quiver", "Algebra",
36
+ "truncated_polynomial", "linear_path_algebra",
37
+ "NakayamaAlgebra", "PathAlgebra", "TruncatedPathAlgebra", "RadicalSquareZero",
38
+ "IncidenceAlgebra", "QuantumCI", "ExteriorAlgebra", "PreprojectiveAlgebra",
39
+ "TrivialExtension", "TensorProduct", "zoo", "families",
40
+ "bibliography",
41
+ "sweep",
42
+ ]
@@ -0,0 +1,20 @@
1
+ """quiverlab.batch: SQLite persistence + scan driver for family sweeps.
2
+
3
+ Expert-facing; deliberately NOT exported at the quiverlab top level -- reach it via
4
+ ``import quiverlab.batch``. This package is the quiverlab lift of hanlab's ``labdb``
5
+ (Marco Armenta's HansConjecture, MIT): the pure ``analyze(spec) -> record`` unit of
6
+ work, the named family builder registry, and the ``ResultsDB`` SQLite store with the
7
+ Han counterexample-search queries.
8
+
9
+ quiverlab.batch.db.ResultsDB -- SQLite results store (context manager)
10
+ quiverlab.batch.builders.BUILDERS -- {name: builder}; build_algebra(spec)
11
+ quiverlab.batch.scan.analyze -- pure spec -> JSON record; run_scan(specs, n_workers)
12
+ quiverlab.batch.PRIME -- the char-0 proxy prime, 32003
13
+ """
14
+ from quiverlab.batch import builders, db, scan # noqa: F401
15
+ # Package-level surface frozen for Plans 07/09 (see Plan-06 Task-14 freeze).
16
+ from quiverlab.batch.builders import BUILDERS, build_algebra # noqa: F401
17
+ from quiverlab.batch.db import ResultsDB # noqa: F401
18
+ from quiverlab.batch.scan import ( # noqa: F401
19
+ PRIME, analyze, depth_for_dim, han_verdict, max_term_dim_for_dim,
20
+ open_zoo_to_specs, run_scan)
@@ -0,0 +1,68 @@
1
+ """Builder registry: name -> callable(*args) -> quiverlab Algebra (quiverlab.batch).
2
+
3
+ Adapted from hanlab's ``labdb`` builder registry (Marco Armenta's HansConjecture,
4
+ MIT): the same catalog of named families, but each name now resolves to the
5
+ corresponding quiverlab family constructor. Every builder threads an optional
6
+ ``field`` (default: the family's own default, exact CC) so ``scan.analyze`` can
7
+ rebuild an algebra over GF(p) for the exact F_p Hochschild computation.
8
+
9
+ The six core builders (mapped per the Plan-06 brief):
10
+ truncated_polynomial(a) -> families.basic.truncated_polynomial(a)
11
+ quantum_ci(c) -> families.QuantumCI(q=c)
12
+ cyclic_nakayama(n, ell) -> families.NakayamaAlgebra(n=n, l=ell, cyclic=True)
13
+ linear_path_algebra(n) -> families.basic.linear_path_algebra(n)
14
+ dynkin(typ, n) -> families.PathAlgebra(f"{typ}{n}")
15
+ reduction_system(ngen, rules, name)
16
+ -> families.zoo.build_from_record({...}) (open zone)
17
+ """
18
+ from quiverlab.families import basic
19
+ from quiverlab.families.nakayama import NakayamaAlgebra
20
+ from quiverlab.families.path_algebra import PathAlgebra
21
+ from quiverlab.families.quantum import QuantumCI
22
+ from quiverlab.families.zoo import build_from_record as _build_from_record
23
+
24
+
25
+ def _truncated_polynomial(a, field=None):
26
+ return basic.truncated_polynomial(a, field=field)
27
+
28
+
29
+ def _quantum_ci(c, field=None):
30
+ return QuantumCI(q=c, field=field)
31
+
32
+
33
+ def _cyclic_nakayama(n, ell, field=None):
34
+ return NakayamaAlgebra(n=n, l=ell, cyclic=True, field=field)
35
+
36
+
37
+ def _linear_path_algebra(n, field=None):
38
+ return basic.linear_path_algebra(n, field=field)
39
+
40
+
41
+ def _dynkin(typ, n, field=None):
42
+ return PathAlgebra(f"{typ}{n}", field=field)
43
+
44
+
45
+ def _reduction_system(ngen, rules, name, arrows=None, vertices=None, field=None):
46
+ """Open-zone builder: materialise A = k<g>/I from a confluent reduction system,
47
+ routed through quiverlab's zoo reduction-system -> Algebra path. Multi-vertex
48
+ records (Plan 18) carry arrows/vertices; legacy args are byte-unchanged."""
49
+ rec = {"ngen": ngen, "rules": rules, "name": name, "dim": None}
50
+ if arrows is not None:
51
+ rec["arrows"] = arrows
52
+ rec["vertices"] = vertices
53
+ return _build_from_record(rec, field=field)
54
+
55
+
56
+ BUILDERS = {
57
+ "truncated_polynomial": _truncated_polynomial, # (a,)
58
+ "quantum_ci": _quantum_ci, # (c,)
59
+ "cyclic_nakayama": _cyclic_nakayama, # (n, ell)
60
+ "linear_path_algebra": _linear_path_algebra, # (n,)
61
+ "dynkin": _dynkin, # (typ, n)
62
+ "reduction_system": _reduction_system, # (ngen, rules, name) -- open zone
63
+ }
64
+
65
+
66
+ def build_algebra(spec):
67
+ """Construct the quiverlab Algebra named by a spec dict (builder + args)."""
68
+ return BUILDERS[spec["builder"]](*spec.get("args", []))
quiverlab/batch/db.py ADDED
@@ -0,0 +1,157 @@
1
+ """SQLite results database for batch scans (quiverlab.batch).
2
+
3
+ Ported verbatim (schema, migration, queries) from hanlab's ``labdb.ResultsDB``
4
+ -- Marco Armenta's ``HansConjecture`` codebase (MIT), the "B4" results-database
5
+ component. Only the module docstring and the surrounding package layout are
6
+ quiverlab's; the SQLite schema, the ``_migrate_open_columns`` back-compat path,
7
+ ``_nullable_bool`` and every query method are the bank's, kept faithful so a
8
+ quiverlab scan DB round-trips byte-for-byte with a hanlab one.
9
+
10
+ ``ResultsDB`` persists ``scan.analyze`` records to SQLite, indexed by complexity
11
+ and structural data, and exposes the counterexample-search queries the Han
12
+ programme needs -- in particular ``bounded_homology_candidates()`` ("bounded HH
13
+ but apparently infinite global dimension", the shape of a Han counterexample) and
14
+ ``asymmetric()`` ("HH_* and HH^* have different complexity"). Records are stored
15
+ as exact JSON (ints/strings; no floats): ``record_json`` is the full record and
16
+ the promoted columns are exact copies of its scalar fields.
17
+ """
18
+
19
+ import json
20
+ import sqlite3
21
+
22
+ _SCHEMA = """
23
+ CREATE TABLE IF NOT EXISTS results (
24
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
25
+ name TEXT,
26
+ builder TEXT,
27
+ args TEXT,
28
+ dim INTEGER,
29
+ N INTEGER,
30
+ associative INTEGER,
31
+ cx_homology TEXT,
32
+ cx_cohomology TEXT,
33
+ asymmetric INTEGER,
34
+ homology_bounded INTEGER,
35
+ homology_nonzero_tail INTEGER,
36
+ kind TEXT, -- 'open' for reduction_system rows, else NULL
37
+ han_verdict TEXT, -- open zone: nonzero-tail / COUNTEREXAMPLE? / ...
38
+ growth TEXT, -- open zone: bounded / growing / ?
39
+ depth_reached INTEGER, -- open zone: last HH_* degree computed
40
+ truncated_at INTEGER, -- open zone: degree the budget stopped at, or NULL
41
+ record_json TEXT,
42
+ UNIQUE(builder, args, N)
43
+ );
44
+ CREATE INDEX IF NOT EXISTS idx_cx ON results(cx_homology, cx_cohomology);
45
+ CREATE INDEX IF NOT EXISTS idx_asym ON results(asymmetric);
46
+ CREATE INDEX IF NOT EXISTS idx_verdict ON results(han_verdict);
47
+ """
48
+
49
+
50
+ def _nullable_bool(v):
51
+ """None -> None (SQL NULL); else 0/1. Open-zone records have asymmetric=None,
52
+ which must not be lossily collapsed to 0 ('symmetric')."""
53
+ return None if v is None else int(bool(v))
54
+
55
+
56
+ class ResultsDB:
57
+ """SQLite store for scan records, with the counterexample-search queries."""
58
+
59
+ def __init__(self, path=":memory:"):
60
+ self.conn = sqlite3.connect(path)
61
+ self.conn.row_factory = sqlite3.Row
62
+ self.conn.executescript(_SCHEMA)
63
+ self._migrate_open_columns() # backward-compat for old on-disk DBs
64
+ self.conn.commit()
65
+
66
+ def _migrate_open_columns(self):
67
+ """Add the open-zone columns to a pre-existing table that lacks them.
68
+
69
+ No-op on a DB created with the current _SCHEMA -- the supported path always
70
+ builds the DB fresh, so this is belt-and-suspenders for ad-hoc reuse of a
71
+ stale on-disk file (its pre-existing rows backfill as NULL)."""
72
+ have = {r["name"] for r in self.conn.execute("PRAGMA table_info(results)")}
73
+ for col, decl in (("kind", "TEXT"), ("han_verdict", "TEXT"),
74
+ ("growth", "TEXT"), ("depth_reached", "INTEGER"),
75
+ ("truncated_at", "INTEGER")):
76
+ if col not in have:
77
+ self.conn.execute(f"ALTER TABLE results ADD COLUMN {col} {decl}")
78
+
79
+ def close(self):
80
+ self.conn.close()
81
+
82
+ def __enter__(self):
83
+ return self
84
+
85
+ def __exit__(self, *a):
86
+ self.close()
87
+
88
+ def insert(self, rec):
89
+ """Insert (or replace) one analyze() record (structural or open-zone).
90
+
91
+ The five open-zone columns are read with `.get(...)`, so a structural record
92
+ (which lacks them) writes NULL -- backward compatible."""
93
+ self.conn.execute(
94
+ """INSERT OR REPLACE INTO results
95
+ (name, builder, args, dim, N, associative, cx_homology, cx_cohomology,
96
+ asymmetric, homology_bounded, homology_nonzero_tail,
97
+ kind, han_verdict, growth, depth_reached, truncated_at, record_json)
98
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
99
+ (
100
+ rec.get("name"),
101
+ rec.get("builder"),
102
+ json.dumps(rec.get("args", [])),
103
+ rec.get("dim"),
104
+ rec.get("N"),
105
+ int(rec.get("associative", False)),
106
+ str(rec.get("cx_homology")),
107
+ str(rec.get("cx_cohomology")),
108
+ _nullable_bool(rec.get("asymmetric")), # open: None -> SQL NULL
109
+ int(rec.get("homology_bounded", False)),
110
+ int(rec.get("homology_nonzero_tail", False)),
111
+ rec.get("kind"), # 'open' or None
112
+ rec.get("han_verdict"), # str or None
113
+ rec.get("growth"), # str or None
114
+ rec.get("depth_reached"), # int or None
115
+ rec.get("truncated_at"), # int or None
116
+ json.dumps(rec),
117
+ ),
118
+ )
119
+ self.conn.commit()
120
+
121
+ def insert_many(self, recs):
122
+ for r in recs:
123
+ self.insert(r)
124
+
125
+ def all(self):
126
+ return [json.loads(r["record_json"])
127
+ for r in self.conn.execute("SELECT record_json FROM results")]
128
+
129
+ def query(self, where, params=()):
130
+ rows = self.conn.execute(
131
+ f"SELECT record_json FROM results WHERE {where}", params)
132
+ return [json.loads(r["record_json"]) for r in rows]
133
+
134
+ def asymmetric(self):
135
+ """Algebras whose HH_* and HH^* have different apparent complexity."""
136
+ return self.query("asymmetric = 1")
137
+
138
+ def bounded_homology_candidates(self):
139
+ """Counterexample-shaped rows (associative): (i) bounded HH_* -- not obviously
140
+ gl.dim finite (structural + open), OR (ii) open-zone rows whose HH_* vanished in
141
+ high degree (han_verdict='COUNTEREXAMPLE?'). `han_verdict` is NULL on structural
142
+ rows, so the second disjunct never changes their behavior."""
143
+ return self.query(
144
+ "associative = 1 AND "
145
+ "(homology_bounded = 1 OR han_verdict = 'COUNTEREXAMPLE?')"
146
+ )
147
+
148
+ def open_counterexample_candidates(self):
149
+ """Open-zone rows whose HH_* appears to vanish in high degree (the literal
150
+ Han-counterexample shape); indexed by idx_verdict."""
151
+ return self.query("han_verdict = 'COUNTEREXAMPLE?'")
152
+
153
+ def by_complexity(self, cx_homology):
154
+ return self.query("cx_homology = ?", (str(cx_homology),))
155
+
156
+ def __len__(self):
157
+ return self.conn.execute("SELECT COUNT(*) FROM results").fetchone()[0]
@@ -0,0 +1,257 @@
1
+ """Pure per-algebra analysis + scan driver (quiverlab.batch).
2
+
3
+ Adapted from hanlab's ``labdb.analyze`` / ``_analyze_open`` / ``run_scan`` (Marco
4
+ Armenta's HansConjecture, MIT). The per-algebra computation is a **pure function**
5
+ ``analyze(spec) -> record``: a ``spec`` is a small JSON-serializable description
6
+ ``{"builder", "args", "N", "primes"}`` and ``analyze`` builds the algebra from the
7
+ builder registry, computes HH_* and HH^* over the requested primes, the apparent
8
+ complexities, and the homology/cohomology asymmetry, and returns a JSON record.
9
+ ``run_scan`` drives a list of specs -- single-worker by default; the same
10
+ ``analyze`` is the unit of work for a SLURM array on a cluster (one spec per task,
11
+ merge the JSON afterwards -- no shared state).
12
+
13
+ Structural builders route through the exact F_p Hochschild engine
14
+ (``Algebra.hochschild_homology`` / ``hochschild_cohomology`` over GF(PRIME)) plus
15
+ the apparent-complexity classifier ``scan3.complexity_of``. The ``reduction_system``
16
+ builder routes to the open-zone minimal A^e-resolution path. The ``scan_open``
17
+ policy helpers ``han_verdict`` / ``depth_for_dim`` / ``max_term_dim_for_dim`` are
18
+ ported verbatim from hanlab.scan_open below.
19
+
20
+ All record values are exact ints / strings (dims are ints; complexities are ints
21
+ or the sentinel string ``'>=2'``) -- there is no float round-trip anywhere.
22
+ """
23
+ from quiverlab.batch.builders import BUILDERS
24
+ from quiverlab.fields import GF
25
+
26
+ PRIME = 32003
27
+
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # scan_open policy helpers -- ported verbatim from hanlab.scan_open (MIT)
31
+ # ---------------------------------------------------------------------------
32
+ def depth_for_dim(d):
33
+ """How deep to push HH_* by dimension (memory- and time-aware on a small host)."""
34
+ if d <= 6:
35
+ return 20
36
+ if d <= 9:
37
+ return 16
38
+ if d <= 12:
39
+ return 12
40
+ return 9
41
+
42
+
43
+ def max_term_dim_for_dim(d):
44
+ # cap on m^2 * r_n (the k-dimension of a single resolution term)
45
+ if d <= 9:
46
+ return 9000
47
+ if d <= 12:
48
+ return 13000
49
+ return 18000
50
+
51
+
52
+ def han_verdict(seq):
53
+ """Classify an HH_* sequence (char-0 proxy) for the Han question.
54
+
55
+ Returns a ``(verdict, growth)`` pair. ``verdict`` is one of:
56
+ 'empty' : the sequence is empty (no data);
57
+ 'nonzero-tail' : HH_n > 0 for the LAST computed n (consistent with Han);
58
+ 'COUNTEREXAMPLE?' : HH_n = 0 for the last computed n -- homology appears to
59
+ vanish in high degree, the shape a Han counterexample
60
+ would take.
61
+ ``growth`` is the tail descriptor:
62
+ 'growing' : >= 6 terms and the last term exceeds the term four back;
63
+ 'bounded' : otherwise (eventually constant / periodic / too short);
64
+ '?' : only for the empty sequence."""
65
+ if not seq:
66
+ return "empty", "?"
67
+ verdict = "COUNTEREXAMPLE?" if seq[-1] == 0 else "nonzero-tail"
68
+ # growth: the last term still rising above the term four back
69
+ growth = "growing" if (len(seq) >= 6 and seq[-1] > seq[max(0, len(seq) - 4)]) else "bounded"
70
+ return verdict, growth
71
+
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # open-zoo -> spec adapter -- ported verbatim from hanlab.cluster.make_shards (MIT)
75
+ # ---------------------------------------------------------------------------
76
+ def _reduction_system_args(entry):
77
+ """Catalog entry -> the ``reduction_system`` builder's args.
78
+
79
+ Matches ``builders._reduction_system(ngen, rules, name[, arrows, vertices])``;
80
+ legacy (local) entries keep the 3-arg shape byte-for-byte (spec back-compat,
81
+ same law as ``max_transient_bytes``). If the builder's arg shape ever changes,
82
+ this is the only place to touch."""
83
+ args = [entry["ngen"], entry["rules"], entry["name"]]
84
+ if "arrows" in entry:
85
+ args += [entry["arrows"], entry["vertices"]]
86
+ return args
87
+
88
+
89
+ def open_zoo_to_specs(catalog, primes=(PRIME,), max_dim=None, min_dim=None, limit=None,
90
+ max_transient_bytes=None):
91
+ """Convert open-zoo catalog entries to ``reduction_system`` specs, using the same
92
+ depth/budget policy as the scan_open helpers (so a cluster sweep matches the local one).
93
+
94
+ ``min_dim``/``max_dim`` keep only algebras with min_dim <= catalog dim <= max_dim (a
95
+ dim band -- the cheap-first curriculum); ``limit`` caps the number of specs (after
96
+ filtering). ``max_transient_bytes`` (bytes; None = off) is baked into each spec as the
97
+ per-worker peak-memory budget for the minimal-resolution radK transient, so the spec
98
+ list stays a pure function of its inputs (sharded == serial). When None the key is
99
+ omitted, so existing specs/records are byte-unchanged (back-compat)."""
100
+ specs = []
101
+ for e in catalog:
102
+ d = e["dim"]
103
+ if max_dim is not None and d > max_dim:
104
+ continue
105
+ if min_dim is not None and d < min_dim:
106
+ continue
107
+ spec = {
108
+ "builder": "reduction_system",
109
+ "args": _reduction_system_args(e),
110
+ "N": depth_for_dim(d),
111
+ "primes": list(primes),
112
+ "max_term_dim": max_term_dim_for_dim(d),
113
+ }
114
+ if max_transient_bytes is not None:
115
+ spec["max_transient_bytes"] = int(max_transient_bytes)
116
+ specs.append(spec)
117
+ if limit is not None and len(specs) >= limit:
118
+ break
119
+ return specs
120
+
121
+
122
+ # ---------------------------------------------------------------------------
123
+ # The pure unit of work
124
+ # ---------------------------------------------------------------------------
125
+ def analyze(spec):
126
+ """Pure function spec -> record. Deterministic; safe to run in any process.
127
+
128
+ spec: {"builder": str, "args": list, "N": int, "primes": list[int] (optional)}.
129
+ The "reduction_system" builder routes to the open-zone minimal-resolution path
130
+ (HH_* only, no cohomology); every other builder takes the structural fast-engine
131
+ path (HH_* and HH^* over GF(p))."""
132
+ if spec["builder"] == "reduction_system":
133
+ return _analyze_open(spec)
134
+ from quiverlab.engine.scan3 import complexity_of
135
+
136
+ primes = tuple(spec.get("primes", (PRIME, 2, 3)))
137
+ N = spec["N"]
138
+ builder = spec["builder"]
139
+ args = list(spec.get("args", []))
140
+ # rebuild the family over each prime field; the fast engine computes exact F_p HH
141
+ algs = {p: BUILDERS[builder](*args, field=GF(p)) for p in primes}
142
+ alg0 = algs[primes[0]]
143
+ name = spec.get("name") or getattr(alg0, "zoo_name", None) or f"{builder}{tuple(args)}"
144
+ hh = {p: list(map(int, algs[p].hochschild_homology(N, engine="auto").dims))
145
+ for p in primes}
146
+ hc = {p: list(map(int, algs[p].hochschild_cohomology(N, engine="auto").dims))
147
+ for p in primes}
148
+ cx_h = complexity_of(hh[primes[0]])
149
+ cx_c = complexity_of(hc[primes[0]])
150
+ hseq = hh[primes[0]] # char-0 proxy drives the flags
151
+ return {
152
+ "name": name,
153
+ "builder": builder,
154
+ "args": args,
155
+ "dim": int(alg0.dim),
156
+ "N": N,
157
+ # every quiverlab family is associative by construction (validated at build)
158
+ "associative": True,
159
+ "HH_homology": {str(p): hh[p] for p in primes},
160
+ "HH_cohomology": {str(p): hc[p] for p in primes},
161
+ "cx_homology": cx_h,
162
+ "cx_cohomology": cx_c,
163
+ "asymmetric": bool(cx_h != cx_c),
164
+ "homology_bounded": bool(cx_h == 0), # HH_* eventually zero
165
+ "homology_nonzero_tail": bool(hseq[-1] != 0), # HH_N != 0
166
+ }
167
+
168
+
169
+ def _analyze_open(spec):
170
+ """Open-zone record path: HH_* via the minimal A^e-resolution over F_p.
171
+
172
+ The minimal engine has no cohomology, so cohomology-side fields are explicit None
173
+ (`cx_cohomology`, `asymmetric`); the counterexample-query flags are still computed
174
+ from the HH_* tail so `bounded_homology_candidates` / `by_complexity` work on open
175
+ rows. A non-confluent / infinite / unbuildable reduction system yields a JSON error
176
+ record (never a raise), so a poison spec cannot kill a multiprocessing shard.
177
+
178
+ NOTE: this path needs a full open-zone reduction-system -> Algebra build (the Plan-04
179
+ Chouhy-Solotar backend) to reproduce the golden values on non-monomial systems; the
180
+ golden port test is skipped until that backend is on the branch."""
181
+ from quiverlab.engine.adapter import to_engine
182
+ from quiverlab.engine.resolutions_minimal import (
183
+ minimal_homology_dims, minimal_resolution)
184
+ from quiverlab.engine.scan3 import complexity_of
185
+ from quiverlab.errors import QuiverlabError
186
+
187
+ primes = tuple(spec.get("primes", (PRIME,)))
188
+ args = spec["args"]
189
+ name = args[2] if len(args) > 2 else "A"
190
+ rec = {
191
+ "name": name,
192
+ "builder": "reduction_system",
193
+ "args": list(args),
194
+ "kind": "open", # marks the open-zone record family
195
+ "N": spec.get("N"), # carried even on the error path so the
196
+ # resume-skip / merge-dedup identity (builder, args, N) matches the spec's.
197
+ }
198
+ # build (catch bad-input errors -> error record, never crash a worker)
199
+ try:
200
+ A = BUILDERS["reduction_system"](*args, field=GF(PRIME))
201
+ except (QuiverlabError, ValueError) as e:
202
+ rec["error"] = f"{type(e).__name__}: {e}"
203
+ rec["associative"] = False # an unbuildable system is not an algebra
204
+ return rec
205
+
206
+ dim = int(A.dim)
207
+ N = int(spec.get("N", depth_for_dim(dim)))
208
+ budget = int(spec.get("max_term_dim", max_term_dim_for_dim(dim)))
209
+ # Peak-memory guard (bytes; None = off): bounds the dense radK transient so a high-dim
210
+ # build truncates gracefully instead of OOM-killing the worker.
211
+ mtb = spec.get("max_transient_bytes")
212
+ mtb = int(mtb) if mtb is not None else None
213
+ rec["dim"] = dim
214
+ rec["N"] = N
215
+ rec["max_term_dim"] = budget
216
+ rec["max_transient_bytes"] = mtb
217
+ rec["associative"] = True # validated at build
218
+
219
+ # unit-adaptation (b_0 = 1_A) is a LOCAL-algebra convention; on multi-vertex
220
+ # algebras it destroys the path-type basis the minimal engine's radical
221
+ # identification needs (the Plan-13 nilpotency guard would refuse) -- pass
222
+ # those through on their path basis unchanged (Plan 18)
223
+ E = to_engine(A if len(A.quiver.vertices) > 1 else A.unit_adapted())
224
+ hh = minimal_homology_dims(E, N, primes=primes, max_term_dim=budget,
225
+ max_transient_bytes=mtb)
226
+ rks, _cols, _eng, trunc = minimal_resolution(E, N, PRIME, max_term_dim=budget,
227
+ max_transient_bytes=mtb)
228
+ hseq = hh[PRIME] # char-0 proxy drives the verdict/flags
229
+ verdict, growth = han_verdict(hseq)
230
+
231
+ rec["HH_homology"] = {str(p): list(map(int, hh[p])) for p in primes}
232
+ rec["resolution_ranks"] = [int(rks.get(n, 0)) for n in range(0, N + 2)]
233
+ rec["depth_reached"] = len(hseq) - 1
234
+ rec["truncated_at"] = trunc # None or int (JSON null / int)
235
+ rec["han_verdict"] = verdict
236
+ rec["growth"] = growth
237
+ # cohomology side does not exist on this engine -> explicit None (not a misleading 0)
238
+ rec["cx_cohomology"] = None
239
+ rec["asymmetric"] = None
240
+ # counterexample-query flags from the HH_* tail (keep the structural queries working)
241
+ rec["cx_homology"] = complexity_of(hseq)
242
+ rec["homology_bounded"] = bool(rec["cx_homology"] == 0)
243
+ rec["homology_nonzero_tail"] = bool(hseq[-1] != 0) if hseq else False
244
+ return rec
245
+
246
+
247
+ def run_scan(specs, n_workers=1):
248
+ """Run `analyze` over a list of specs. n_workers>1 uses a multiprocessing Pool.
249
+
250
+ Returns the list of records in the same order as `specs` (deterministic).
251
+ Default single-worker: the Pool is imported/created only when n_workers > 1
252
+ (guarded -- no process fan-out on a small host)."""
253
+ if n_workers <= 1:
254
+ return [analyze(s) for s in specs]
255
+ from multiprocessing import Pool
256
+ with Pool(processes=n_workers) as pool:
257
+ return pool.map(analyze, specs)
@@ -0,0 +1,5 @@
1
+ """quiverlab citations registry (spec §3.9)."""
2
+ from quiverlab.citations.registry import ( # noqa: F401
3
+ REGISTRY, Reference, reference, bibtex, all_keys, references_bib_path,
4
+ )
5
+ from quiverlab.citations.bibliography import bibliography, Bibliography # noqa: F401
@@ -0,0 +1,125 @@
1
+ """Grouped, annotated, ITERABLE bibliography (spec §3.9). Plan 09's
2
+ references.entry_view() iterates bibliography() and reads e.key / e.formatted /
3
+ e.doi / e.arxiv / e.topic / e.annotation off each entry -- those attribute names
4
+ are the frozen contract. `formatted`, `doi`, and `arxiv` are built from
5
+ references.bib; `topic` is mapped from the registry `kind`; annotations come from
6
+ the registry."""
7
+ import re
8
+ from dataclasses import dataclass
9
+
10
+ from quiverlab.citations.registry import (
11
+ REGISTRY, bibtex as _bibtex, reference, references_bib_path,
12
+ )
13
+
14
+ # topic = the /literature grouping label; mapped from the registry kind.
15
+ _KIND_TOPIC = {"algorithm": "Algorithms", "family": "Families",
16
+ "field": "Finite fields", "foundation": "Foundations"}
17
+ _TOPIC_ORDER = ("Algorithms", "Families", "Finite fields", "Foundations")
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class Entry:
22
+ """One bibliography row. Attribute names match Plan 09's entry_view() reads."""
23
+ key: str # the registry key -- matches table.references / A.citations()
24
+ bibtex_key: str
25
+ formatted: str # human citation string built from references.bib
26
+ doi: str | None
27
+ arxiv: str | None
28
+ topic: str # mapped from kind
29
+ annotation: str
30
+
31
+
32
+ def _bib_fields(bibtex_key: str) -> dict:
33
+ """Pull author/title/journal/year/volume/pages/doi/note from one .bib entry."""
34
+ text = references_bib_path().read_text(encoding="utf-8")
35
+ m = re.search(r"@\w+\{" + re.escape(bibtex_key) + r",(.*?\n)\}", text, re.S)
36
+ body = m.group(1) if m else ""
37
+ out = {}
38
+ for fm in re.finditer(r"(\w+)\s*=\s*\{(.*?)\}(?=,\s*\n|\s*\n\})", body, re.S):
39
+ out[fm.group(1).lower()] = re.sub(r"\s+", " ", fm.group(2)).strip()
40
+ return out
41
+
42
+
43
+ def _clean(s: str) -> str:
44
+ return (s.replace("{", "").replace("}", "").replace("\\", "")
45
+ .replace("--", "-").replace("~", " ").strip())
46
+
47
+
48
+ def _format(bibtex_key: str, f: dict) -> str:
49
+ authors = _clean(f.get("author", "")).replace(" and ", "; ")
50
+ year = f.get("year", "")
51
+ title = _clean(f.get("title", bibtex_key))
52
+ venue = _clean(f.get("journal") or f.get("booktitle") or f.get("publisher") or "")
53
+ vol = f.get("volume", "")
54
+ pages = _clean(f.get("pages", ""))
55
+ bits = [b for b in [f"{authors} ({year})." if authors else "", f"{title}.",
56
+ f"{venue} {vol}".strip() + (f", {pages}" if pages else "") + "."
57
+ if venue else ""] if b]
58
+ return " ".join(bits)
59
+
60
+
61
+ def _arxiv_of(f: dict) -> str | None:
62
+ m = re.search(r"arXiv:([\w./-]+)", f.get("note", "") + " " + f.get("eprint", ""))
63
+ return m.group(1) if m else None
64
+
65
+
66
+ def _entry(key: str) -> Entry:
67
+ r = REGISTRY[key]
68
+ f = _bib_fields(r.bibtex_key)
69
+ return Entry(key=key, bibtex_key=r.bibtex_key, formatted=_format(r.bibtex_key, f),
70
+ doi=f.get("doi"), arxiv=_arxiv_of(f),
71
+ topic=_KIND_TOPIC.get(r.kind, "Other"), annotation=r.annotation)
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class Bibliography:
76
+ keys: tuple
77
+ _entries: tuple
78
+
79
+ def __iter__(self):
80
+ return iter(self._entries) # Plan 09 consumes this
81
+
82
+ @property
83
+ def groups(self) -> dict:
84
+ g = {}
85
+ for e in self._entries:
86
+ g.setdefault(e.topic, []).append(e)
87
+ return g
88
+
89
+ def bibtex(self) -> str:
90
+ out, seen = [], set()
91
+ for k in self.keys:
92
+ bk = REGISTRY[k].bibtex_key
93
+ if bk not in seen:
94
+ seen.add(bk)
95
+ out.append(_bibtex(k))
96
+ return "\n\n".join(out) + ("\n" if out else "")
97
+
98
+ def to_dict(self) -> dict:
99
+ return {"keys": list(self.keys),
100
+ "groups": {t: [e.__dict__ for e in es] for t, es in self.groups.items()}}
101
+
102
+ def __str__(self) -> str:
103
+ lines = []
104
+ for t in _TOPIC_ORDER:
105
+ es = self.groups.get(t)
106
+ if not es:
107
+ continue
108
+ lines.append(f"## {t}")
109
+ for e in es:
110
+ lines.append(f" [{e.key}] {e.formatted}")
111
+ lines.append(f" {e.annotation}")
112
+ lines.append("")
113
+ return "\n".join(lines).rstrip() + "\n"
114
+
115
+
116
+ def bibliography(keys=None, grouped=True, annotated=True) -> Bibliography:
117
+ if keys is None:
118
+ keys = list(REGISTRY)
119
+ seen, ordered = set(), []
120
+ for k in keys:
121
+ reference(k) # loud on unknown
122
+ if k not in seen:
123
+ seen.add(k)
124
+ ordered.append(k)
125
+ return Bibliography(tuple(ordered), tuple(_entry(k) for k in ordered))