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

DNMY: Support Domain Restriction Inversions #166

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
10 changes: 8 additions & 2 deletions docs/src/guide/constraint.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ illustrate this, let's define the constraint
julia> @constraint(model, 2ya^2 + z[1] >= 3, DomainRestrictions(t => 0, x => [-1, 1]))
2 ya(t, x)² + z[1] ≥ 3.0, ∀ t = 0, x[1] ∈ [-1, 1], x[2] ∈ [-1, 1]
```
We can also enforce the logical compliment of the restrictions using the
`invert_logic` keyword argument:
```jldoctest constrs
julia> @constraint(model, 2ya^2 + z[1] >= 3, DomainRestrictions(t => 0, invert_logic = true))
2 ya(t, x)² + z[1] ≥ 3.0, ∀ t ≠ 0, x[1] ∈ [-2, 2], x[2] ∈ [-2, 2]
```

Now we have added constraints to our model and it is ready to be solved!

Expand Down Expand Up @@ -388,10 +394,10 @@ provided for the cases in which one wants to query solely off of set or off
expression type. Let's illustrate this with `num_constraints`:
```jldoctest constrs
julia> num_constraints(model) # total number of constraints
16
17

julia> num_constraints(model, GenericQuadExpr{Float64, GeneralVariableRef})
5
6

julia> num_constraints(model, MOI.LessThan{Float64})
5
Expand Down
11 changes: 7 additions & 4 deletions src/TranscriptionOpt/transcribe.jl
Original file line number Diff line number Diff line change
Expand Up @@ -652,13 +652,16 @@ end
function _support_in_restrictions(
support::Vector{Float64},
indices::Vector{Int},
domains::Vector{InfiniteOpt.IntervalDomain}
)::Bool
domains::Vector{InfiniteOpt.IntervalDomain},
invert::Bool
)
for i in eachindex(indices)
s = support[indices[i]]
if !isnan(s) && (s < JuMP.lower_bound(domains[i]) ||
if !isnan(s) && !invert && (s < JuMP.lower_bound(domains[i]) ||
s > JuMP.upper_bound(domains[i]))
return false
elseif !isnan(s) && invert && JuMP.lower_bound(domains[i]) <= s <= JuMP.upper_bound(domains[i])
return false
end
end
return true
Expand Down Expand Up @@ -769,7 +772,7 @@ function transcribe_constraints!(
raw_supp = index_to_support(trans_model, i)
# ensure the support satisfies parameter bounds and then add it
if _support_in_restrictions(raw_supp, restrict_indices,
restrict_domains)
restrict_domains, restrictions.invert)
new_name = isempty(name) ? "" : string(name, "(support: ", counter, ")")
new_cref = _process_constraint(trans_model, constr, func,
set, raw_supp, new_name)
Expand Down
9 changes: 8 additions & 1 deletion src/constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -221,11 +221,12 @@ function _validate_restrictions(
restrictions::DomainRestrictions
)::Nothing
depend_supps = Dict{DependentParametersIndex, Matrix{Float64}}()
is_inverted = restrictions.invert
for (pref, domain) in restrictions
# check validity
JuMP.check_belongs_to_model(pref, model)
# ensure has a support if a point constraint was given
if (JuMP.lower_bound(domain) == JuMP.upper_bound(domain))
if !is_inverted && (JuMP.lower_bound(domain) == JuMP.upper_bound(domain))
if _index_type(pref) == IndependentParameterIndex
# label will be UserDefined
add_supports(pref, JuMP.lower_bound(domain), check = false)
Expand Down Expand Up @@ -856,6 +857,12 @@ function _update_restrictions(
old::DomainRestrictions{GeneralVariableRef},
new::DomainRestrictions{GeneralVariableRef}
)::Nothing
# check if logic is compatible
if old.invert != new.invert
error("The new domain restrictions are incompatible with the existing ",
"ones. The restriction logic doesn't match. Ensure `invert_logic` ",
"is the same for both.")
end
# check each new restriction
for (pref, domain) in new
# we have a new restriction
Expand Down
30 changes: 20 additions & 10 deletions src/datatypes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,7 @@ Note that the GeneralVariableRef must pertain to infinite parameters.

The constructor syntax is
```julia
DomainRestrictions(restrictions...)
DomainRestrictions(restrictions...; [invert_logic = false])
```
where each argument of `restrictions` is one of the following forms:
- `pref => value`
Expand All @@ -666,14 +666,18 @@ where each argument of `restrictions` is one of the following forms:
- `prefs => value`
- `prefs => [lb, ub]`
- `prefs => IntervalDomain(lb, ub)`.
Note that `pref` and `prefs` must correspond to infinite parameters.
Note that `pref` and `prefs` must correspond to infinite parameters. If we set
`invert_logic = true` then we specify the subdomain that inverts the logic of
the conditions given (i.e., the logical compliment).

**Fields**
- `intervals::Dict{GeneralVariableRef, IntervalDomain}`: A dictionary
of interval bounds on infinite parameters.
- `invert::Bool`: Specify the domain that is the logical compliment of intervals.
"""
struct DomainRestrictions{P <: JuMP.AbstractVariableRef}
intervals::Dict{P, IntervalDomain}
invert::Bool
end

################################################################################
Expand Down Expand Up @@ -1756,19 +1760,23 @@ end

# Constructor for expanding array parameters
function DomainRestrictions(
intervals::NTuple{N, Pair}
intervals::NTuple{N, Pair};
invert_logic::Bool = false
)::DomainRestrictions{GeneralVariableRef} where {N}
return DomainRestrictions(_expand_parameter_tuple(intervals))
return DomainRestrictions(_expand_parameter_tuple(intervals), invert_logic)
end

# Convenient constructor
function DomainRestrictions(args...)::DomainRestrictions{GeneralVariableRef}
return DomainRestrictions(args)
function DomainRestrictions(
args...;
invert_logic::Bool = false
)::DomainRestrictions{GeneralVariableRef}
return DomainRestrictions(args; invert_logic = invert_logic)
end

# Default method
function DomainRestrictions()::DomainRestrictions{GeneralVariableRef}
return DomainRestrictions(Dict{GeneralVariableRef, IntervalDomain}())
return DomainRestrictions(Dict{GeneralVariableRef, IntervalDomain}(), false)
end

# Make dictionary accessor
Expand Down Expand Up @@ -1796,7 +1804,7 @@ end

# Extend Base.copy
function Base.copy(restrictions::DomainRestrictions)
return DomainRestrictions(copy(intervals(restrictions)))
return DomainRestrictions(copy(intervals(restrictions)), restrictions.invert)
end

# Extend Base.setindex!
Expand All @@ -1822,15 +1830,17 @@ function Base.merge(
dr1::DomainRestrictions{P},
dr2::DomainRestrictions{P}
)::DomainRestrictions{P} where {P}
@assert dr1.invert == dr2.invert
new_dict = merge(intervals(dr1), intervals(dr2))
return DomainRestrictions(new_dict)
return DomainRestrictions(new_dict, dr1.invert)
end

# Extend Base.merge!
function Base.merge!(
dr1::DomainRestrictions{P},
dr2::DomainRestrictions{P}
)::DomainRestrictions{P} where {P}
@assert dr1.invert == dr2.invert
merge!(intervals(dr1), intervals(dr2))
return dr1
end
Expand All @@ -1841,5 +1851,5 @@ function Base.filter(
dr::DomainRestrictions{P}
)::DomainRestrictions{P} where {P}
new_dict = filter(f, intervals(dr))
return DomainRestrictions(new_dict)
return DomainRestrictions(new_dict, dr.invert)
end
61 changes: 55 additions & 6 deletions src/show.jl
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ function _math_symbol(::Type{JuMP.REPLMode}, name::Symbol)::String
return Sys.iswindows() ? "||" : "‖"
elseif name == :sub2
return Sys.iswindows() ? "_2" : "₂"
elseif name == :neq
return Sys.iswindows() ? "!=" : "≠"
elseif name == :logic_not
return Sys.iswindows() ? "!" : "¬"
else
error("Internal error: Unrecognized symbol $name.")
end
Expand Down Expand Up @@ -110,6 +114,10 @@ function _math_symbol(::Type{JuMP.IJuliaMode}, name::Symbol)::String
return "\\Vert"
elseif name == :sub2
return "_2"
elseif name == :neq
return "\\neq"
elseif name == :logic_not
return "\\neg"
else
error("Internal error: Unrecognized symbol $name.")
end
Expand Down Expand Up @@ -193,6 +201,18 @@ function in_domain_string(print_mode, domain::AbstractInfiniteDomain)::String
domain_string(print_mode, domain))
end

# Inverted interval domain
function in_domain_string(print_mode, lb1, ub1, lb2, ub2)
if ub1 == lb2
return string(_math_symbol(print_mode, :neq), " ", _string_round(ub1))
else
return string(_math_symbol(print_mode, :in), " [", _string_round(lb1),
", ", _string_round(ub1), ") ",
_math_symbol(print_mode, :union), " (", _string_round(lb2),
", ", _string_round(ub2), "]")
end
end

## Extend in domain string to consider domain restrictions
# IntervalDomain
function in_domain_string(print_mode,
Expand All @@ -202,8 +222,16 @@ function in_domain_string(print_mode,
# determine if in restrictions
in_restrictions = haskey(restrictions, pref)
# make the string
interval = in_restrictions ? restrictions[pref] : domain
return in_domain_string(print_mode, interval)
if in_restrictions && restrictions.invert
return in_domain_string(print_mode, domain.lower_bound,
restrictions[pref].lower_bound,
restrictions[pref].upper_bound,
domain.upper_bound)
elseif in_restrictions
return in_domain_string(print_mode, restrictions[pref])
else
return in_domain_string(print_mode, domain)
end
end

# InfiniteScalarDomain
Expand All @@ -212,7 +240,7 @@ function in_domain_string(print_mode,
domain::InfiniteScalarDomain,
restrictions::DomainRestrictions{GeneralVariableRef})::String
# determine if in restrictions
if haskey(restrictions, pref)
if !restrictions.invert && haskey(restrictions, pref)
bound_domain = restrictions[pref]
if JuMP.lower_bound(bound_domain) == JuMP.upper_bound(bound_domain)
return in_domain_string(print_mode, bound_domain)
Expand All @@ -221,6 +249,22 @@ function in_domain_string(print_mode,
_math_symbol(print_mode, :intersect),
" ", domain_string(print_mode, bound_domain))
end
elseif haskey(restrictions, pref)
bound_domain = restrictions[pref]
lb = bound_domain.lower_bound
ub = bound_domain.upper_bound
if JuMP.lower_bound(bound_domain) == JuMP.upper_bound(bound_domain)
return string(_math_symbol(print_mode, :neq), " ",
_string_round(bound_domain.lower_bound))
else
return string(in_domain_string(print_mode, domain), " ",
_math_symbol(print_mode, :intersect),
" ((-", _math_symbol(print_mode, :infty), ", ",
_string_round(lb), ") ",
_math_symbol(print_mode, :union), " (",
_string_round(ub), ", ",
_math_symbol(print_mode, :infty), "))")
end
else
return in_domain_string(print_mode, domain)
end
Expand Down Expand Up @@ -556,12 +600,14 @@ function restrict_string(
print_mode,
restrictions::DomainRestrictions{GeneralVariableRef}
)::String
is_inverted = restrictions.invert
string_list = ""
for (pref, domain) in restrictions
string_list *= string(JuMP.function_string(print_mode, pref), " ",
in_domain_string(print_mode, domain), ", ")
end
return string_list[1:end-2]
str = string_list[1:end-2]
return is_inverted ? string(_math_symbol(print_mode, :logic_not), "(", str, ")") : str
end

## Return the parameter domain string given the object index
Expand Down Expand Up @@ -603,14 +649,17 @@ function _param_domain_string(print_mode, model::InfiniteModel,
filtered_restrictions = filter(e -> e[1].raw_index == MOIUC.key_to_index(index) &&
e[1].index_type == DependentParameterIndex, restrictions)
# build the domain string
if is_eq
if is_eq && !restrictions.invert
domain_str = restrict_string(print_mode, filtered_restrictions)
else
domain_str = string(_remove_name_index(first_gvref), " ",
in_domain_string(print_mode, domain))
if !isempty(filtered_restrictions)
if !isempty(filtered_restrictions) && !restrictions.invert
domain_str *= string(" ", _math_symbol(print_mode, :intersect),
" (", restrict_string(print_mode, filtered_restrictions), ")")
elseif !isempty(filtered_restrictions)
domain_str *= string(" ", _math_symbol(print_mode, :intersect),
" ", restrict_string(print_mode, filtered_restrictions))
end
end
end
Expand Down
12 changes: 7 additions & 5 deletions test/TranscriptionOpt/transcribe.jl
Original file line number Diff line number Diff line change
Expand Up @@ -286,11 +286,13 @@ end
end
# test _support_in_restrictions
@testset "_support_in_restrictions" begin
@test IOTO._support_in_restrictions([0., 0., 0.], [3], [IntervalDomain(0, 0)])
@test IOTO._support_in_restrictions([0., 0., 0.], Int[], IntervalDomain[])
@test IOTO._support_in_restrictions([NaN, 0., 0.], [1], [IntervalDomain(0, 1)])
@test !IOTO._support_in_restrictions([NaN, 0., 0.], [1, 2], [IntervalDomain(1, 1), IntervalDomain(1, 1)])
@test !IOTO._support_in_restrictions([NaN, 0., 2.], [1, 3], [IntervalDomain(1, 1), IntervalDomain(1, 1)])
@test IOTO._support_in_restrictions([0., 0., 0.], [3], [IntervalDomain(0, 0)], false)
@test IOTO._support_in_restrictions([0., 0., 0.], Int[], IntervalDomain[], false)
@test IOTO._support_in_restrictions([NaN, 0., 0.], [1], [IntervalDomain(0, 1)], false)
@test !IOTO._support_in_restrictions([NaN, 0., 0.], [1, 2], [IntervalDomain(1, 1), IntervalDomain(1, 1)], false)
@test !IOTO._support_in_restrictions([NaN, 0., 2.], [1, 3], [IntervalDomain(1, 1), IntervalDomain(1, 1)], false)
@test !IOTO._support_in_restrictions([0., 0., 0.], [3], [IntervalDomain(0, 0)], true)
@test IOTO._support_in_restrictions([0., 0., 1.], [3], [IntervalDomain(0, 0)], true)
end
# test _process_constraint
@testset "_process_constraint" begin
Expand Down
6 changes: 6 additions & 0 deletions test/constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ end
rs = DomainRestrictions(pars => 0)
@test InfiniteOpt._validate_restrictions(m, rs) isa Nothing
@test supports(pars) == zeros(2, 1)
# test inverted conditions
rs = DomainRestrictions(pars => 0.2, invert_logic = true)
@test InfiniteOpt._validate_restrictions(m, rs) isa Nothing
@test supports(pars) == zeros(2, 1)
end
# test _update_var_constr_mapping
@testset "_update_var_constr_mapping" begin
Expand Down Expand Up @@ -352,6 +356,8 @@ end
@test_throws ErrorException InfiniteOpt._update_restrictions(rs1, new_rs)
new_rs = DomainRestrictions(par => -1)
@test_throws ErrorException InfiniteOpt._update_restrictions(rs1, new_rs)
new_rs = DomainRestrictions(par => 0, invert_logic = true)
@test_throws ErrorException InfiniteOpt._update_restrictions(rs1, new_rs)
end
# test add_parameter_restrictions
@testset "add_domain_restrictions" begin
Expand Down
3 changes: 2 additions & 1 deletion test/datatypes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ end
@testset "DataType" begin
@test DomainRestrictions isa UnionAll
d = Dict(par3 => IntervalDomain(0, 1))
@test DomainRestrictions(d) isa DomainRestrictions{GeneralVariableRef}
@test DomainRestrictions(d, true) isa DomainRestrictions{GeneralVariableRef}
@test DomainRestrictions() isa DomainRestrictions{GeneralVariableRef}
end
# test _expand_parameter_tuple
Expand Down Expand Up @@ -303,6 +303,7 @@ end
d = (pars => IntervalDomain(0, 1), par3 => IntervalDomain(0, 1))
@test DomainRestrictions(d).intervals isa Dict{GeneralVariableRef, IntervalDomain}
@test DomainRestrictions(par3 => 0).intervals isa Dict
@test DomainRestrictions(par3 => 0, invert_logic = true).invert
@test_deprecated ParameterBounds(par3 => 0)
end
dr = DomainRestrictions(par3 => [0, 1])
Expand Down
Loading