Skip to content

Commit

Permalink
Added variable restrictions (#147)
Browse files Browse the repository at this point in the history
* Added variable restrictions

* coverage_fix
  • Loading branch information
pulsipher authored Jun 20, 2021
1 parent c772d3f commit b8c5c02
Show file tree
Hide file tree
Showing 14 changed files with 285 additions and 8 deletions.
6 changes: 6 additions & 0 deletions docs/src/guide/constraint.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,12 @@ julia> @constraint(model, [i = 1:2], ya^2 + z[i] <= 1, DomainRestrictions(x[i] =
ya(t, x)² + z[2] ≤ 1.0, ∀ t ∈ [0, 10], x[1] ∈ [-2, 2], x[2] ∈ [0, 1]
```

!!! tip
For more complex constraint restrictions that `DomainRestrictions` won't
allow (e.g., manually defining derivative approximations), consider using
[Restricted Variables](@ref) instead. However, where possible it will be
more performant to use `DomainRestrictions` instead.

## Queries
In this section, we describe a variety of methods to extract constraint
information.
Expand Down
46 changes: 46 additions & 0 deletions docs/src/guide/variable.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ respective semi-infinite variables `w[i](0, x)` stored in `model`.
combination with adding [`DomainRestrictions`](@ref) to constraints which
restrict the infinite domain as needed.

See [Restricted Variables](@ref) to learn about symbolic inline definition of
semi-infinite variables.

### Point Variables
Now let's add some point variables. These allow us to consider an infinite
variable evaluated at a certain infinite parameter point. For example, let's
Expand All @@ -129,6 +132,9 @@ case the lower bound inherited from `y(t)` is overwritten by instead fixing
combination with adding [`DomainRestrictions`](@ref) to constraints which
restrict the infinite domain as needed.

See [Restricted Variables](@ref) to learn about symbolic inline definition of
point variables.

### Finite Variables
Finally, we can add finite variables to our model. These denote variables that
hold a single value over the infinite domain or some portion of it (e.g.,
Expand Down Expand Up @@ -604,6 +610,46 @@ julia> @variables(model, begin
```

## Restricted Variables
To define point and semi-infinite variables, we can also use [`restrict`](@ref)
for convenient inline definitions. This can be convenient for certain complex
constraint definition schemes, but should be used cautiously.

For example, let's consider restricting the infinite variable `y(t, x)`:
```jldoctest restrict_vars; setup = :(using InfiniteOpt)
julia> model = InfiniteModel();
julia> @infinite_parameter(model, t in [0, 1]);
julia> @infinite_parameter(model, x[1:2] in [-1, 1]);
julia> @variable(model, y, Infinite(t, x))
y(t, x)
julia> pt = restrict(y, 0, [-1, 1]) # make point variable y(0, [-1, 1])
y(0, [-1, 1])
julia> semi = restrict(y, 0, x) # make semi-infinite variable y(0, x)
y(0, [x[1], x[2]])
```

We can also, even more conveniently, treat the infinite variable as a function
to accomplish this in a more intuitive syntax:
```jldoctest restrict_vars
julia> pt = y(0, [-1, 1]) # make point variable y(0, [-1, 1])
y(0, [-1, 1])
julia> semi = y(0, x) # make semi-infinite variable y(0, x)
y(0, [x[1], x[2]])
```
These can be conviently embedded in constraints to enable more complex schemes
than what using [`DomainRestrictions`](@ref) can acheive.

!!! note
Where possible [`DomainRestrictions`](@ref) should be used instead of
defining restricted variables when creating constraints for better
performance.

## Queries
`InfiniteOpt` contains a large suite of methods to query information about
variables. This suite is comprised of extensions to all current `JuMP` query
Expand Down
1 change: 1 addition & 0 deletions docs/src/manual/variable.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Infinite
JuMP.build_variable(::Function, ::JuMP.VariableInfo, ::Infinite)
InfiniteVariable
JuMP.add_variable(::InfiniteModel, ::JuMP.AbstractVariable, ::String)
restrict
VariableData
InfiniteVariableIndex
InfiniteVariableRef
Expand Down
19 changes: 19 additions & 0 deletions src/general_variables.jl
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,25 @@ function JuMP.delete(model::InfiniteModel,
return JuMP.delete(model, dispatch_variable_ref.(prefs))
end

################################################################################
# FUNCTIONAL CALLS
################################################################################
# Dispatch fallback
function _functional_reference_call(vref, idx_type, args...; kwargs...)
arg_str = join(args, ", ")
kwarg_str = join(Tuple(string(k, " = ", v) for (k, v) in kwargs), ", ")
kwarg_str = isempty(kwarg_str) ? "" : "; " * kwarg_str
error("Functional call `$(vref)($(arg_str)$(kwarg_str))` is unrecognized ",
"syntax.")
end

# Function calls made directly on variable references
# Unfortunetly, this is not readily compatible with docstrings...
function (vref::GeneralVariableRef)(args...; kwargs...)
return _functional_reference_call(vref, _index_type(vref), args...;
kwargs...)
end

################################################################################
# PARAMETER METHODS
################################################################################
Expand Down
97 changes: 97 additions & 0 deletions src/infinite_variables.jl
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ each argument is either of the first two options listed.
"""
struct Infinite{VT <: Collections.VectorTuple} <: InfOptVariableType
parameter_refs::VT
function Infinite(vt::VT) where {VT <: Collections.VectorTuple}
return new{VT}(vt)
end
end
function Infinite(args...)
return Infinite(Collections.VectorTuple(args))
Expand Down Expand Up @@ -310,6 +313,98 @@ function _check_and_make_variable_ref(
return vref
end

################################################################################
# RESTRICTION METHODS
################################################################################
## Dispatch functions for functional syntax of making restricted variables
# Point variable
function _restrict_infinite_variable(
ivref::GeneralVariableRef,
vt::Collections.VectorTuple{<:Real}
)::GeneralVariableRef
info = JuMP.VariableInfo(false, NaN, false, NaN, false, NaN, false, NaN,
false, false)
new_var = JuMP.build_variable(error, info, Point(ivref, vt))
return JuMP.add_variable(JuMP.owner_model(ivref), new_var)
end

# Infinite variable
function _restrict_infinite_variable(
ivref::GeneralVariableRef,
vt::Collections.VectorTuple{<:GeneralVariableRef}
)::GeneralVariableRef
if parameter_list(ivref) != vt.values
error("Unrecognized syntax for infinite variable restriction.")
end
@warn("Unnecessary use of functional infinite variable restriction syntax " *
"that will cause performance degredations. This was probably caused " *
"by using syntax like `y(t, x)` inside expressions. Instead just " *
"use the infinite variable reference (e.g. `y`).")
return ivref
end

# Semi-Infinite variable
function _restrict_infinite_variable(
ivref::GeneralVariableRef,
vt::Collections.VectorTuple
)::GeneralVariableRef
info = JuMP.VariableInfo(false, NaN, false, NaN, false, NaN, false, NaN,
false, false)
new_var = JuMP.build_variable(error, info, SemiInfinite(ivref, vt))
return JuMP.add_variable(JuMP.owner_model(ivref), new_var)
end

"""
restrict(ivref::GeneralVariableRef, supps...)::GeneralVariableRef
Restrict the input domain of an infinite variable/derivative `ivref` in
accordance with the infinite parameters and/or values `supps`. Here `supps` must
match the formatting of `ivref`'s infinite parameters. Here the following
outputs are possible:
- Equivalent to `@variable(model, variable_type = Point(ivref, supps...)` if
`supps` are a complete support point
- Equivalent to `@variable(model, variable_type = SemiInfinite(ivref, supps...)`
if `supps` are a partial support point.
Conveniently, we can also invoke this method by calling `ivref(supps...)`.
Errors if ivref is not an infinite variable or derivative or the formatting of
`supps` is incorrect. Will warn if `supps` only contain infinite parameters and
will simply return `ivref`.
**Example**
```julia-repl
julia> restrict(y, 0, x)
y(0, [x[1], x[2]])
julia> restrict(y, 0, [0, 0])
y(0, [0, 0])
julia> y(0, x)
y(0, [x[1], x[2]])
julia> y(0, [0, 0])
y(0, [0, 0])
```
"""
function restrict(ivref::GeneralVariableRef, supps...)::GeneralVariableRef
idx_type = _index_type(ivref)
if idx_type != InfiniteVariableIndex && idx_type != DerivativeIndex
error("The `vref(values..)` restriction syntax is only valid for ",
"infinite variable and derivative references.")
end
return _restrict_infinite_variable(ivref, Collections.VectorTuple(supps))
end

# This enables functional calls (e.g., `y(0, x)` => semi-infinite variable)
function _functional_reference_call(
ivref::GeneralVariableRef,
::Union{Type{InfiniteVariableIndex}, Type{DerivativeIndex}},
supps...
)::GeneralVariableRef
return _restrict_infinite_variable(ivref, Collections.VectorTuple(supps))
end

################################################################################
# VARIABLE DEPENDENCIES
################################################################################
Expand Down Expand Up @@ -561,6 +656,7 @@ function set_start_value_function(
param_nums = _parameter_numbers(vref)
new_var = InfiniteVariable(new_info, prefs, param_nums, obj_nums, is_vect_func)
_set_core_variable_object(vref, new_var)
# TODO update point variable start values as appropriate
return
end

Expand All @@ -587,6 +683,7 @@ function reset_start_value_function(vref::InfiniteVariableRef)::Nothing
param_nums = _parameter_numbers(vref)
new_var = InfiniteVariable(new_info, prefs, param_nums, obj_nums, true)
_set_core_variable_object(vref, new_var)
# TODO update point variable start values as appropriate
return
end

Expand Down
3 changes: 3 additions & 0 deletions src/point_variables.jl
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ of both real valued supports.
struct Point{V, VT <: Collections.VectorTuple} <: InfOptVariableType
infinite_variable_ref::V
parameter_values::VT
function Point(vref::V, vt::VT) where {V, VT <: Collections.VectorTuple}
return new{V, VT}(vref, vt)
end
end
function Point(ivref, vals...)
return Point(ivref, Collections.VectorTuple(vals))
Expand Down
17 changes: 12 additions & 5 deletions src/semi_infinite_variables.jl
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,17 @@ of both real valued supports and/or infinite parameters.
struct SemiInfinite{V, VT <: Collections.VectorTuple} <: InfOptVariableType
infinite_variable_ref::V
parameter_values::VT
function SemiInfinite(ivref::V, vt::VT) where {V, VT <: Collections.VectorTuple}
processed_vals = _process_value.(vt.values)
if processed_vals != vt.values
vt2 = Collections.VectorTuple(processed_vals, vt.ranges, vt.indices)
return new{V, typeof(vt2)}(ivref, vt2)
end
return new{V, VT}(ivref, vt)
end
end
function SemiInfinite(ivref, vals...)
vt = Collections.VectorTuple(vals)
processed_vals = _process_value.(vt.values)
if processed_vals != vt.values
vt = Collections.VectorTuple(processed_vals, vt.ranges, vt.indices)
end
return SemiInfinite(ivref, vt)
end

Expand Down Expand Up @@ -174,7 +178,10 @@ function JuMP.build_variable(
end
# check that the values are the same format as the infinite variable
raw_params = var_type.parameter_values
if !Collections.same_structure(raw_params, raw_parameter_refs(ivref))
if !all(v isa Union{Real, GeneralVariableRef} for v in raw_params)
_error("Unexpected inputs given, expected them to be parameter references ",
"and real numbers.")
elseif !Collections.same_structure(raw_params, raw_parameter_refs(ivref))
_error("The parameter reference/value input format $(raw_params) does ",
"not match that of the infinite variable $(ivref).")
end
Expand Down
15 changes: 15 additions & 0 deletions test/derivatives.jl
Original file line number Diff line number Diff line change
Expand Up @@ -759,4 +759,19 @@ end
@test infinite_variable_ref(dref) == d
@test parameter_values(dref) == (0, [0, 0])
end
# test using restrict
@testset "Restriction Definition" begin
# test semi-infinite
dref = GeneralVariableRef(m, 2, SemiInfiniteVariableIndex)
@test restrict(d, 0, x) == dref
@test parameter_refs(dref) == (x,)
@test infinite_variable_ref(dref) == d
@test eval_supports(dref)[1] == 0
# test point
dref = GeneralVariableRef(m, 2, PointVariableIndex)
@test d(1, [0, 0]) == dref
@test parameter_refs(dref) == ()
@test infinite_variable_ref(dref) == d
@test parameter_values(dref) == (1, [0, 0])
end
end
15 changes: 15 additions & 0 deletions test/general_variables.jl
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,21 @@ end
end
end

# test calling the general variable reference as a function
@testset "Functional Calls" begin
# Setup data
m = InfiniteModel();
gvref = GeneralVariableRef(m, 1, TestIndex2)
# test _functional_reference_call (Fallback)
@testset "_functional_reference_call (Fallback)" begin
@test_throws ErrorException InfiniteOpt._functional_reference_call(gvref, TestIndex2, 42)
end
# test GeneralVariableRef as a function
@testset "GeneralVariableRef(args...)" begin
@test_throws ErrorException gvref(42, a = 4)
end
end

# test Parameter Methods
@testset "Parameter Methods" begin
# Setup data
Expand Down
21 changes: 21 additions & 0 deletions test/infinite_variables.jl
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,27 @@ end
end
end

# test restriciton definition
@testset "Restriction Definition" begin
# setup the info
m = InfiniteModel()
@infinite_parameter(m, t in [0, 1])
@infinite_parameter(m, x[1:2] in [0, 1])
@variable(m, y, Infinite(t, x))
# test errors
@test_throws ErrorException restrict(y, t)
@test_throws ErrorException restrict(t, x)
@test_throws ErrorException y(t)
# test warning
warn = "Unnecessary use of functional infinite variable restriction syntax " *
"that will cause performance degredations. This was probably caused " *
"by using syntax like `y(t, x)` inside expressions. Instead just " *
"use the infinite variable reference (e.g. `y`)."
@test_logs (:warn, warn) restrict(y, t, x)
@test_logs (:warn, warn) y(t, x)
# Note: the rest of the cases are tested with point variables and semi-infinite variables
end

# Test variable(s) constrained on creation
@testset "Creation Constraints" begin
# initialize model and stuff
Expand Down
21 changes: 21 additions & 0 deletions test/point_variables.jl
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,27 @@ end
end
end

# test restriciton definition
@testset "Restriction Definition" begin
# setup the info
m = InfiniteModel()
@infinite_parameter(m, t in [0, 1])
@infinite_parameter(m, x[1:2] in [0, 1])
@variable(m, y, Infinite(t, x))
# test errors
@test_throws ErrorException restrict(y, 0, [2, 2])
@test_throws ErrorException restrict(y, 0, 2)
@test_throws ErrorException y(0, 2)
# test normal wth restrict
vref = GeneralVariableRef(m, 1, PointVariableIndex)
@test restrict(y, 0, [0, 0]) == vref
@test parameter_values(vref) == (0, [0, 0])
# test normal functionally
vref = GeneralVariableRef(m, 2, PointVariableIndex)
@test y(0.5, [0, 0]) == vref
@test parameter_values(vref) == (0.5, [0, 0])
end

# test usage methods
@testset "Usage" begin
# initialize model and stuff
Expand Down
3 changes: 0 additions & 3 deletions test/scalar_parameters.jl
Original file line number Diff line number Diff line change
Expand Up @@ -296,18 +296,15 @@ end
param = build_parameter(error, IntervalDomain(0, 1))
pref = add_parameter(m, param, "test")
dpref = dispatch_variable_ref(pref)
bad = TestVariableRef(m, TestIndex(-1))
bad_pref = FiniteParameterRef(m, FiniteParameterIndex(-1))
# JuMP.name
@testset "JuMP.name" begin
@test_throws ArgumentError name(bad)
@test name(pref) == "test"
@test name(dpref) == "test"
@test name(bad_pref) == ""
end
# JuMP.set_name
@testset "JuMP.set_name" begin
@test_throws ArgumentError set_name(bad, "test")
@test isa(set_name(pref, "new"), Nothing)
@test name(pref) == "new"
@test isa(set_name(dpref, "test"), Nothing)
Expand Down
Loading

0 comments on commit b8c5c02

Please sign in to comment.