Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement quadratic forms #62

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/StarAlgebras.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import MutableArithmetics as MA
export StarAlgebra, AlgebraElement
export basis, coeffs, star

function star end

# AbstractCoefficients
## abstract definitions
include("coefficients.jl")
Expand All @@ -31,6 +33,7 @@ include("algebra_elts.jl")
include("star.jl")

include("arithmetic.jl")
include("quadratic_form.jl")
include("show.jl")

# augmented basis implementation
Expand Down
1 change: 1 addition & 0 deletions src/algebra_elts.jl
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@ function LinearAlgebra.dot(w::AbstractVector, b::AlgebraElement)
return LinearAlgebra.dot(w, coeffs(b))
end
function LinearAlgebra.dot(a::AlgebraElement, w::AbstractVector)
@assert key_type(basis(parent(a))) <: Integer
return LinearAlgebra.dot(coeffs(a), w)
end
30 changes: 13 additions & 17 deletions src/arithmetic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,6 @@
end
end

function Base.:*(args::Vararg{AlgebraElement,N}) where {N}
return MA.operate_to!(_preallocate_output(*, args...), *, args...)
end

Base.:^(a::AlgebraElement, p::Integer) = Base.power_by_squaring(a, p)

# mutable API
Expand Down Expand Up @@ -130,34 +126,34 @@
X::AlgebraElement,
Y::AlgebraElement,
)
@assert parent(res) === parent(X) === parent(Y)
@assert parent(res) == parent(X)
@assert parent(X) == parent(Y)
MA.operate_to!(coeffs(res), -, coeffs(X), coeffs(Y))
return res
end

function MA.operate_to!(
res::AlgebraElement,
::typeof(*),
args::Vararg{AlgebraElement,N},
ABC::Vararg{AlgebraElement,N},
) where {N}
for arg in args
@assert parent(res) == parent(arg)
end
@assert allequal(parent, (res, ABC...))
mstr = mstructure(basis(res))
MA.operate_to!(coeffs(res), mstr, coeffs.(args)...)
MA.operate_to!(coeffs(res), mstr, map(coeffs, ABC)..., true)
return res
end

function MA.operate!(
::UnsafeAddMul{typeof(*)},
mul::UnsafeAddMul,
res::AlgebraElement,
args::Vararg{AlgebraElement,N},
ABC::Vararg{AlgebraElement,N},
) where {N}
for arg in args
@assert parent(res) == parent(arg)
end
mstr = mstructure(basis(res))
MA.operate!(UnsafeAddMul(mstr), coeffs(res), coeffs.(args)...)
MA.operate!(mul, coeffs(res), map(coeffs, ABC)..., true)
return res
end

function MA.operate!(add::UnsafeAdd, res::AlgebraElement, A::AlgebraElement)
MA.operate!(add, coeffs(res), coeffs(A))

Check warning on line 156 in src/arithmetic.jl

View check run for this annotation

Codecov / codecov/patch

src/arithmetic.jl#L155-L156

Added lines #L155 - L156 were not covered by tests
return res
end

Expand Down
2 changes: 1 addition & 1 deletion src/coefficients.jl
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ function LinearAlgebra.dot(w::AbstractVector, ac::AbstractCoefficients)
if isempty(values(ac))
return zero(MA.promote_sum_mul(eltype(w), value_type(ac)))
else
return sum(w[i] * star(v) for (i, v) in nonzero_pairs(ac))
return sum(w[i] * v for (i, v) in nonzero_pairs(ac))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we take the conjugation?

end
end

Expand Down
64 changes: 43 additions & 21 deletions src/mstructures.jl
Original file line number Diff line number Diff line change
Expand Up @@ -31,52 +31,74 @@ When the product is not representable faithfully,
"""
abstract type MultiplicativeStructure end

"""
struct UnsafeAddMul{M<:Union{typeof(*),MultiplicativeStructure}}
structure::M
end

The value of `(op::UnsafeAddMul)(a, b, c)` is `a + structure(b, c)`
where `a` is not expected to be canonicalized before the operation `+`
and should not be expected to be canonicalized after either.
"""
struct UnsafeAddMul{M<:Union{typeof(*),MultiplicativeStructure}}
structure::M
end

function MA.operate_to!(res, ms::MultiplicativeStructure, args::Vararg{Any,N}) where {N}
"""
operate_to!(res, ms::MultiplicativeStructure, A, B[, α = true])
kalmarek marked this conversation as resolved.
Show resolved Hide resolved
Compute `α·A·B` storing the result in `res`. Return `res`.

`res` is assumed to behave like `AbstractCoefficients` and not aliased with any
other arguments.
`A` and `B` are assumed to behave like `AbstractCoefficients`, while `α` will
be treated as a scalar.

Canonicalization of the result happens only once at the end of the operation.
"""
function MA.operate_to!(
res,
ms::MultiplicativeStructure,
args::Vararg{Any,N},
) where {N}
if any(Base.Fix1(===, res), args)
throw(ArgumentError("No alias allowed"))
throw(
ArgumentError(
"Aliasing arguments in multiplication is not supported",
),
)
end
MA.operate!(zero, res)
MA.operate!(UnsafeAddMul(ms), res, args...)
res = MA.operate!(UnsafeAddMul(ms), res, args...)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With operate! you are guaranteed that the argument res is the same as what is returned so no need to get the result

MA.operate!(canonical, res)
return res
end

function MA.operate!(::UnsafeAddMul, res, c)
for (k, v) in nonzero_pairs(c)
struct UnsafeAdd end

function MA.operate!(::UnsafeAdd, res, b)
blegat marked this conversation as resolved.
Show resolved Hide resolved
for (k, v) in nonzero_pairs(b)
unsafe_push!(res, k, v)
end
return res
end

function MA.operate!(op::UnsafeAddMul, res, b, c, args::Vararg{Any, N}) where {N}
for (kb, vb) in nonzero_pairs(b)
for (kc, vc) in nonzero_pairs(c)
for (k, v) in nonzero_pairs(op.structure(kb, kc))
function MA.operate!(op::UnsafeAddMul, res, A, B, α)
blegat marked this conversation as resolved.
Show resolved Hide resolved
for (kA, vA) in nonzero_pairs(A)
for (kB, vB) in nonzero_pairs(B)
for (k, v) in nonzero_pairs(op.structure(kA, kB))
cfs = MA.@rewrite α * vA * vB * v
MA.operate!(
op,
UnsafeAdd(),
res,
SparseCoefficients((_key(op.structure, k),), (vb * vc * v,)),
args...,
SparseCoefficients((_key(op.structure, k),), (cfs,)),
)
end
end
end
return res
end

function MA.operate!(op::UnsafeAddMul, res, A, B, C, α)
for (kA, vA) in nonzero_pairs(A)
for (kB, vB) in nonzero_pairs(B)
cfs = MA.@rewrite α * vA * vB
MA.operate!(op, res, op.structure(kA, kB), C, cfs)
end
end
return res
end

struct DiracMStructure{Op} <: MultiplicativeStructure
op::Op
end
Expand Down
84 changes: 84 additions & 0 deletions src/quadratic_form.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""
QuadraticForm(Q)
QuadraticForm{star}(Q)
A simple wrapper for representing a quadratic form.

`QuadraticForm(Q)` represents an honest quadratic form, however in the context
of `*`-algebras a more canonical object is a **sesquilinear form** which can be
constructed as `QuadraticForm{star}(Q)`.

Both objects are defined by matrix coefficients with respect to their bases `b`,
i.e. their values at `i`-th and `j`-th basis elements:
`star.(b)·Q·b = ∑ᵢⱼ Q[i,j]·star(b[i])·b[j]`.

# Necessary methods:
* `basis(Q)` - a **finite** basis `b` of the quadratic form;
* `Base.getindex(Q, i::T, j::T)` - the value at `i`-th and `j`-th basis elements
where `T` is the type of indicies of `basis(Q)`;
* `Base.eltype(Q)` - the type of `Q[i,j]·star(b[i])·b[j]`.
"""
struct QuadraticForm{T,involution}
Q::T
QuadraticForm(Q) = new{typeof(Q),identity}(Q)
QuadraticForm{star}(Q) = new{typeof(Q),star}(Q)
end

Base.eltype(qf::QuadraticForm) = eltype(qf.Q)
basis(qf::QuadraticForm) = basis(qf.Q)
Base.getindex(qf::QuadraticForm, i::T, j::T) where {T} = qf.Q[i, j]

function MA.operate_to!(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be operate!(ms, res, Q, args...). For Putinar, I need to add several quadratic form so it won't work if you zero. And I need the extra args 😇

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I read correctly, that you need to add several quadratic forms together?
E.g.

function MA.operate_to!(res, ms::MultiplicativeStructure, Qs::Vararg{<:QuadraticForm})
   MA.operate!(zero, res)

   for Q in Qs
       MA.operate!(ms, res, Q)
   end
   MA.operate!(canonical, res)
   return res
end

function MA.operate!(ms::MultiplicativeStructure, res, Q::QuadraticForm{T,ε}) where {T,ε}
   op = UnsafeAddMul(ms)
   for (i, b1) in pairs(basis(Q))
       b1★ = ε(b1)
       for (j, b2) in pairs(basis(Q))
           MA.operate!(op, res, coeffs(b1★), coeffs(b2), Q[i, j])
       end
    end
    return res
end

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be much easier to push this over the finish line had you provided the use-cases you have in mind for SoS as I asked many a times... :D

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm as lost in the design space as you are ^^

MA.operate_to!(res, ms::MultiplicativeStructure, Qs::Vararg{<:QuadraticForm}) doesn't work because it's a sum, MA.operate_to!(res, ::typeof(+), Qs::Vararg{<:QuadraticForm}) is correct but I won't use it because I will multiply each graph by a poynomial to do putinar

So the use case is

p = zero(...)
for (q, g) in zip(...)
    MA.operate!(UnsafeAddMul(...), p, QuadraticForm(q), g)
end

res,
ms::MultiplicativeStructure,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the plan for this ms ? Is the plan to allow res to have a different basis compared to Q and ms is the rule to do the mapping ? If yes, this should be commented on. Otherwise, just use copy

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, this signature needs to change. What we want here is to say:
Take this quadratic form (which comes with its own basis) and realize it algebraically storing the result in res.

Is copy the correct convention? store of copyto could also work;
how about MA.operate_to!(res, Q) where the quadratic form itself "operates"?

(this ms is an artifact of the past.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is precedent for copy in
https://github.com/jump-dev/MutableArithmetics.jl/blob/master/src/implementations/BigInt.jl#L20
It's weird that it's not the same type though so it might not be the right one.
Having Q be the operator is weird also. We could use UnsafeAdd here, it's like the unary +.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ms::MultiplicativeStructure,
::typeof(copy),

Q::QuadraticForm,
args...,
)
MA.operate!(zero, res)
MA.operate!(UnsafeAddMul(ms), res, Q, args...)
MA.operate!(canonical, res)
return res
end

function MA.operate!(
op::UnsafeAddMul,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we have UnsafeAdd, we should use UnsafeAdd here

res,
Q::QuadraticForm{T,ε},
) where {T,ε}
for (i, b1) in pairs(basis(Q))
b1★ = ε(b1)
for (j, b2) in pairs(basis(Q))
MA.operate!(op, res, coeffs(b1★), coeffs(b2), Q[i, j])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So every basis element should implement coeffs ? It's weird because it's not really an AlgebraElement

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the elements of the basis are just group elements

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we see AlgebraElement, this implementation is correct. Otherwise we should multiply b1 and b2 using the multiplicative structure

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can just return coeffs(x) = (x, true) if you like;

They are not just group elements, they are Diracs at group elements. The proper formalism is to talk about functions (on groups or other objects) instead of formal sums.

So far it seems that you can place in a basis something sortable (unless you canonicalize! yourself) that implements coeffs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you use arbitrary SA.ExplicitBasis it won't work right ? For example you place group elements in the basis and coeffs is not part of GroupsCore API.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just call some new internal function and distinguish two cases with multiple dispatch

  • Two AlgebraElement -> then do what is currently done
  • Otherwise, just multiply them with the multiplicative structure

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't just place group elements in basis, you need to pick a multiplicative structure. It could be e.g. DiracMStructure and then Diracs are in your basis (they aren't AlgebraElements either).

Does your element behave like coefficients? then we could define SA.coeffs(x::YourTypeWhichBehavesLikeAbstractCoeffs) = x to push x down the call?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but in general, I'm not convinced that this quadratic form is a good design. Let's brainstorm for a bit where should all coeffs calls go.

Can you formulate a concrete example specifying:

  • What will be in the basis
  • what is the mstructure
    ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok let me formulate a use case in the main thread

end
end
end

function MA.operate!(
op::UnsafeAddMul,
res,
Q::QuadraticForm{T,ε},
p,
) where {T,ε}
for (i, b1) in pairs(basis(Q))
b1★ = ε(b1)
for (j, b2) in pairs(basis(Q))
MA.operate!(op, res, coeffs(b1★), coeffs(b2), coeffs(p), Q[i, j])
end
end
end

"""
AlgebraElement(qf::QuadraticForm, A::AbstractStarAlgebra)
Construct an algebra element in `A` representing quadratic form `qf`.

!!! warning
It is assumed that all basis elements of `qf` belong to `A`, or at least
that `keys` of their coefficients can be found in the basis of `A`.
"""
function AlgebraElement(qf::QuadraticForm, A::AbstractStarAlgebra)
@assert all(b -> parent(b) == A, basis(qf))
res = zero(eltype(qf), A)
MA.operate_to!(coeffs(res), mstructure(A), qf)
return res
end

(A::AbstractStarAlgebra)(qf::QuadraticForm) = AlgebraElement(qf, A)
12 changes: 6 additions & 6 deletions test/caching_allocations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ end

@test Y isa AlgebraElement

@static if v"1.10" ≤ VERSION < v"1.11"
@static if v"1.10" ≤ VERSION
star(Y)
star(Y)
@test (@allocations star(Y)) ≤ 4
@test (@allocations star(Y)) ≤ 6
end

@test SA.supp(Y) == basis(fRG)[1:k]
Expand All @@ -48,7 +48,7 @@ end
@test all(!iszero(mstr.table))
@test_throws SA.UndefRefError all(!iszero, SA.mstructure(fRG).table)

@static if v"1.10" ≤ VERSION < v"1.11"
@static if v"1.10" ≤ VERSION
@test (@allocations Y * Y) > k^2 - 2 * k
@test Y * Y isa AlgebraElement
@test (@allocations Y * Y) ≤ 26
Expand All @@ -63,15 +63,15 @@ end
@test all(!iszero, SA.mstructure(fRG).table)


@static if v"1.10" ≤ VERSION < v"1.11"
@static if v"1.10" ≤ VERSION
YY = deepcopy(Y)
_alloc_test(YY, *, Y, Y, 25)
_alloc_test(YY, +, Y, Y, 0)
_alloc_test(YY, -, Y, Y, 0)

# SparseArrays calls `Base.unalias` which allocates:
_alloc_test(YY, +, YY, Y, 2)
_alloc_test(YY, +, Y, YY, 2)
_alloc_test(YY, +, YY, Y, 4)
_alloc_test(YY, +, Y, YY, 4)
end
end

Expand Down
21 changes: 13 additions & 8 deletions test/monoid_algebra.jl
Original file line number Diff line number Diff line change
Expand Up @@ -127,17 +127,22 @@
@test_throws ArgumentError MA.operate_to!(d, *, d, b)

d = deepcopy(a)
@test @allocated(MA.operate_to!(d, *, 2, d)) == 0
@test d == 2a

MA.operate_to!(d, *, 2, a) # preallocates memory in coefficients
@test @allocated(MA.operate_to!(d, *, 2, a)) == 0
@test d == 2a

MA.operate!(zero, d)
MA.operate!(SA.UnsafeAddMul(*), d, a, b, b)
MA.operate!(SA.canonical, SA.coeffs(d))
@test a * b^2 == *(a, b, b)
@test d == *(a, b, b)
@test @allocated(MA.operate_to!(d, *, 2, d)) == 0
@test d == 4a

MA.operate_to!(d, *, a, b)
@test d == a * b
MA.operate!(SA.UnsafeAddMul(SA.mstructure(RG)), d, a, b)
@test d == 2 * a * b

MA.operate_to!(d, *, a, b, b)
@test d == a * b * b
MA.operate!(SA.UnsafeAddMul(SA.mstructure(RG)), d, a, b, b)
@test d == 2 * a * b * b
end
end
end
Loading
Loading