JuliaAlgebra / DynamicPolynomials.jl

Multivariate polynomials implementation of commutative and non-commutative variables
Other
60 stars 20 forks source link

Better compatibility with packages such as ApproxFun.jl and Polynomials.jl #127

Open benjione opened 1 year ago

benjione commented 1 year ago

If DynamicPolynomials library is used together with e.g. ApproxFun, there is an error if the package IntervalSets is used and the following is executed with v being of type PolyVar:

_in(v, I::TypedEndpointsInterval{:closed,:closed}) = leftendpoint(I) ≤ v ≤ rightendpoint(I)
_in(v, I::TypedEndpointsInterval{:open,:open}) = leftendpoint(I) < v < rightendpoint(I)
_in(v, I::TypedEndpointsInterval{:closed,:open}) = leftendpoint(I) ≤ v < rightendpoint(I)
_in(v, I::TypedEndpointsInterval{:open,:closed}) = leftendpoint(I) < v ≤ rightendpoint(I)

The error is because Base.isless is not defined for PolyVar Code example producing the error:

using SumOfSquares
using DynamicPolynomials
using DynamicPolynomials: PolyVar
using ApproxFun
using SCS
using IntervalSets

## use it for something:
@polyvar x

model = SOSModel(SCS.Optimizer)
@variable(model, a[1:10])
p = Fun(Chebyshev(), a)
p(x)  # this throws for example the error

One can manually overwrite those functions for the desired type, e.g.

using DynamicPolynomials: PolyVar
using IntervalSets

## define these functions as a trick:
IntervalSets.in(v::PolyVar, I::IntervalSets.TypedEndpointsInterval{:closed,:closed}) = true
IntervalSets.in(v::PolyVar, I::IntervalSets.TypedEndpointsInterval{:open,:open}) = true
IntervalSets.in(v::PolyVar, I::IntervalSets.TypedEndpointsInterval{:closed,:open}) = true
IntervalSets.in(v::PolyVar, I::IntervalSets.TypedEndpointsInterval{:open,:closed}) = true

Is it possible to overwrite the IntervalSets.in in order to get better compatibility between the polynomial packages? Or has this to be done in MultivariatePolynomials.jl instead of here?

benjione commented 1 year ago

Also see here and this pull request here for how to do this specialization.

blegat commented 1 year ago

I'm tempted to say that we don't want to define in for the same reason we do not define isless. It does not make sense to ask whether a symbolic variable is less than a number. Maybe there is a function higher in the stacktrace that would make sense to define

benjione commented 1 year ago

While this is true, I think the desired behavior would be:

julia> @polyvar x
(x,)
julia> 0 ≤ x ≤ 1 # this would have a side effect on x
0 ≤ x ≤ 1
julia> p = Fun(Chebyshev(), [0.0, 1.0, 2.0])
Fun(Chebyshev(), [0.0, 1.0, 2.0])
juliy> p(x)
4x^2 + x - 2 for 0 ≤ x ≤ 1

Or maybe something like this to avoid side effects:

julia> @polyvar x
(x,)
julia> x2 = impose(x, lowerbound=0, upperbound=1) # no side effect on x, Base.isless can be defined as x2 < y if true for all x2
0 ≤ x2 ≤ 1
julia> @impose 0 ≤ x ≤ 1 # maybe a macro overwriting x
0 ≤ x ≤ 1
julia> p = Fun(Chebyshev(), [0.0, 1.0, 2.0])
Fun(Chebyshev(), [0.0, 1.0, 2.0])
juliy> p(x2)
4x2^2 + x2 - 2 for 0 ≤ x2 ≤ 1
blegat commented 1 year ago

We have such concept of domain for x in https://github.com/JuliaAlgebra/SemialgebraicSets.jl but it wouldn't work like that. Note that this can be achieved as follows:

julia> @polyvar x
(x,)
julia> p = Fun(Chebyshev(), [0.0, 1.0, 2.0])
Fun(Chebyshev(), [0.0, 1.0, 2.0])
julia> clenshaw(p.space, p.coefficients, x)
4x2^2 + x2 - 2

See https://github.com/JuliaApproximation/ApproxFunOrthogonalPolynomials.jl/blob/8bafeff745ad5191916cf26d909a152cd29b7c48/src/Spaces/PolynomialSpace.jl#L21 That's a bit hacky though. @jishnub is there a recommended way in ApproxFun to evaluate while skipping the domain check ?

jishnub commented 1 year ago

I don't think there's a way at present, although this does sound like a good idea. Perhaps @benjione can do this at present (an extension of their impose idea):

julia> struct InDomain{T}
       x :: T
       end

julia> p = Fun(Chebyshev(), [0.0, 1.0, 2.0])
Fun(Chebyshev(), [0.0, 1.0, 2.0])

julia> ApproxFunBase.tocanonical(::ChebyshevInterval{<:Number}, x::InDomain) = x.x

julia> Base.in(::InDomain, ::ChebyshevInterval) = true

julia> p(InDomain(x))
4.0x² + x - 2.0

However, using clenshaw directly should be fine as well:

julia> clenshaw(space(p), ApproxFun.coefficients(p), x)
4.0x² + x - 2.0
benjione commented 1 year ago

Thanks, I think I will use the InDomain{T} solution for now.

Nevertheless, I think it would be nice if polyvar would work with other polynomial packages. E.g. Polynomials.jl throws exact the same error as ApproxFun.jl.

Maybe the InDomain{T} solution could be added somewhere in the documentation. But probably there are not many users anyway combining these packages.

jishnub commented 1 year ago

E.g. Polynomials.jl throws exact the same error as ApproxFun.jl.

what's the error? This seems to work for me:

julia> @polyvar x
(x,)

julia> Polynomials.Polynomial([1,2,3])(x)
3x² + 2x + 1
benjione commented 1 year ago

I think the normal polynomials are not domain bounded. For Chebyshev, I get:

ChebyshevT(rand(10))(x)
ERROR: MethodError: no method matching isless(::Int64, ::PolyVar{true})
Closest candidates are:
...
blegat commented 1 year ago

We try to avoid defining methods just to make a workflow work as it might break another workflow. Here, defining isless between a variable and a polynomial variable would be confusing for other workflow so we'd rather not do it.