diff --git a/docs/src/guide/constraint.md b/docs/src/guide/constraint.md index fb6573583..ac5d1ed6b 100644 --- a/docs/src/guide/constraint.md +++ b/docs/src/guide/constraint.md @@ -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! @@ -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 diff --git a/src/TranscriptionOpt/transcribe.jl b/src/TranscriptionOpt/transcribe.jl index d2b903296..06f76ec31 100644 --- a/src/TranscriptionOpt/transcribe.jl +++ b/src/TranscriptionOpt/transcribe.jl @@ -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 @@ -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) diff --git a/src/constraints.jl b/src/constraints.jl index 3987c2a35..ef3365352 100644 --- a/src/constraints.jl +++ b/src/constraints.jl @@ -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) @@ -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 diff --git a/src/datatypes.jl b/src/datatypes.jl index b5e58044a..bab99009b 100644 --- a/src/datatypes.jl +++ b/src/datatypes.jl @@ -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` @@ -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 ################################################################################ @@ -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 @@ -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! @@ -1822,8 +1830,9 @@ 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! @@ -1831,6 +1840,7 @@ 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 @@ -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 diff --git a/src/show.jl b/src/show.jl index 2956652e7..e24d8cf12 100644 --- a/src/show.jl +++ b/src/show.jl @@ -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 @@ -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 @@ -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, @@ -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 @@ -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) @@ -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 @@ -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 @@ -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 diff --git a/test/TranscriptionOpt/transcribe.jl b/test/TranscriptionOpt/transcribe.jl index 3d02ca1a2..b7d746795 100644 --- a/test/TranscriptionOpt/transcribe.jl +++ b/test/TranscriptionOpt/transcribe.jl @@ -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 diff --git a/test/constraints.jl b/test/constraints.jl index 40a7a6b86..618f9cc10 100644 --- a/test/constraints.jl +++ b/test/constraints.jl @@ -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 @@ -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 diff --git a/test/datatypes.jl b/test/datatypes.jl index 3b406160c..13e2f8a45 100644 --- a/test/datatypes.jl +++ b/test/datatypes.jl @@ -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 @@ -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]) diff --git a/test/show.jl b/test/show.jl index 36ff24a0a..a0bdf7951 100644 --- a/test/show.jl +++ b/test/show.jl @@ -38,6 +38,8 @@ @test InfiniteOpt._math_symbol(REPLMode, :infty) == "Inf" @test InfiniteOpt._math_symbol(REPLMode, :Vert) == "||" @test InfiniteOpt._math_symbol(REPLMode, :sub2) == "_2" + @test InfiniteOpt._math_symbol(REPLMode, :neq) == "!=" + @test InfiniteOpt._math_symbol(REPLMode, :logic_not) == "!" else @test InfiniteOpt._math_symbol(REPLMode, :intersect) == "∩" @test InfiniteOpt._math_symbol(REPLMode, :partial) == "∂" @@ -53,6 +55,8 @@ @test InfiniteOpt._math_symbol(REPLMode, :infty) == "∞" @test InfiniteOpt._math_symbol(REPLMode, :Vert) == "‖" @test InfiniteOpt._math_symbol(REPLMode, :sub2) == "₂" + @test InfiniteOpt._math_symbol(REPLMode, :neq) == "≠" + @test InfiniteOpt._math_symbol(REPLMode, :logic_not) == "¬" end @test InfiniteOpt._math_symbol(REPLMode, :times) == "*" @test InfiniteOpt._math_symbol(REPLMode, :prop) == "~" @@ -94,6 +98,8 @@ @test InfiniteOpt._math_symbol(IJuliaMode, :succeq0) == "\\succeq 0" @test InfiniteOpt._math_symbol(IJuliaMode, :Vert) == "\\Vert" @test InfiniteOpt._math_symbol(IJuliaMode, :sub2) == "_2" + @test InfiniteOpt._math_symbol(IJuliaMode, :neq) == "\\neq" + @test InfiniteOpt._math_symbol(IJuliaMode, :logic_not) == "\\neg" @test_throws ErrorException InfiniteOpt._math_symbol(IJuliaMode, :bad) end # test _plural @@ -215,6 +221,22 @@ @test in_domain_string(REPLMode, par1, domain, rs) == str str = InfiniteOpt._math_symbol(IJuliaMode, :eq) * " 0" @test in_domain_string(IJuliaMode, par1, domain, rs) == str + # test in restrictions and inverted with equality + rs = DomainRestrictions(par1 => 0, invert_logic = true) + domain = IntervalDomain(0, 1) + str = InfiniteOpt._math_symbol(REPLMode, :neq) * " 0" + @test in_domain_string(REPLMode, par1, domain, rs) == str + str = InfiniteOpt._math_symbol(IJuliaMode, :neq) * " 0" + @test in_domain_string(IJuliaMode, par1, domain, rs) == str + # test in restrictions and inverted without equality + rs = DomainRestrictions(par1 => [0.1, 0.4], invert_logic = true) + domain = IntervalDomain(0, 1) + str = InfiniteOpt._math_symbol(REPLMode, :in) * " [0, 0.1) " * + InfiniteOpt._math_symbol(REPLMode, :union) * " (0.4, 1]" + @test in_domain_string(REPLMode, par1, domain, rs) == str + str = InfiniteOpt._math_symbol(IJuliaMode, :in) * " [0, 0.1) " * + InfiniteOpt._math_symbol(IJuliaMode, :union) * " (0.4, 1]" + @test in_domain_string(IJuliaMode, par1, domain, rs) == str # test not in restrictions str = InfiniteOpt._math_symbol(REPLMode, :in) * " [0, 1]" @test in_domain_string(REPLMode, pars[1], domain, rs) == str @@ -239,6 +261,26 @@ str = InfiniteOpt._math_symbol(IJuliaMode, :prop) * " Uniform " * InfiniteOpt._math_symbol(IJuliaMode, :intersect) * " [0, 1]" @test in_domain_string(IJuliaMode, par1, domain, rs) == str + # test in restrictions and inverted + rs = DomainRestrictions(par1 => 0, invert_logic = true) + domain = UniDistributionDomain(Uniform()) + str = InfiniteOpt._math_symbol(REPLMode, :neq) * " 0" + @test in_domain_string(REPLMode, par1, domain, rs) == str + str = InfiniteOpt._math_symbol(IJuliaMode, :neq) * " 0" + @test in_domain_string(IJuliaMode, par1, domain, rs) == str + # test in restrictions and not equality and inverted + rs = DomainRestrictions(par1 => [0, 1], invert_logic = true) + domain = UniDistributionDomain(Uniform()) + infy = InfiniteOpt._math_symbol(REPLMode, :infty) + str = InfiniteOpt._math_symbol(REPLMode, :prop) * " Uniform " * + InfiniteOpt._math_symbol(REPLMode, :intersect) * " ((-$infy, 0) " * + InfiniteOpt._math_symbol(REPLMode, :union) * " (1, $infy))" + @test in_domain_string(REPLMode, par1, domain, rs) == str + infy = InfiniteOpt._math_symbol(IJuliaMode, :infty) + str = InfiniteOpt._math_symbol(IJuliaMode, :prop) * " Uniform " * + InfiniteOpt._math_symbol(IJuliaMode, :intersect) * " ((-$infy, 0) " * + InfiniteOpt._math_symbol(IJuliaMode, :union) * " (1, $infy))" + @test in_domain_string(IJuliaMode, par1, domain, rs) == str # test not in restrictions str = InfiniteOpt._math_symbol(REPLMode, :prop) * " Uniform" @test in_domain_string(REPLMode, pars[1], domain, rs) == str @@ -489,6 +531,14 @@ @test InfiniteOpt.restrict_string(REPLMode, rs) == str str = "par1 " * InfiniteOpt._math_symbol(IJuliaMode, :in) * " [0.5, 0.7]" @test InfiniteOpt.restrict_string(IJuliaMode, rs) == str + # test inverted restriction + rs = DomainRestrictions(par1 => [0.5, 0.7], invert_logic = true) + str = "par1 " * InfiniteOpt._math_symbol(REPLMode, :in) * " [0.5, 0.7]" + str = InfiniteOpt._math_symbol(REPLMode, :logic_not) * "($str)" + @test InfiniteOpt.restrict_string(REPLMode, rs) == str + str = "par1 " * InfiniteOpt._math_symbol(IJuliaMode, :in) * " [0.5, 0.7]" + str = InfiniteOpt._math_symbol(IJuliaMode, :logic_not) * "($str)" + @test InfiniteOpt.restrict_string(IJuliaMode, rs) == str end # test constraint_string (Finite constraint) @testset "JuMP.constraint_string (Finite)" begin @@ -537,6 +587,17 @@ str = InfiniteOpt.restrict_string(IJuliaMode, rs) str2 = string(split(str, ", ")[2], ", ", split(str, ", ")[1]) @test InfiniteOpt._param_domain_string(IJuliaMode, m, idx, rs) in [str, str2] + # inverted restrictions + rs = DomainRestrictions(pars[1] => [0, 1], invert_logic = true) + str = "pars " * InfiniteOpt._math_symbol(REPLMode, :prop) * + " IsoNormal(dim: (2)) " * + InfiniteOpt._math_symbol(REPLMode, :intersect) * " " * + restrict_string(REPLMode, rs) + str2 = "pars " * InfiniteOpt._math_symbol(REPLMode, :prop) * + " MvNormal(dim: (2)) " * + InfiniteOpt._math_symbol(REPLMode, :intersect) * " " * + restrict_string(REPLMode, rs) + @test InfiniteOpt._param_domain_string(REPLMode, m, idx, rs) in [str, str2] # other set without equalities and including in the restrictions rs = DomainRestrictions(pars[1] => [0, 1]) str = "pars " * InfiniteOpt._math_symbol(REPLMode, :prop) *