diff --git a/.github/scripts/voronoi-julia.jl b/.github/scripts/voronoi-julia.jl new file mode 100644 index 00000000..bd2c519f --- /dev/null +++ b/.github/scripts/voronoi-julia.jl @@ -0,0 +1,54 @@ +# 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 +# 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..b9022ef4 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -43,3 +43,38 @@ 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' + 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: 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 + .github/scripts/voronoi-julia.jl diff --git a/lib/Voronoi/seb.gi b/lib/Voronoi/seb.gi index f8663298..de815d70 100644 --- a/lib/Voronoi/seb.gi +++ b/lib/Voronoi/seb.gi @@ -1,10 +1,50 @@ HAP_KOMPLEKS123:=fail; DIMS:=0;; #THIS IS NOT ALLOWED! CHANGE IT!! +# 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" + 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, 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)(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); + ############################################################ ############################################################ 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,HAPQuadraticVoronoiRequest("GL",n,d)); +fi; if d<0 then file:="~/pkg/Voronoi/VMH-ImaginaryQuadraticNumberFields/Magma/BasicData.m"; @@ -43,7 +83,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,HAPQuadraticVoronoiRequest("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..7d1dff5d --- /dev/null +++ b/tst/testextra/voronoi-backends.tst @@ -0,0 +1,30 @@ +gap> START_TEST("voronoi-backends.tst"); +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([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> 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", request -> fail); +gap> Print(VoronoiComplex(rec() : backend := "hap-test-fail"),"\n"); +fail +gap> Print(VoronoiComplexSL(2,5 : backend := "hap-test-fail"),"\n"); +fail +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 a0cf35ac..2c2d7e01 100644 --- a/tutorial/tutorialBianchi.xml +++ b/tutorial/tutorialBianchi.xml @@ -407,5 +407,51 @@ The signs in the boundary maps of the free resolution are delicate. Another easy <#Include SYSTEM "tutex/bianchi.17.txt"> + + +
Optional Voronoi backends +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.