From 1b6ca70f8253f3c674c861c08861f72fcc93eb23 Mon Sep 17 00:00:00 2001
From: Dima Pasechnik
Date: Wed, 21 Oct 2026 14:55:04 -0500
Subject: [PATCH 1/4] Allow optional backends for Voronoi complex construction
---
lib/Voronoi/seb.gi | 33 ++++++++++++++++++++++++++++--
read.g | 4 ++++
tst/testextra/voronoi-backends.tst | 18 ++++++++++++++++
tutorial/tutorialBianchi.xml | 25 ++++++++++++++++++++++
4 files changed, 78 insertions(+), 2 deletions(-)
create mode 100644 tst/testextra/voronoi-backends.tst
diff --git a/lib/Voronoi/seb.gi b/lib/Voronoi/seb.gi
index f8663298..79bf318e 100644
--- a/lib/Voronoi/seb.gi
+++ b/lib/Voronoi/seb.gi
@@ -1,10 +1,34 @@
HAP_KOMPLEKS123:=fail;
DIMS:=0;; #THIS IS NOT ALLOWED! CHANGE IT!!
+# Optional providers implement function(kind,n,d), where kind is "GL" or "SL"
+# and d is a squarefree quadratic radicand. A provider returns a HAP non-free
+# resolution, or fail for unsupported inputs. The caller explicitly chooses it
+# with : backend := "name". No extra dependency is loaded by HAP.
+BindGlobal("HAP_VORONOI_BACKENDS", rec());
+BindGlobal("RegisterVoronoiBackend", function(name, provider)
+ if not IsString(name) or Length(name)=0 or name="magma"
+ or not IsFunction(provider) then
+ Error("expected a nonempty backend name other than magma and a function");
+ fi;
+ HAP_VORONOI_BACKENDS.(name) := provider;
+end);
+BindGlobal("CallVoronoiBackend", function(name, kind, n, d)
+ if not IsString(name) or not IsBound(HAP_VORONOI_BACKENDS.(name)) then
+ Error("unknown Voronoi backend; register it with RegisterVoronoiBackend");
+ fi;
+ return HAP_VORONOI_BACKENDS.(name)(kind,n,d);
+end);
+
############################################################
############################################################
VoronoiComplexGL:=function(n,d)
-local file,file2,m,k,G;
+local file,file2,m,k,G,backend;
+
+backend:=ValueOption("backend");
+if backend<>fail and backend<>"magma" then
+return CallVoronoiBackend(backend,"GL",n,d);
+fi;
if d<0 then
file:="~/pkg/Voronoi/VMH-ImaginaryQuadraticNumberFields/Magma/BasicData.m";
@@ -43,7 +67,12 @@ end;
############################################################
############################################################
VoronoiComplexSL:=function(n,d)
-local file,file2,m,k,G;
+local file,file2,m,k,G,backend;
+
+backend:=ValueOption("backend");
+if backend<>fail and backend<>"magma" then
+return CallVoronoiBackend(backend,"SL",n,d);
+fi;
if d<0 then
file:="~/pkg/Voronoi/VMH-ImaginaryQuadraticNumberFields/Magma/BasicData.m";
diff --git a/read.g b/read.g
index cfb0b26a..e247f2e8 100644
--- a/read.g
+++ b/read.g
@@ -112,6 +112,10 @@ fi;
ReadPackageHap( "lib/TitlePage/makeHapMan.gi");
+# Load the Voronoi entry points and optional provider interface. This defines
+# functions only; Magma is invoked only when its backend is requested.
+ReadPackageHap( "lib/Voronoi/seb.gi");
+
################# OBJECTIFICATIONS ###############################
ReadPackageHap( "lib/Objectifications/basicMethods.gi");
################# OBJECTIFICATIONS DONE ##########################
diff --git a/tst/testextra/voronoi-backends.tst b/tst/testextra/voronoi-backends.tst
new file mode 100644
index 00000000..26a7d89d
--- /dev/null
+++ b/tst/testextra/voronoi-backends.tst
@@ -0,0 +1,18 @@
+gap> START_TEST("voronoi-backends.tst");
+gap> RegisterVoronoiBackend("hap-test", function(kind,n,d)
+> return [kind,n,d]; end);
+gap> Print(VoronoiComplexGL(2,-7 : backend := "hap-test"),"\n");
+[ "GL", 2, -7 ]
+gap> Print(VoronoiComplexSL(2,-1 : backend := "hap-test"),"\n");
+[ "SL", 2, -1 ]
+gap> Print(VoronoiComplexGL(3,5 : backend := "hap-test"),"\n");
+[ "GL", 3, 5 ]
+gap> RegisterVoronoiBackend("hap-test-fail", function(kind,n,d)
+> return fail; end);
+gap> Print(VoronoiComplexSL(2,5 : backend := "hap-test-fail"),"\n");
+fail
+gap> Print(VoronoiComplexGL(2,-7 : backend := "hap-test"),"\n");
+[ "GL", 2, -7 ]
+gap> Unbind(HAP_VORONOI_BACKENDS.("hap-test"));
+gap> Unbind(HAP_VORONOI_BACKENDS.("hap-test-fail"));
+gap> STOP_TEST("voronoi-backends.tst", 1000);
diff --git a/tutorial/tutorialBianchi.xml b/tutorial/tutorialBianchi.xml
index a0cf35ac..dae372e0 100644
--- a/tutorial/tutorialBianchi.xml
+++ b/tutorial/tutorialBianchi.xml
@@ -407,5 +407,30 @@ The signs in the boundary maps of the free resolution are delicate. Another easy
<#Include SYSTEM "tutex/bianchi.17.txt">
+
+
+Optional Voronoi backends
+The functions VoronoiComplexGL(n,d) and
+VoronoiComplexSL(n,d) accept the option backend.
+Without this option, or with backend := "magma", they use the
+Magma implementation described above. An external package can register an
+alternative using RegisterVoronoiBackend(name, provider). The name
+must be a nonempty string other than "magma", and
+provider(kind,n,d) must be a function. Its first argument is
+"GL" or "SL"; d is a squarefree radicand, not a
+field discriminant. The provider returns a HAP non-free resolution for the
+requested group, or fail if the input is unsupported. Failure never
+silently falls back to Magma. Registering an existing name replaces its
+provider.
+For example, a Julia package running with GAP.jl can register an
+in-process callback under the name "julia". Once registered,
+VoronoiComplexGL(2,-7 : backend := "julia") calls that provider.
+HAP itself does not install or load Julia. An unregistered backend name is
+an error. Providers must supply complete stabilizers, the orientation action
+and equivariant boundaries; perfect forms or a group presentation alone
+do not suffice for higher homology. Standard HAP operations such as
+FreeGResolution and TensorWithIntegers then apply to the
+returned object. A contracting homotopy is a separate capability and must
+not be inferred merely from the existence of a resolution.
From 882003fa9dd1f8deb70e29ff97cd9c3d4f2c116d Mon Sep 17 00:00:00 2001
From: Dima Pasechnik
Date: Wed, 21 Oct 2026 16:15:37 -0500
Subject: [PATCH 2/4] Accept general request records in Voronoi providers
---
lib/Voronoi/seb.gi | 32 ++++++++++++-----
tst/testextra/voronoi-backends.tst | 30 +++++++++++-----
tutorial/tutorialBianchi.xml | 57 ++++++++++++++++++++----------
3 files changed, 84 insertions(+), 35 deletions(-)
diff --git a/lib/Voronoi/seb.gi b/lib/Voronoi/seb.gi
index 79bf318e..de815d70 100644
--- a/lib/Voronoi/seb.gi
+++ b/lib/Voronoi/seb.gi
@@ -1,10 +1,10 @@
HAP_KOMPLEKS123:=fail;
DIMS:=0;; #THIS IS NOT ALLOWED! CHANGE IT!!
-# Optional providers implement function(kind,n,d), where kind is "GL" or "SL"
-# and d is a squarefree quadratic radicand. A provider returns a HAP non-free
-# resolution, or fail for unsupported inputs. The caller explicitly chooses it
-# with : backend := "name". No extra dependency is loaded by HAP.
+# Optional providers implement function(request), returning a HAP non-free
+# resolution or fail for unsupported inputs. HAP passes the request record
+# unchanged; providers interpret and validate its field, order, lattice, group
+# and other data. No extra dependency is loaded by HAP.
BindGlobal("HAP_VORONOI_BACKENDS", rec());
BindGlobal("RegisterVoronoiBackend", function(name, provider)
if not IsString(name) or Length(name)=0 or name="magma"
@@ -13,11 +13,27 @@ BindGlobal("RegisterVoronoiBackend", function(name, provider)
fi;
HAP_VORONOI_BACKENDS.(name) := provider;
end);
-BindGlobal("CallVoronoiBackend", function(name, kind, n, d)
+BindGlobal("CallVoronoiBackend", function(name, request)
+ if not IsRecord(request) then
+ Error("a Voronoi request must be a record");
+ fi;
if not IsString(name) or not IsBound(HAP_VORONOI_BACKENDS.(name)) then
Error("unknown Voronoi backend; register it with RegisterVoronoiBackend");
fi;
- return HAP_VORONOI_BACKENDS.(name)(kind,n,d);
+ return HAP_VORONOI_BACKENDS.(name)(request);
+end);
+BindGlobal("VoronoiComplex", function(request)
+ local backend;
+ backend := ValueOption("backend");
+ if backend=fail then
+ Error("specify a registered Voronoi backend with : backend := name");
+ fi;
+ return CallVoronoiBackend(backend,request);
+end);
+BindGlobal("HAPQuadraticVoronoiRequest", function(kind,n,d)
+ return rec(group:=kind,rank:=n,
+ field:=rec(kind:="quadratic",radicand:=d),
+ order:=rec(kind:="maximal"),lattice:=rec(kind:="free"));
end);
############################################################
@@ -27,7 +43,7 @@ local file,file2,m,k,G,backend;
backend:=ValueOption("backend");
if backend<>fail and backend<>"magma" then
-return CallVoronoiBackend(backend,"GL",n,d);
+return CallVoronoiBackend(backend,HAPQuadraticVoronoiRequest("GL",n,d));
fi;
if d<0 then
@@ -71,7 +87,7 @@ local file,file2,m,k,G,backend;
backend:=ValueOption("backend");
if backend<>fail and backend<>"magma" then
-return CallVoronoiBackend(backend,"SL",n,d);
+return CallVoronoiBackend(backend,HAPQuadraticVoronoiRequest("SL",n,d));
fi;
if d<0 then
diff --git a/tst/testextra/voronoi-backends.tst b/tst/testextra/voronoi-backends.tst
index 26a7d89d..7d1dff5d 100644
--- a/tst/testextra/voronoi-backends.tst
+++ b/tst/testextra/voronoi-backends.tst
@@ -1,18 +1,30 @@
gap> START_TEST("voronoi-backends.tst");
-gap> RegisterVoronoiBackend("hap-test", function(kind,n,d)
-> return [kind,n,d]; end);
-gap> Print(VoronoiComplexGL(2,-7 : backend := "hap-test"),"\n");
+gap> RegisterVoronoiBackend("hap-test", request -> request);
+gap> haprequest := rec(group:="unit",rank:=3,
+> field:=rec(polynomial:=[-1,-1,0,1]),order:=rec(basis:=[1,2,3]),
+> lattice:=rec(ideals:=["a","b","c"]),extra:=rec(example:=true));;
+gap> Print(IsIdenticalObj(haprequest,VoronoiComplex(haprequest : backend := "hap-test")),"\n");
+true
+gap> haprequest := VoronoiComplexGL(2,-7 : backend := "hap-test");;
+gap> Print([haprequest.group,haprequest.rank,haprequest.field.radicand],"\n");
[ "GL", 2, -7 ]
-gap> Print(VoronoiComplexSL(2,-1 : backend := "hap-test"),"\n");
+gap> Print([haprequest.field.kind,haprequest.order.kind,haprequest.lattice.kind],"\n");
+[ "quadratic", "maximal", "free" ]
+gap> haprequest := VoronoiComplexSL(2,-1 : backend := "hap-test");;
+gap> Print([haprequest.group,haprequest.rank,haprequest.field.radicand],"\n");
[ "SL", 2, -1 ]
-gap> Print(VoronoiComplexGL(3,5 : backend := "hap-test"),"\n");
+gap> haprequest := VoronoiComplexGL(3,5 : backend := "hap-test");;
+gap> Print([haprequest.group,haprequest.rank,haprequest.field.radicand],"\n");
[ "GL", 3, 5 ]
-gap> RegisterVoronoiBackend("hap-test-fail", function(kind,n,d)
-> return fail; end);
+gap> RegisterVoronoiBackend("hap-test-fail", request -> fail);
+gap> Print(VoronoiComplex(rec() : backend := "hap-test-fail"),"\n");
+fail
gap> Print(VoronoiComplexSL(2,5 : backend := "hap-test-fail"),"\n");
fail
-gap> Print(VoronoiComplexGL(2,-7 : backend := "hap-test"),"\n");
-[ "GL", 2, -7 ]
+gap> RegisterVoronoiBackend("hap-test", request -> request.rank);
+gap> Print(VoronoiComplexGL(4,5 : backend := "hap-test"),"\n");
+4
+gap> Unbind(haprequest);
gap> Unbind(HAP_VORONOI_BACKENDS.("hap-test"));
gap> Unbind(HAP_VORONOI_BACKENDS.("hap-test-fail"));
gap> STOP_TEST("voronoi-backends.tst", 1000);
diff --git a/tutorial/tutorialBianchi.xml b/tutorial/tutorialBianchi.xml
index dae372e0..2c2d7e01 100644
--- a/tutorial/tutorialBianchi.xml
+++ b/tutorial/tutorialBianchi.xml
@@ -410,27 +410,48 @@ The signs in the boundary maps of the free resolution are delicate. Another easy
Optional Voronoi backends
-The functions VoronoiComplexGL(n,d) and
-VoronoiComplexSL(n,d) accept the option backend.
-Without this option, or with backend := "magma", they use the
-Magma implementation described above. An external package can register an
-alternative using RegisterVoronoiBackend(name, provider). The name
-must be a nonempty string other than "magma", and
-provider(kind,n,d) must be a function. Its first argument is
-"GL" or "SL"; d is a squarefree radicand, not a
-field discriminant. The provider returns a HAP non-free resolution for the
-requested group, or fail if the input is unsupported. Failure never
-silently falls back to Magma. Registering an existing name replaces its
-provider.
-For example, a Julia package running with GAP.jl can register an
-in-process callback under the name "julia". Once registered,
-VoronoiComplexGL(2,-7 : backend := "julia") calls that provider.
-HAP itself does not install or load Julia. An unregistered backend name is
-an error. Providers must supply complete stabilizers, the orientation action
+An external package can register a provider using
+RegisterVoronoiBackend(name, provider), where provider is a
+function of one argument. The name must be a nonempty string other than
+"magma". Registering an existing name replaces its provider.
+
+The general entry point is VoronoiComplex(request : backend := name).
+It requires a record and an explicitly selected registered provider. HAP
+passes the record unchanged; the provider interprets and validates its
+contents. The interface imposes no particular field degree, rank or group
+family. For example, a provider may accept the following description:
+
+Field polynomials, order bases, lattice ideals or subgroup data can be
+included when a provider supports them. This does not assert that any
+particular provider implements those cases. A provider returns a HAP
+non-free resolution or fail for an unsupported request; it may also
+raise an error explaining the limitation. Failure never falls back to Magma.
+
+The existing functions VoronoiComplexGL(n,d) and
+VoronoiComplexSL(n,d) also accept the option backend.
+When selecting a registered provider, these construct a record with
+group equal to "GL" or "SL", rank := n,
+field := rec(kind := "quadratic", radicand := d),
+order := rec(kind := "maximal") and
+lattice := rec(kind := "free"). Here d is a squarefree
+radicand, not a field discriminant. Without the backend option, or with
+backend := "magma", these two convenience functions retain the
+Magma implementation described above.
+
+A Julia package running with GAP.jl can register an in-process callback
+under the name "julia". HAP itself does not install or load Julia.
+An unregistered backend name is an error. Providers must supply complete stabilizers, the orientation action
and equivariant boundaries; perfect forms or a group presentation alone
do not suffice for higher homology. Standard HAP operations such as
FreeGResolution and TensorWithIntegers then apply to the
returned object. A contracting homotopy is a separate capability and must
-not be inferred merely from the existence of a resolution.
+not be inferred merely from the existence of a resolution.
From 4d305d3e48069cc684ad5c5b935a49bd4223046b Mon Sep 17 00:00:00 2001
From: Dima Pasechnik
Date: Wed, 21 Oct 2026 17:11:54 -0500
Subject: [PATCH 3/4] Exercise the Julia Voronoi provider in CI with a small
homology example
---
.github/scripts/voronoi-julia.jl | 49 ++++++++++++++++++++++++++++++++
.github/workflows/CI.yml | 32 +++++++++++++++++++++
2 files changed, 81 insertions(+)
create mode 100644 .github/scripts/voronoi-julia.jl
diff --git a/.github/scripts/voronoi-julia.jl b/.github/scripts/voronoi-julia.jl
new file mode 100644
index 00000000..b0ace8aa
--- /dev/null
+++ b/.github/scripts/voronoi-julia.jl
@@ -0,0 +1,49 @@
+# Run with the ArithmeticVoronoi environment pinned in ../workflows/CI.yml:
+# julia --startup-file=no --project=/path/to/arithmetic_voronoi \
+# .github/scripts/voronoi-julia.jl
+using ArithmeticVoronoi, Oscar, Test
+import Oscar: GAP
+
+# Test this HAP checkout, including its startup registration. Check before
+# enabling the provider so the adapter's compatibility wrapper cannot mask
+# missing entry points in HAP itself.
+hap_root = realpath(joinpath(@__DIR__, "..", ".."))
+@assert GAP.Packages.load(hap_root)
+@assert realpath(GAP.Packages.locate_package("hap")) == hap_root
+@assert GAP.Globals.IsBoundGlobal(GAP.GapObj("RegisterVoronoiBackend"))
+@assert GAP.Globals.IsBoundGlobal(GAP.GapObj("VoronoiComplex"))
+dispatcher = GAP.Globals.VoronoiComplex
+println("HAP checkout: ", hap_root)
+println("Julia ", VERSION, "; OSCAR ", pkgversion(Oscar), "; GAP ", GAP.GAP_VERSION)
+
+@testset "Julia provider: GL2 over O_(Q(sqrt(-7)))" begin
+ enable_hap_julia_backend(; progress=true, resolution_length=3)
+ @test GAP.Globals.IsIdenticalObj(dispatcher, GAP.Globals.VoronoiComplex)
+
+ request = Dict(
+ "group" => "GL", "rank" => 2,
+ "field" => Dict("kind" => "quadratic", "discriminant" => -7),
+ "order" => Dict("kind" => "maximal"),
+ "lattice" => Dict("kind" => "free"),
+ )
+ println("Request: ", request)
+ flush(stdout)
+ construct = GAP.evalstr("request -> VoronoiComplex(request : backend := \"julia\")")
+ # A fresh native enumeration, without Magma, stored complexes or a C++ build.
+ K = construct(GAP.GapObj(request; recursive=true))
+ @test GAP.Globals.IsHapNonFreeResolution(K)
+ cells = Int.(GAP.gap_to_julia(
+ GAP.evalstr("K -> List([0..2], n -> K!.dimension(n))")(K)))
+ @test cells == [1, 2, 1]
+ println("Cell orbits in dimensions 0, 1, 2: ", cells)
+
+ R = GAP.Globals.FreeGResolution(K, 3)
+ C = GAP.Globals.TensorWithIntegers(R)
+ # These invariants were independently checked with Magma/VMH;
+ # neither the example nor CI needs Magma to compute them.
+ for (degree, expected) in ((1, [2, 2, 2]), (2, [2, 12]))
+ invariants = sort(Int.(GAP.gap_to_julia(GAP.Globals.Homology(C, degree))))
+ println("H", degree, " invariant factors: ", invariants)
+ @test invariants == expected
+ end
+end
diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index e709d5cd..e6d3aeaf 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -43,3 +43,35 @@ jobs:
- uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
+
+ julia-voronoi:
+ name: Julia Voronoi example (GL2, D=-7)
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ env:
+ JULIA_NUM_THREADS: '1'
+ JULIA_NUM_PRECOMPILE_TASKS: '1'
+ OPENBLAS_NUM_THREADS: '1'
+ steps:
+ - uses: actions/checkout@v7
+ - name: Check out the Julia provider and its pinned environment
+ uses: actions/checkout@v7
+ with:
+ repository: dimpase/arithmetic_voronoi
+ ref: 007df75debde04c7469124891ac78d555d5de20b
+ path: arithmetic-voronoi
+ - uses: julia-actions/setup-julia@v3
+ with:
+ version: '1.13.2'
+ - uses: julia-actions/cache@v3
+ with:
+ delete-old-caches: 'false'
+ - name: Install Julia dependencies
+ run: >-
+ julia --startup-file=no --project=arithmetic-voronoi
+ -e 'using Pkg; Pkg.instantiate(;allow_autoprecomp=false)'
+ - name: Construct a fresh complex and check integral H1 and H2
+ run: >-
+ julia --startup-file=no --project=arithmetic-voronoi
+ .github/scripts/voronoi-julia.jl
From f1347b0542009bbfd8d9a1194318d2cb4d59f84c Mon Sep 17 00:00:00 2001
From: Dima Pasechnik
Date: Wed, 21 Oct 2026 20:17:59 -0500
Subject: [PATCH 4/4] Make Julia precompilation explicit and use its default
parallelism
---
.github/scripts/voronoi-julia.jl | 5 +++++
.github/workflows/CI.yml | 5 ++++-
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/.github/scripts/voronoi-julia.jl b/.github/scripts/voronoi-julia.jl
index b0ace8aa..bd2c519f 100644
--- a/.github/scripts/voronoi-julia.jl
+++ b/.github/scripts/voronoi-julia.jl
@@ -1,8 +1,13 @@
# Run with the ArithmeticVoronoi environment pinned in ../workflows/CI.yml:
# julia --startup-file=no --project=/path/to/arithmetic_voronoi \
# .github/scripts/voronoi-julia.jl
+println("Loading the Julia provider and its dependencies...")
+flush(stdout)
+package_load_started = time_ns()
using ArithmeticVoronoi, Oscar, Test
import Oscar: GAP
+println("Package loading: ", round((time_ns() - package_load_started) / 1e9; digits=2), " s")
+flush(stdout)
# Test this HAP checkout, including its startup registration. Check before
# enabling the provider so the adapter's compatibility wrapper cannot mask
diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index e6d3aeaf..b9022ef4 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -51,7 +51,6 @@ jobs:
contents: read
env:
JULIA_NUM_THREADS: '1'
- JULIA_NUM_PRECOMPILE_TASKS: '1'
OPENBLAS_NUM_THREADS: '1'
steps:
- uses: actions/checkout@v7
@@ -71,6 +70,10 @@ jobs:
run: >-
julia --startup-file=no --project=arithmetic-voronoi
-e 'using Pkg; Pkg.instantiate(;allow_autoprecomp=false)'
+ - name: Precompile Julia provider
+ run: >-
+ julia --startup-file=no --project=arithmetic-voronoi
+ -e 'using Pkg; Pkg.precompile("ArithmeticVoronoi"; strict=true, timing=true)'
- name: Construct a fresh complex and check integral H1 and H2
run: >-
julia --startup-file=no --project=arithmetic-voronoi