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.
- quiverlab/__init__.py +42 -0
- quiverlab/batch/__init__.py +20 -0
- quiverlab/batch/builders.py +68 -0
- quiverlab/batch/db.py +157 -0
- quiverlab/batch/scan.py +257 -0
- quiverlab/citations/__init__.py +5 -0
- quiverlab/citations/bibliography.py +125 -0
- quiverlab/citations/references.bib +420 -0
- quiverlab/citations/registry.py +275 -0
- quiverlab/combinat/__init__.py +2 -0
- quiverlab/combinat/quiver.py +108 -0
- quiverlab/combinat/relations.py +199 -0
- quiverlab/core/__init__.py +2 -0
- quiverlab/core/algebra.py +678 -0
- quiverlab/core/monomial.py +158 -0
- quiverlab/engine/__init__.py +13 -0
- quiverlab/engine/_kernels.py +276 -0
- quiverlab/engine/adapter.py +84 -0
- quiverlab/engine/bimodule.py +297 -0
- quiverlab/engine/coxeter.py +373 -0
- quiverlab/engine/coxeter2.py +241 -0
- quiverlab/engine/coxeter_spectrum.py +259 -0
- quiverlab/engine/cyclic.py +271 -0
- quiverlab/engine/deepen.py +236 -0
- quiverlab/engine/hh_engine.py +323 -0
- quiverlab/engine/linalg_fast.py +102 -0
- quiverlab/engine/resolutions.py +112 -0
- quiverlab/engine/resolutions_bardzell.py +472 -0
- quiverlab/engine/resolutions_minimal.py +1029 -0
- quiverlab/engine/resolutions_periodic.py +96 -0
- quiverlab/engine/scan2.py +430 -0
- quiverlab/engine/scan3.py +136 -0
- quiverlab/engine/tt_calculus.py +339 -0
- quiverlab/errors.py +40 -0
- quiverlab/families/__init__.py +16 -0
- quiverlab/families/basic.py +22 -0
- quiverlab/families/discover.py +79 -0
- quiverlab/families/dynkin.py +58 -0
- quiverlab/families/exterior.py +16 -0
- quiverlab/families/incidence.py +35 -0
- quiverlab/families/nakayama.py +70 -0
- quiverlab/families/path_algebra.py +13 -0
- quiverlab/families/poset.py +51 -0
- quiverlab/families/preprojective.py +116 -0
- quiverlab/families/quantum.py +67 -0
- quiverlab/families/radical_square_zero.py +9 -0
- quiverlab/families/tensor.py +52 -0
- quiverlab/families/trivial_extension.py +327 -0
- quiverlab/families/truncated.py +30 -0
- quiverlab/families/zoo.py +72 -0
- quiverlab/families/zoo_catalog.json +6280 -0
- quiverlab/fields/__init__.py +6 -0
- quiverlab/fields/complexfield.py +137 -0
- quiverlab/fields/conway.py +32 -0
- quiverlab/fields/domain.py +132 -0
- quiverlab/fields/finitefield.py +149 -0
- quiverlab/fields/gaussian.py +50 -0
- quiverlab/fields/linalg.py +87 -0
- quiverlab/fields/primefield.py +51 -0
- quiverlab/fields/rationals.py +38 -0
- quiverlab/groebner/__init__.py +13 -0
- quiverlab/groebner/certificate.py +42 -0
- quiverlab/groebner/complete.py +80 -0
- quiverlab/groebner/events.py +26 -0
- quiverlab/groebner/lower.py +52 -0
- quiverlab/groebner/order.py +74 -0
- quiverlab/groebner/overlap.py +63 -0
- quiverlab/groebner/reduction.py +99 -0
- quiverlab/groebner/system.py +90 -0
- quiverlab/hochschild/__init__.py +3 -0
- quiverlab/hochschild/bar.py +152 -0
- quiverlab/hochschild/basis_reps.py +165 -0
- quiverlab/hochschild/cyclic.py +165 -0
- quiverlab/hochschild/cyclic_reps.py +174 -0
- quiverlab/hochschild/hh_reps.py +193 -0
- quiverlab/hochschild/products.py +454 -0
- quiverlab/hochschild/table.py +31 -0
- quiverlab/hpc/__init__.py +24 -0
- quiverlab/hpc/__main__.py +8 -0
- quiverlab/hpc/cli.py +456 -0
- quiverlab/hpc/deepen_runner.py +62 -0
- quiverlab/hpc/report.py +682 -0
- quiverlab/hpc/resources.py +296 -0
- quiverlab/hpc/spec.py +1745 -0
- quiverlab/invariants/__init__.py +1 -0
- quiverlab/invariants/betti.py +71 -0
- quiverlab/invariants/cartan.py +71 -0
- quiverlab/invariants/frobenius.py +314 -0
- quiverlab/invariants/pathbasis.py +58 -0
- quiverlab/invariants/scalar.py +119 -0
- quiverlab/invariants/spectral.py +149 -0
- quiverlab/invariants/sweep.py +65 -0
- quiverlab/modules/__init__.py +6 -0
- quiverlab/modules/builders.py +92 -0
- quiverlab/modules/complex_reps.py +542 -0
- quiverlab/modules/decompose.py +396 -0
- quiverlab/modules/duality.py +117 -0
- quiverlab/modules/ext.py +115 -0
- quiverlab/modules/ext_algebra.py +666 -0
- quiverlab/modules/hom.py +212 -0
- quiverlab/modules/injective.py +83 -0
- quiverlab/modules/koszul.py +505 -0
- quiverlab/modules/linalg_mod.py +139 -0
- quiverlab/modules/module.py +327 -0
- quiverlab/modules/opposite.py +67 -0
- quiverlab/modules/qpa_module.py +186 -0
- quiverlab/modules/radtopsoc.py +121 -0
- quiverlab/modules/resolution.py +172 -0
- quiverlab/modules/tor.py +181 -0
- quiverlab/modules/yoneda.py +373 -0
- quiverlab/qpa/__init__.py +7 -0
- quiverlab/qpa/crosscheck.py +377 -0
- quiverlab/qpa/scripts.py +262 -0
- quiverlab/qpa/session.py +108 -0
- quiverlab/resolutions_cs/__init__.py +33 -0
- quiverlab/resolutions_cs/_fieldshim.py +22 -0
- quiverlab/resolutions_cs/aarith.py +49 -0
- quiverlab/resolutions_cs/ambiguities.py +59 -0
- quiverlab/resolutions_cs/build.py +17 -0
- quiverlab/resolutions_cs/cap.py +108 -0
- quiverlab/resolutions_cs/comparison.py +833 -0
- quiverlab/resolutions_cs/cup.py +106 -0
- quiverlab/resolutions_cs/diagonal.py +325 -0
- quiverlab/resolutions_cs/engine_facade.py +79 -0
- quiverlab/resolutions_cs/homology.py +173 -0
- quiverlab/resolutions_cs/pelt.py +69 -0
- quiverlab/resolutions_cs/products.py +113 -0
- quiverlab/resolutions_cs/resolution.py +247 -0
- quiverlab/resolutions_cs/terms.py +19 -0
- quiverlab/resolutions_cs/trace.py +42 -0
- quiverlab/trace/__init__.py +17 -0
- quiverlab/trace/events.py +249 -0
- quiverlab/trace/interpretations.py +292 -0
- quiverlab/trace/json_guide.py +221 -0
- quiverlab/trace/modules.py +876 -0
- quiverlab/trace/products.py +619 -0
- quiverlab/trace/provenance.py +88 -0
- quiverlab/trace/recorder.py +163 -0
- quiverlab/trace/render_html.py +2200 -0
- quiverlab/trace/render_json.py +123 -0
- quiverlab/trace/render_text.py +286 -0
- quiverlab/trace/results_html.py +661 -0
- quiverlab/trace/writer.py +176 -0
- quiverlab/viz/__init__.py +16 -0
- quiverlab/viz/draw.py +77 -0
- quiverlab/viz/layout.py +159 -0
- quiverlab/viz/tikz.py +40 -0
- quiverlab-0.1.0.dist-info/METADATA +603 -0
- quiverlab-0.1.0.dist-info/RECORD +153 -0
- quiverlab-0.1.0.dist-info/WHEEL +5 -0
- quiverlab-0.1.0.dist-info/entry_points.txt +2 -0
- quiverlab-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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]
|
quiverlab/batch/scan.py
ADDED
|
@@ -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,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))
|