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