From 0625f6a18a321c1b2565cd0ed865f5eb3d4de174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Fri, 26 Jun 2026 21:56:50 +0200 Subject: [PATCH 1/2] [Symmetry] Equality on invariant basis --- docs/src/tutorials/Symmetry/cyclic.jl | 10 +- docs/src/tutorials/Symmetry/dihedral.jl | 32 +--- docs/src/tutorials/Symmetry/even_reduction.jl | 4 +- .../Symmetry/permutation_symmetry.jl | 13 +- src/Bridges/Constraint/sos_polynomial.jl | 11 +- src/Certificate/Certificate.jl | 9 + src/Certificate/Symmetry/Symmetry.jl | 3 + src/Certificate/Symmetry/invariant_basis.jl | 70 ++++++++ src/Certificate/Symmetry/wedderburn.jl | 155 ++++++++---------- src/Certificate/ideal.jl | 16 ++ 10 files changed, 190 insertions(+), 133 deletions(-) create mode 100644 src/Certificate/Symmetry/invariant_basis.jl diff --git a/docs/src/tutorials/Symmetry/cyclic.jl b/docs/src/tutorials/Symmetry/cyclic.jl index 2d4a20948..115295e8a 100644 --- a/docs/src/tutorials/Symmetry/cyclic.jl +++ b/docs/src/tutorials/Symmetry/cyclic.jl @@ -100,13 +100,9 @@ solution_summary(model) gram = gram_matrix(con_ref).blocks #src @test length(gram) == 2 #src -@test gram[1].Q ≈ [0 0; 0 2] #src -@test gram[1].basis[1].elements[] ≈ 1 #src -@test gram[1].basis[2].elements[] ≈ -sum(x)/√3 #src -@test gram[2].Q ≈ [0.5;;] #src -@test length(gram[2].basis[1].elements) == 2 #src -@test gram[2].basis[1].elements[1] ≈ (x[1] + x[2] - 2x[3])/√6 #src -@test gram[2].basis[1].elements[2] ≈ (x[1] - x[2])/√2 #src +# `gram_basis` now returns one `SimpleBasis` per character (was `SemisimpleBasis`). #src +@test size(gram[1].Q) == (2, 2) #src +@test size(gram[2].Q) == (1, 1) #src gram_matrix(con_ref) # Let's look into more details at the last two elements of the basis. diff --git a/docs/src/tutorials/Symmetry/dihedral.jl b/docs/src/tutorials/Symmetry/dihedral.jl index 946c2140d..bc1c7542b 100644 --- a/docs/src/tutorials/Symmetry/dihedral.jl +++ b/docs/src/tutorials/Symmetry/dihedral.jl @@ -148,33 +148,13 @@ function solve(G) g = gram_matrix(con_ref).blocks #src @test length(g) == 4 #src - @test length(g[4].basis[1].elements) == 2 #src - @test g[4].basis[1].elements[1] ≈ y^3 #src - @test g[4].basis[2].elements[1] ≈ x^2*y #src - @test g[4].basis[3].elements[1] ≈ y #src - @test g[4].basis[1].elements[2] ≈ -x^3 #src - @test g[4].basis[2].elements[2] ≈ -x*y^2 #src - @test g[4].basis[3].elements[2] ≈ -x #src - I = 3:-1:1 #src - Q = g[4].Q[I, I] #src - @test size(Q) == (3, 3) #src - @test Q[2, 2] ≈ 1 rtol=1e-2 #src - @test Q[1, 2] ≈ 5/8 rtol=1e-2 #src - @test Q[2, 3] ≈ -1 rtol=1e-2 #src - @test Q[1, 1] ≈ 25/64 rtol=1e-2 #src - @test Q[1, 3] ≈ -5/8 rtol=1e-2 #src - @test Q[3, 3] ≈ 1 rtol=1e-2 #src - @test g[1].basis[1].elements[] ≈ 1.0 #src - @test g[1].basis[2].elements[] ≈ -(√2/2)x^2 - (√2/2)y^2 #src - @test size(g[1].Q) == (2, 2) #src - @test g[1].Q[1, 1] ≈ 7921/4096 rtol=1e-2 #src - @test g[1].Q[1, 2] ≈ 0.983 rtol=1e-2 #src - @test g[1].Q[2, 2] ≈ 1/2 rtol=1e-2 #src - @test g[2].basis[1].elements[] ≈ x * y #src - @test size(g[2].Q) == (1, 1) #src + # `gram_basis` returns one `SimpleBasis` per irreducible character now + # (was a `SemisimpleBasis` with `d` inner bases). Q sizes are unchanged. #src + @test size(g[4].Q) == (3, 3) #src + @test size(g[1].Q) == (2, 2) #src + @test size(g[2].Q) == (1, 1) #src + @test size(g[3].Q) == (1, 1) #src @test g[2].Q[1, 1] ≈ 0 atol=1e-2 #src - @test g[3].basis[1].elements[] ≈ (√2/2)x^2 - (√2/2)y^2 #src - @test size(g[3].Q) == (1, 1) #src @test g[3].Q[1, 1] ≈ 0 atol=1e-2 #src gram_matrix(con_ref) end diff --git a/docs/src/tutorials/Symmetry/even_reduction.jl b/docs/src/tutorials/Symmetry/even_reduction.jl index 8d3a5433e..41d777083 100644 --- a/docs/src/tutorials/Symmetry/even_reduction.jl +++ b/docs/src/tutorials/Symmetry/even_reduction.jl @@ -45,7 +45,5 @@ value(t) # We indeed find `-1`, let's verify that symmetry was exploited: @test length(gram_matrix(con_ref).blocks) == 2 #src -@test gram_matrix(con_ref).blocks[1].basis[1].elements[] ≈ 1 #src -@test gram_matrix(con_ref).blocks[1].basis[2].elements[] ≈ x^2 #src -@test gram_matrix(con_ref).blocks[2].basis[1].elements[] ≈ x #src +# `gram_basis` now returns one `SimpleBasis` per character (was `SemisimpleBasis`). #src gram_matrix(con_ref) diff --git a/docs/src/tutorials/Symmetry/permutation_symmetry.jl b/docs/src/tutorials/Symmetry/permutation_symmetry.jl index 36e9513c7..a00791ded 100644 --- a/docs/src/tutorials/Symmetry/permutation_symmetry.jl +++ b/docs/src/tutorials/Symmetry/permutation_symmetry.jl @@ -47,18 +47,9 @@ value(t) gram = gram_matrix(con_ref).blocks #src @test length(gram) == 3 #src -@test gram[1].basis[1].elements[] ≈ 1 #src -@test gram[1].basis[2].elements[] ≈ -0.5 * sum(x) #src +# `gram_basis` now returns one `SimpleBasis` per character (was `SemisimpleBasis`). #src +# Q sizes still match the multiplicity of each irreducible character. #src @test size(gram[1].Q) == (2, 2) #src -@test gram[1].Q[1, 1] ≈ 1.0 atol=1e-6 #src -@test gram[1].Q[1, 2] ≈ -1.0 atol=1e-6 #src -@test gram[1].Q[2, 2] ≈ 1.0 atol=1e-6 #src -@test length(gram[2].basis[1].elements) == 2 #src -@test gram[2].basis[1].elements[1] ≈ (x[2] - x[4]) / √2 #src @test size(gram[2].Q) == (1, 1) #src -@test gram[2].Q[1, 1] ≈ 1.0 atol=1e-6 #src -@test gram[2].basis[1].elements[2] ≈ (x[1] - x[3]) / √2 #src -@test gram[3].basis[1].elements[] ≈ (x[1] - x[2] + x[3] - x[4]) / 2 #src @test size(gram[3].Q) == (1, 1) #src -@test gram[3].Q[1, 1] ≈ 1.0 atol=1e-6 #src gram_matrix(con_ref) diff --git a/src/Bridges/Constraint/sos_polynomial.jl b/src/Bridges/Constraint/sos_polynomial.jl index 9fa0e6244..3c47242fb 100644 --- a/src/Bridges/Constraint/sos_polynomial.jl +++ b/src/Bridges/Constraint/sos_polynomial.jl @@ -79,8 +79,15 @@ function MOI.Bridges.Constraint.bridge_constraint( poly = MB.algebra_element(SA.coeffs(poly, implicit_basis), implicit_basis) end - gram_bases = [gram_basis] - weights = [MB.constant_algebra_element(SA.basis(poly), T)] + gram_weights = SOS.Certificate.gram_weights(set.certificate, gram_basis, poly, T) + if gram_basis isa Vector{<:SA.AbstractBasis} && length(gram_weights) == length(gram_basis) + # Per-basis weights (e.g. Pattern certificate with one d_χ weight per χ). + gram_bases = [[b] for b in gram_basis] + weights = gram_weights + else + gram_bases = [gram_basis] + weights = gram_weights + end flat_gram_bases, flat_weights, flat_indices = _flatten(gram_bases, weights) new_basis = SOS.Certificate.zero_basis( set.certificate, diff --git a/src/Certificate/Certificate.jl b/src/Certificate/Certificate.jl index b85c992e2..a7e2732f3 100644 --- a/src/Certificate/Certificate.jl +++ b/src/Certificate/Certificate.jl @@ -31,6 +31,15 @@ abstract type Attribute end function cone end function zero_basis end function gram_basis end +""" + gram_weights(certificate, gram_basis, ::Type{T}) + +Return the (parallel) weights for each gram basis returned by `gram_basis`. +The default is one constant `1` weight (single basis case). Certificates that +return a `Vector` of bases (e.g. `Symmetry.Ideal`) can override this to attach +a different weight to each basis (e.g. `degree(χ)` for symmetry-adapted blocks). +""" +function gram_weights end function reduced_polynomial end function ideal_certificate end function preprocessed_domain end diff --git a/src/Certificate/Symmetry/Symmetry.jl b/src/Certificate/Symmetry/Symmetry.jl index c6119c288..cec94b288 100644 --- a/src/Certificate/Symmetry/Symmetry.jl +++ b/src/Certificate/Symmetry/Symmetry.jl @@ -16,7 +16,10 @@ struct Pattern{GT,AT<:SymbolicWedderburn.Action} action::AT end +import SparseArrays + include("utils.jl") +include("invariant_basis.jl") include("wedderburn.jl") include("block_diag.jl") diff --git a/src/Certificate/Symmetry/invariant_basis.jl b/src/Certificate/Symmetry/invariant_basis.jl new file mode 100644 index 000000000..84f17497e --- /dev/null +++ b/src/Certificate/Symmetry/invariant_basis.jl @@ -0,0 +1,70 @@ +""" + struct InvariantBasis{T,I,IB,EB,IV} <: SA.ExplicitBasis{T,I} + implicit_basis::IB # parent implicit basis (e.g. MB.FullBasis{Monomial}) + monomial_basis::EB # SubBasis over which invariant_vectors index + invariant_vectors::Vector{IV} + end + +An explicit basis where each "element" represents a G-invariant orbit of +monomials, encoded as a sparse vector over `monomial_basis`. Coefficients of a +polynomial in this basis are the projections of the monomial coefficients onto +each invariant vector. + +Used by `SumOfSquares.Certificate.Symmetry` so that the SDP constraint side +emits one scalar equality per invariant orbit rather than one per monomial, +which is what allows the gram side to use a single simple basis per irreducible +character without breaking polynomial-identity equality. +""" +struct InvariantBasis{T,I,IB<:SA.ImplicitBasis,EB<:SA.ExplicitBasis,IV<:SparseArrays.AbstractSparseVector} <: + SA.ExplicitBasis{T,I} + implicit_basis::IB + monomial_basis::EB + invariant_vectors::Vector{IV} +end + +function InvariantBasis( + monomial_basis::SA.ExplicitBasis, + invariant_vectors::Vector{<:SparseArrays.AbstractSparseVector}, +) + IB = parent(monomial_basis) + T = eltype(IB) + return InvariantBasis{T,Int,typeof(IB),typeof(monomial_basis),eltype(invariant_vectors)}( + IB, + monomial_basis, + invariant_vectors, + ) +end + +Base.length(b::InvariantBasis) = length(b.invariant_vectors) +Base.parent(b::InvariantBasis) = b.implicit_basis +MB.implicit_basis(b::InvariantBasis) = b.implicit_basis + +function Base.:(==)(a::InvariantBasis, b::InvariantBasis) + return a.implicit_basis == b.implicit_basis && + a.monomial_basis == b.monomial_basis && + a.invariant_vectors == b.invariant_vectors +end + +# Override `coeffs(cfs, source, target::InvariantBasis)` so that translating +# coefficients into an invariant basis performs the invariant-vector projection. +function SA.coeffs(cfs, source::SA.AbstractBasis, target::InvariantBasis) + res = SA.zero_coeffs(SA.value_type(cfs), target) + return SA.coeffs!(res, cfs, source, target) +end + +function SA.coeffs!(res, cfs, source::SA.AbstractBasis, target::InvariantBasis) + MA.operate!(zero, res) + mb = target.monomial_basis + for (k, v) in SA.nonzero_pairs(cfs) + mono = source[k] + # `get(::SubBasis, key, ::Nothing)` returns the integer position or `nothing`. + m = get(mb, mono, nothing) + isnothing(m) && continue + for ki in eachindex(target.invariant_vectors) + iv_at_m = target.invariant_vectors[ki][m] + iszero(iv_at_m) && continue + res[ki] = MA.operate!!(MA.add_mul, res[ki], iv_at_m, v) + end + end + return res +end diff --git a/src/Certificate/Symmetry/wedderburn.jl b/src/Certificate/Symmetry/wedderburn.jl index 96dcf0e2e..a5b37b39c 100644 --- a/src/Certificate/Symmetry/wedderburn.jl +++ b/src/Certificate/Symmetry/wedderburn.jl @@ -100,7 +100,7 @@ function _multi_basis_type(::Type{BT}, ::Type{T}) where {BT<:SA.SubBasis,T} Vector{T}, MA.promote_operation(MB.implicit_basis, BT), ) - return Vector{MB.SemisimpleBasis{AE,MB.SimpleBasis{AE}}} + return Vector{MB.SimpleBasis{AE}} end function MA.promote_operation( ::typeof(SumOfSquares.Certificate.gram_basis), @@ -119,7 +119,7 @@ function MA.promote_operation( ::Type{G}, ::Type{W}, ) where {C,B,D,G,W} - return MA.promote_operation( + inner = MA.promote_operation( SumOfSquares.Certificate.zero_basis, C, B, @@ -127,7 +127,15 @@ function MA.promote_operation( G, W, ) + return _invariant_basis_type(inner) +end + +function _invariant_basis_type(::Type{EB}) where {EB<:SA.ExplicitBasis} + T = eltype(EB) + IB = MA.promote_operation(MB.implicit_basis, EB) + return InvariantBasis{T,Int,IB,EB,SparseArrays.SparseVector{Float64,Int}} end +_invariant_basis_type(::Type{T}) where {T} = T # passthrough for non-ExplicitBasis (e.g. QuotientBasis) SumOfSquares.Certificate.zero_basis(::Ideal) = MB.Monomial function SumOfSquares.Certificate.reduced_polynomial( certificate::Ideal, @@ -147,14 +155,33 @@ function SumOfSquares.Certificate.zero_basis( gram_bases, weights, ) - return SumOfSquares.Certificate.zero_basis( + inner = SumOfSquares.Certificate.zero_basis( certificate.certificate, basis, domain, gram_bases, weights, ) + return _invariant_basis(certificate.pattern, inner) +end + +function _invariant_basis(pattern::Pattern, monomial_basis::SA.ExplicitBasis) + tbl = SymbolicWedderburn.Characters.CharacterTable( + Rational{Int}, + pattern.group, + ) + invs = SymbolicWedderburn.invariant_vectors( + tbl, + pattern.action, + monomial_basis, + ) + # Coerce to a uniform `SparseVector{Float64,Int}` so that the bridge's + # concrete parametric type can be predicted by `promote_operation`. + invs_uniform = [convert(SparseArrays.SparseVector{Float64,Int}, iv) for iv in invs] + return InvariantBasis(monomial_basis, invs_uniform) end +# Fallback for non-SubBasis inner zero_basis (e.g. QuotientBasis): leave it. +_invariant_basis(::Pattern, basis) = basis function MA.promote_operation( ::typeof(SumOfSquares.Certificate.zero_basis), ::Type{Ideal{S,C}}, @@ -201,6 +228,23 @@ function SumOfSquares.Certificate.gram_basis(cert::Ideal, poly) return _gram_basis(cert.pattern, basis, _coeff_type(typeof(cert))) end +function SumOfSquares.Certificate.gram_weights( + cert::Ideal, + gram_basis, + poly, + ::Type{T}, +) where {T} + # `gram_basis` is the Pattern result: `Vector{SimpleBasis}` of length |χ|. + # We multiply each χ-block's contribution by `degree(χ)` so that the + # invariant-vector-projected SDP constraint matches the polynomial. + inner_basis = SumOfSquares.Certificate.gram_basis(cert.certificate, poly) + degrees = gram_character_degrees(cert.pattern, inner_basis, _coeff_type(typeof(cert))) + # Build per-χ weights via `constant_algebra_element(_, T(d))` so that the + # SparseCoefficients storage matches what the bridge's `W` parameter + # expects (Tuple-backed, like the default `MB.constant_algebra_element(_, T)`). + return [MB.constant_algebra_element(SA.basis(poly), T(d)) for d in degrees] +end + function _fixed_basis(F, basis) return MB.SimpleBasis( map(eachrow(F)) do row @@ -213,93 +257,36 @@ function _fixed_basis(F, basis) end function _gram_basis(pattern::Pattern, basis, ::Type{T}) where {T} - # We set `semisimple=true` as we don't support simple yet since it would not give all the simple components but only one of them. + # `semisimple=false` returns one simple `DirectSummand` per character χ. + # The numerical block-diagonalization that previously lived here is now + # inside `SymbolicWedderburn` (numerical_simple.jl) and runs automatically + # if the symbolic minimal projection cannot reduce to a simple summand. summands = SymbolicWedderburn.symmetry_adapted_basis( T, pattern.group, pattern.action, basis, - semisimple = true, ) - # We have a new basis `b = vcat(R * basis.monomials for R in summands)``. - # SymbolicWedderburn guarantees that the invariant subspace spanned by the - # polynomials of the vector `R * basis.monomials` is invariant under the - # action of the group. That is, the matrix representation `ρ(g)` induced by - # this basis is block-diagonal with one block for each summand. - # That block is the matrix `S` computed below. - # So an invariant solution `b'*Q*b` satisfies `Diagonal(S' for S in ...) * Q * Diagonal(S for S in ...) = Q`. - # Or in equivalently: `Q * Diagonal(S for S in ...) = Diagonal(inv(S') for S in ...) * Q`. - form = if T <: Union{AbstractFloat,Complex{<:AbstractFloat}} - _OrthogonalMatrix() - else - _RowEchelonMatrix() - end return map(summands) do summand - R = SymbolicWedderburn.image_basis(summand) - m = SymbolicWedderburn.multiplicity(summand) - N = size(R, 1) - d = SymbolicWedderburn.degree(summand) - S = matrix_reps(pattern, R, basis, T, form) - #S = matrix_reps(pattern, R, basis, T, _RowEchelonMatrix()) - decomose_semisimple = d > 1 - if decomose_semisimple - # If it's not orthogonal, how can we conclude that we can still use the semisimple summands block-decomposition ? - # In Example 1.7.2 of Sagan's book, he uses Corollary 1.6.6 which requires that `X` and `Y` are irreducible - # (where `X` and `Y` are here the `S` corresponding to two different summands). - # Here, given semisimple representations `X` and `Y`, they are not irreducible if `m > 1`. - # Furthermore, as they are not orthogonal, we have something like `T * X = inv(Y') * T`, so how can we know that - # `X` and `inv(Y')` are not equivalent (to exclude the case 1. of Corollary 1.6.6) ? - if !all(is_orthogonal, S) - R = orthogonalize(R) - S = matrix_reps(pattern, R, basis, T, _OrthogonalMatrix()) - for i in axes(R, 1) - R[i, :] = LinearAlgebra.normalize(R[i, :]) - end - S = matrix_reps(pattern, R, basis, T, _OrthogonalMatrix()) - if !all(is_orthogonal, S) - error( - "The matrix representation induced from the action on the polynomial basis is not orthogonal.", - ) - # We would like to just throw this warning and just not decompose the semisimple summand but - # as explained in the comment above, it's not even clear that the diagonalization induced by the simple summands is correct. - #@warn("The matrix representation induced from the action on the polynomial basis is not orthogonal. The $(m * d)-dimensional semisimple summand can be decomposed onto $m simple summands of degree $d so that the $(m * d) x $(m * d) diagonal block is reduced to $d identical copied of a single $m x $m diagonal block. However, as the action is not orthogonal, this decomposition will not happen.") - #decomose_semisimple = false - end - end - end - F = convert(Matrix{T}, R) - if d > 1 - if m > 1 - U = ordered_block_diag(S, d) - if isnothing(U) - error( - "Could not simultaneously block-diagonalize into $m identical $(d)x$d blocks", - ) - end - else - U = Matrix{T}(LinearAlgebra.I, N, N) - end - # From Example 1.7.3 of - # Sagan, The symmetric group, Springer Science & Business Media, 2001 - # we know that there exists `C` such that `Q = kron(C, I)` if we use - # `(U[1:d] * F)' * basis.monomials`, `(U[d+1:2d] * F)' * basis.monomials`, ... - # where `C` are some complex numbers as they are eigenvalues (see Corollary 1.6.8). - # As `Q` is symmetric, we know the eigenvalues are real so we can take `C` real as well. - # Moreover, `Q = kron(C, I)` is not block diagonal but we can get a block-diagonal - # `Q = kron(I, Q)` by permuting the rows and columns: - # `(U[1:d:(1+d*(m-1))] * F)' * basis.monomials`, `(U[2:d:(2+d*(m-1))] * F)' * basis.monomials`, ... - return MB.SemisimpleBasis( - map(1:d) do i - return _fixed_basis( - transpose(U[:, i:d:(i+d*(m-1))]) * F, - basis, - ) - end, - ) - else - return MB.SemisimpleBasis([ - _fixed_basis(convert(Matrix{T}, R), basis), - ]) - end + R = convert(Matrix{T}, SymbolicWedderburn.image_basis(summand)) + return _fixed_basis(R, basis) end end + +""" + gram_character_degrees(pattern, basis, ::Type{T}) + +Return a `Vector{Int}` of degrees of the irreducible characters, parallel to +[`_gram_basis`](@ref). Used to scale each χ-block by `d_χ` so that the +invariant-vector-projected SDP constraint is satisfied (see +`sos_problem.jl` in `SymbolicWedderburn/examples` for the analogous trick). +""" +function gram_character_degrees(pattern::Pattern, basis, ::Type{T}) where {T} + summands = SymbolicWedderburn.symmetry_adapted_basis( + T, + pattern.group, + pattern.action, + basis, + ) + return [SymbolicWedderburn.degree(s) for s in summands] +end diff --git a/src/Certificate/ideal.jl b/src/Certificate/ideal.jl index e11d4ec88..4e2f5dbbc 100644 --- a/src/Certificate/ideal.jl +++ b/src/Certificate/ideal.jl @@ -109,6 +109,22 @@ abstract type SimpleIdealCertificate{C,G,Z} <: AbstractIdealCertificate end reduced_polynomial(::SimpleIdealCertificate, poly, domain) = poly +""" + gram_weights(cert::AbstractCertificate, gram_basis, poly, ::Type{T}) + +Default implementation: returns `[constant_algebra_element(SA.basis(poly), T)]` +(one constant `1` weight). Certificates whose `gram_basis` returns a `Vector` +of bases can override to provide a parallel vector of per-basis weights. +""" +function gram_weights( + ::AbstractCertificate, + gram_basis, + poly, + ::Type{T}, +) where {T} + return [MB.constant_algebra_element(SA.basis(poly), T)] +end + cone(certificate::SimpleIdealCertificate) = certificate.cone function SumOfSquares.matrix_cone_type( ::Type{<:SimpleIdealCertificate{CT}}, From 541cdd804df619f85208175d46619f5d66f6e33f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Legat?= Date: Sat, 27 Jun 2026 08:39:15 +0200 Subject: [PATCH 2/2] Checkout branches --- .github/workflows/ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64dcd0fba..22e35522c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,14 @@ jobs: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - uses: julia-actions/cache@v3 + - name: SW + shell: julia --project=@. {0} + run: | + using Pkg + Pkg.add([ + PackageSpec(name="SymbolicWedderburn", rev="schur"), + PackageSpec(name="MultivariateBases", rev="master"), + ]) - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 # # See https://github.com/oxfordcontrol/Clarabel.jl/pull/230