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

(WIP) Allow variables to carry constraints, custom variable types #313

Closed
wants to merge 16 commits into from

Conversation

ericphanson
Copy link
Collaborator

@ericphanson ericphanson commented Aug 16, 2019

Remaining things to do:

  • Wait for Switch to MathOptInterface #330 and then rebase
  • Add examples and explanations to the documentation
  • Add more tests (complex variable constructors, various numeric types, problems using the new variables)
  • Improve style and formatting
  • ideally get some feedback :).

This PR introduces an AbstractVariable{T} type with a corresponding interface. User-defined types can implement the interface to have custom variable types which can be dispatched on in user-code. The interface consists of

  • value, value!: get or set the numeric value of the variable. value should return nothing when no numeric value is set.
  • vexity, vexity!: get or set the vexity of the variable. The vexity should be AffineVexity() unless the variable has been fix!'d, in which case it is ConstVexity().
  • sign, vartype, and constraints: get the Sign, VarType (a new enum, for binary, integer, or continuous variables), numeric type, and a (possibly empty) vector of constraints which are to be applied to any problem in which the variable is used.

Optionally, also users can also implement sign!, vartype!, and add_constraint! to allow users to modify those values or add a constraint to a list of constraints which are applied to any problem in which the variable is used.

Subtypes of AbstractVariable are expected to have fields (or getproperty overloads) for head, id_hash and size, as those are expected of all AbstractExpr's. Future work could be to clarify the AbstractExpr interface. Moreover, AbstractVariable's need to add themselves to Convex's global registrar of variables, and set their own hash. This sounds confusing but I will add several examples to the docs. Moreover, subtyping AbstractVariable is definitely not necessary for casual use (we got this far without it anyway!).

The type parameter T gives the eltype of the variable. This allows variables which accept e.g. BigFloats to be created, instead of implicitly assuming variables are all Float64. The Problem companion to this change is #289.

The venerable Variable is the first user of this interface. The new constraints and vartype methods lets us finish what we started in #299: getting rid of sets. The problem with sets is that it tried to serve several orthogonal purposes simultaneously while at the same time, it wasn't always very clear what you could put in there that would have an effect.

  • sets was populated by :fixed to indicate a variable was constant; this was changed in Allow calling fix! twice in a row #299 by using the vexity to indicate constantness.
  • sets was also used to add one particular constraint to a variable: semidefiniteness. This is replaced by the much more flexible constraints (and add_constraint!) method. Now, a Variable can carry around arbitrary constraints which automatically get applied to any problem the variable is in, just like :semidefinite in sets did.
  • sets was finally used to indicate if a variable was binary, integer, or continuous. This was replaced by vartype which uses an enum. I prefer the enum because it makes clear what the possible choices are, whereas when entering symbols, it wasn't immediately obvious (e.g. :bin or :binary?)

What is the benefit? One nice thing you can do now is

function probabilityvector(d::Int)
    x = Variable(d, Positive())
    add_constraint!(x, sum(x) == 1)
    x
end

Then later, p = probabilityvector(3) can be used like any other variable, but the constraints of having non-negative entries which add to 1 will be automatically applied to any problem it is used in. This allows very simple implementations of DSLs.

The introduction of AbstractVariable allows more ambitious extensions as well. Since custom types can subtype AbstractVariable, variables can be used in dispatch, or even be used as callable types. Continuing with the probability example, let's say I often use my probability vectors for getting the expectation value of other variables, and I want to use function notation for this. I could define

using Convex
mutable struct ProbabilityVector <: Convex.AbstractVariable{Float64}
    head::Symbol
    id_hash::UInt64
    size::Tuple{Int, Int}
    value::Convex.ValueOrNothing
    vexity::Vexity
    function ProbabilityVector(d)
        this = new(:ProbabilityVector, 0, (d,1), nothing, Convex.AffineVexity())
        this.id_hash = objectid(this)
        Convex.id_to_variables[this.id_hash] = this
        this
    end
end
Convex.constraints(p::ProbabilityVector) = [ sum(p) == 1 ]
Convex.sign(::ProbabilityVector) = Convex.Positive()
Convex.vartype(::ProbabilityVector) = Convex.ContVar

(p::ProbabilityVector)(x) = dot(p, x)

And then I can use the new type freely in Convex problems. E.g.

p = ProbabilityVector(3)
x = [1.0, 2.0, 3.0]
prob = minimize( p(x) )
solve!(prob, solver)
evaluate(p) # [1.0, 0.0, 0.0]

I've been experimenting with a similar DSL for quantum information theory in https://github.com/ericphanson/QuantumSDPs.jl.

These changes should be entirely non-breaking (as long as user code was not dipping into Variable fields too much). I re-implemented the old constructors that accept symbols for set, and translate them to the new variable type. (These could be deprecated at some point). Many of the internals changes are simply changing e.g. x.sign to sign(x), to use the interface instead.

Lines 310-312 of variables.jl contain the core logic change; instead of adding constraints for hardcoded symbols that correspond to constraints, we just apply each constraint given in constraints(x). Most of the lines of code changed are new constructors, fallback constructors for the old set behavior, and tests for the constructors.

This was referenced Aug 31, 2019
@ericphanson
Copy link
Collaborator Author

Something like this might be nice to hook into MOI's constrained variables (http://www.juliaopt.org/MathOptInterface.jl/stable/apireference/#MathOptInterface.add_constrained_variable), once #330 is done.

@ericphanson
Copy link
Collaborator Author

Continued in #358

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

1 participant