diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml new file mode 100644 index 00000000..df92e1b7 --- /dev/null +++ b/.JuliaFormatter.toml @@ -0,0 +1,7 @@ +# Configuration file for JuliaFormatter.jl, see: https://domluna.github.io/JuliaFormatter.jl/stable/config/ + +always_for_in = true +always_use_return = true +margin = 80 +remove_extra_newlines = true +short_to_long_function_def = true diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml new file mode 100644 index 00000000..083baf95 --- /dev/null +++ b/.github/workflows/TagBot.yml @@ -0,0 +1,15 @@ +name: TagBot +on: + issue_comment: + types: + - created + workflow_dispatch: +jobs: + TagBot: + if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' + runs-on: ubuntu-latest + steps: + - uses: JuliaRegistries/TagBot@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ssh: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 00000000..ed224493 --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,23 @@ +name: Documentation +on: + push: + branches: [master] + tags: '*' + pull_request: + types: [opened, synchronize, reopened] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@latest + with: + # Build documentation on Julia 1.0 + version: '1.0' + - name: Install dependencies + run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' + - name: Build and deploy + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # For authentication with GitHub Actions token + DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # For authentication with SSH deploy key + run: julia --project=docs/ docs/make.jl diff --git a/src/eago_semiinfinite/interface/optimizer.jl b/src/eago_semiinfinite/interface/optimizer.jl new file mode 100644 index 00000000..e1556f73 --- /dev/null +++ b/src/eago_semiinfinite/interface/optimizer.jl @@ -0,0 +1,68 @@ +Base.@kwdef mutable struct SIPOptimizer <: MOI.AbstractOptimizer + objective_sense::MOI.OptimizationSense = MOI.FEASIBLE_SENSE + time_limit::Float64 = 1000.0 + is_silent::Bool = false + x::Vector{Float64} = Float64[] + obj_bound::Float64 = 0.0 + obj_value::Float64 = 0.0 + termination_status::MOI.TerminationStatusCode = MOI.OPTIMIZE_NOT_CALLED + result_status_code::MOI.ResultStatusCode = MOI.UNKNOWN_RESULT_STATUS + run_time::Float64 = 0.0 +end + +function MOI.empty!(m::SIPOptimizer) +end +function MOI.is_empty(m::Optimizer) +end + +function MOI.set(m::SIPOptimizer, ::MOI.Silent, value) + m.is_silent = value + return nothing +end + +function MOI.set(m::SIPOptimizer, ::MOI.TimeLimitSec, value::Nothing) + m.time_limit = Inf + return nothing +end +function MOI.set(m::SIPOptimizer, ::MOI.TimeLimitSec, value::Float64) + m.time_limit = value + return nothing +end + +MOI.set(m::SIPOptimizer, p::MOI.RawParameter, value) = error("The SIPOptimizer does not have any raw parameters.") + +function MOI.get(m::SIPOptimizer, ::MOI.ListOfVariableIndices) +end +function MOI.get(m::SIPOptimizer, ::MOI.NumberOfVariables) +end + +function MOI.get(m::SIPOptimizer, ::MOI.ObjectiveValue) + +end +function MOI.get(m::SIPOptimizer, ::MOI.ObjectiveBound) + +end + +MOI.get(m::SIPOptimizer, p::MOI.RawParameter) = error("The SIPOptimizer does not have any raw parameters.") + + +function MOI.get(m::SIPOptimizer, ::MOI.RelativeGap) + abs(objective_bound() - objective_value())/objective_value() +end +MOI.get(m::SIPOptimizer, ::MOI.SolverName) = "EAGO: SIPOptimizer" +MOI.get(m::SIPOptimizer, ::MOI.TerminationStatus) = m._termination_status_code +MOI.get(m::SIPOptimizer, ::MOI.PrimalStatus) = m.result_status_code +MOI.get(m::SIPOptimizer, ::MOI.SolveTime) = m.run_time +MOI.get(m::SIPOptimizer, ::MOI.ResultCount) = (m.result_status_code === MOI.FEASIBLE_POINT) ? 1 : 0 +MOI.get(m::SIPOptimizer, ::MOI.TimeLimitSec) = m.time_limit + +MOI.supports(::SIPOptimizer, ::MOI.TimeLimitSec) = true +MOI.supports(::SIPOptimizer, ::MOI.ObjectiveSense) = true +function MOI.set(m::SIPOptimizer, ::MOI.ObjectiveSense, sense::MOI.OptimizationSense) + m.objective_sense = sense + return nothing +end + + +function MOI.optimize!(m::SIPOptimizer) +end diff --git a/src/eago_semiinfinite/interface/scratch_sheet.jl b/src/eago_semiinfinite/interface/scratch_sheet.jl index 190965e3..baf5316d 100644 --- a/src/eago_semiinfinite/interface/scratch_sheet.jl +++ b/src/eago_semiinfinite/interface/scratch_sheet.jl @@ -1,104 +1,29 @@ #= =# - - - -#= -Basic idea behind syntax -mSIP = SIPModel() - -@decision_variable(mSIP, 0.0 <= x <= 1.0) -@decision_variable(mSIP, z, Bin) -@uncertain_variable(mSIP, p, 0.0 <= x <= 1.0) - - -@constraint(mSIP, ...) -@objective(mSIP, Min, ...) - -@NLconstraint(mSIP, ...) -@NLobjective(mSIP, Min, ...) - -optimize!() -=# - using JuMP -import JuMP.object_dictionary +function pseudo_copy(m::JuMP.Model) -@enum(SIPCons, DECISION, UNCERTAIN, SEMIINFINITE, SIPNOTSET) + nlp_new = deepcopy(m.nlp_data) + nlp_new.evaluator = NLPEvaluator(m) + MOI.initialize(nlp_new.evaluator, Symbol[:ExprGraph]) -""" -""" -Base.@kwdef mutable struct SIPModel <: JuMP.AbstractModel - m::JuMP.Model = JuMP.Model() - p::Dict{JuMP.VariableRef,Bool} = Dict{JuMP.VariableRef,Bool}() - constr_type::Dict{JuMP.ConstraintRef, SIPCons} = Dict{JuMP.ConstraintRef, SIPCons}() - nl_constraint_type::Dict{Int, SIPCons} = Dict{Int, SIPCons}() - nl_expression_type::Dict{Int, SIPCons} = Dict{Int, SIPCons}() -end + obj_expr = MOI.objective_expr(nlp_new.evaluator) + con_expr = MOI.constraint_expr(nlp_new.evaluator, 1) -#= -One option is to fully extend JuMP... That requires alot of function definitions -and the only new functionality we actually want to add is classifying constraints -based on is it a decision variable or is it a -=# -macro decision_variable(args...) - esc(quote - mSIP = $args[1] - inputs_2f = $(args[2:end]...) - @show mSIP - @show inputs_2f - #inputs = $(esc(args)) - #@show inputs - #@show typeof(inputs) - vi = @variable($(args...)) - #model = $inputs[1] - #for vi in variable_indices - # model.p[vi] = false - #end - # return vi - end) -end + @show obj_expr + @show con_expr -mSIP = SIPModel() -@decision_variable(mSIP, x) - -#= -using MathOptInterface + return nothing +end m = Model() -@variable(m, 0 <= y <= 1) -@variable(m, x) -@variable(m, a, Bin) -@variable(m, q[i=1:2]) -@constraint(m, y^2 + y + x<= 0) -@constraint(m, [x, y-1, y-2] in SecondOrderCone()) -@NLconstraint(m, sin(x) + cos(y) <= 0.0) -@constraint(m, 2x - 1 ⟂ x) -@constraint(m, q in SOS2([3,5])) -@constraint(m, a => {x + y <= 1}) -@SDconstraint(m, [x 2x; 3x 4x] >= ones(2, 2)) +@variable(m, x[1:3]) +@variable(m, y) +@NLconstraint(m, x[1] + y^2 <= 0.0) +@NLobjective(m, Min, x[2]*x[3]*y^2) -A = [1 2; 3 4] -b = [5,6] -@constraint(m, con, A * x .== b) +nlp_data = m.nlp_data - -list = list_of_constraint_types(m) - -cons0 = all_constraints(m, VariableRef, MOI.LessThan{Float64}) -cons1 = all_constraints(m, VariableRef, MOI.GreaterThan{Float64}) -cons2 = all_constraints(m, GenericQuadExpr{Float64,VariableRef}, MOI.LessThan{Float64}) -cons2 = all_constraints(m, GenericQuadExpr{Float64,VariableRef}, MOI.LessThan{Float64}) - -cons0_1 = cons0[1] -cons1_1 = cons1[1] -cons2_1 = cons2[1] -cons3_1 = cons2[1] - -out = constraint_object(cons2_1).func -l_terms = linear_terms(out) -q_terms = quad_terms(out) -#all_consts = all_constraints(m, list[1][1], list[1][1]) -=# +out = pseudo_copy(m) diff --git a/src/eago_semiinfinite/interface/sip_model.jl b/src/eago_semiinfinite/interface/sip_model.jl index bd0a1d4d..5a3d5712 100644 --- a/src/eago_semiinfinite/interface/sip_model.jl +++ b/src/eago_semiinfinite/interface/sip_model.jl @@ -12,17 +12,90 @@ for a discussion of the roadmap here. =# @enum(SIPCons, DECISION, UNCERTAIN, SEMIINFINITE, SIPNOTSET) +const MOIU_AUTO_CACHE = MOIU.CachingOptimizerMode.AUTOMATIC - -""" -""" Base.@kwdef mutable struct SIPData p::Dict{JuMP.VariableRef,Bool} = Dict{JuMP.VariableRef,Bool}() constr_type::Dict{JuMP.ConstraintRef, SIPCons} = Dict{JuMP.ConstraintRef, SIPCons}() nl_constraint_type::Dict{Int, SIPCons} = Dict{Int, SIPCons}() nl_expression_type::Dict{Int, SIPCons} = Dict{Int, SIPCons}() + model_decision::Model = Model(caching_mode = MOIU_AUTO_CACHE) + model_uncertain::Model = Model((aching_mode = MOIU_AUTO_CACHE) + model_semiinfinite::Model = Model(caching_mode = MOIU_AUTO_CACHE) +end +uncertain_variable_num(d::SIPData) = any(x -> x, d.p) + +function _copy_nlp_objective!(out::JuMP.Model, m::JuMP.Model, nlp_data::JuMP._NLPData) + if nlp_data.nlobj !== nothing + # TODO CHECK THAT THE FORMULATION IS PEACHY + obj_expr = MOI.objective_expr(nlp_new.evaluator) + # obj_expr_jref = substitute moi variables to jump refs... + @NLobjective(m.ext[:sip].model_decision, Min, obj_expr_jref) + end + return nothing +end + +function _copy_nlp_constraints!(m::JuMP.Model, nlp_data::JuMP._NLPData) + for i = 1:length(nlp_data.nlconstr) + con_expr = MOI.constraint_expr(nlp_data.evaluator, i) + end + return nothing +end + +""" + +Populates variables and constraints into one of three models based on classification: +1) store decision variables and constraints containing only decision variables in +`m.model_decision`, 2) store uncertain variables and constraints containing only +uncertain variables in `m.model_uncertain`, 3) store all variables and all +constraints containing both decision variables to `m.model_semiinfinite`. +""" +function initialize_pure_models!(m::JuMP.Model) + if haskey(m.ext, :sip) + + # extract sip data and storage models + sip_data = _get_sip_data(m)::SIPData + m_dec = sip_data.model_decision + m_unc = sip_data.model_uncertain + m_sip = sip_data.model_semiinfinite + + # get index map + indx_map_dec = MOI.copy_to(backend(m_dec), backend(m), copy_names = true) + indx_map_unc = MOI.copy_to(backend(m_unc), backend(m), copy_names = true) + indx_map_sip = MOI.copy_to(backend(m_sip), backend(m), copy_names = true) + + # copy extension data + for (key, data) in m.ext + if key != :sip + m_dec.ext[key] = copy_extension_data(data, m_dec, m) + m_unc.ext[key] = copy_extension_data(data, m_unc, m) + m_sip.ext[key] = copy_extension_data(data, m_sip, m) + end + end + + # copy objects (TODO need to check on SIP, DEC, UNC) + reference_map = ReferenceMap(new_model, index_map) + for (name, value) in object_dictionary(model) + new_model[name] = getindex.(reference_map, value) + end + + if m.nlp_data !== nothing + nlp_data = deepcopy(m.nlp_data) + nlp_data.evaluator = NLPEvaluator(m) + MOI.initialize(nlp_data.evaluator, Symbol[:ExprGraph]) + copy_nlp_objective!(m, nlp_data) + copy_nlp_expressions!(m, nlp_data) + copy_nlp_constraints!(m, nlp_data) + end + + for (vi, is_uncertain) in m.ext.p + model_semiinfinite + model_decision_ + end + return nothing end + function initialize_sip_data(m::JuMP.Model) m.ext[:sip] = SIPData() end @@ -32,7 +105,8 @@ function sip_optimizehook(m::JuMP.Model; kwargs...) if uncertain_variable_num(data) == 0.0 ret = JuMP.optimize!(m::JuMP.Model, ignore_optimize_hook = true, kwargs...) else - # ret = model_sip_solve(m) + initialize_pure_models!(m) + ret = sip_solve(data) end return ret end @@ -46,6 +120,7 @@ end function ModelWithSIP(args...; kwargs...) m = JuMP.Model(args...; kwargs...) + set_optimizer(SIPOptimizer) enable_semiinfinite(m) return m end @@ -58,16 +133,36 @@ arguments `kw_args` and returns the variable. @uncertain_variable(m, expr, args..., kw_args...) -This macro simply denotes the variable as belonging to the set of descision +This macro simply denotes the variable as belonging to the set of uncertain variables and then performs `@variable(m, expr, args..., kw_args...)` to add the variable to the basic JuMP model for storage. All JuMP syntax is supported. """ macro uncertain_variable(args...) - vi = @variable(args...) - model = esc(args[1]) - for vi in variable_indices - model.p[vi] = true - end - return vi + esc(quote + vi = @variable($(args...)) + $(args[1]).ext[:sip].p[vi] = true + vi + end) +end + +""" + @decision_variable + +Add an *anonymous* variable to `m::SIPModel` described by the keyword +arguments `kw_args` and returns the variable. + + @decision_variable(m, expr, args..., kw_args...) + +This macro simply denotes the variable as belonging to the set of decision +variables and then performs `@variable(m, expr, args..., kw_args...)` to +add the variable to the basic JuMP model for storage. All JuMP syntax is +supported. +""" +macro decision_variable(args...) + esc(quote + vi = @variable($(args...)) + $(args[1]).ext[:sip].p[vi] = false + vi + end) end