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

[RFC] Feasibility and Optimality checker #1

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
12 changes: 12 additions & 0 deletions .github/workflows/TagBot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: TagBot
on:
schedule:
- cron: 0 * * * *
jobs:
TagBot:
runs-on: ubuntu-latest
Comment on lines +1 to +7
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is an update TagBot job

ame: 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 }}

steps:
- uses: JuliaRegistries/TagBot@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
ssh: ${{ secrets.DOCUMENTER_KEY }}
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: CI
on:
push:
branches: [master]
pull_request:
types: [opened, synchronize, reopened]
jobs:
test:
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- version: '1'
os: ubuntu-latest
arch: x64
- version: '1.0'
os: ubuntu-latest
arch: x64
steps:
- uses: actions/checkout@v2
- uses: julia-actions/setup-julia@v1
with:
version: ${{ matrix.version }}
arch: ${{ matrix.arch }}
- uses: actions/cache@v1
env:
cache-name: cache-artifacts
with:
path: ~/.julia/artifacts
key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }}
restore-keys: |
${{ runner.os }}-test-${{ env.cache-name }}-
${{ runner.os }}-test-
${{ runner.os }}-
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1
- uses: julia-actions/julia-processcoverage@v1
- uses: codecov/codecov-action@v1
with:
file: lcov.info
20 changes: 20 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,23 @@ name = "FeasibilityOptInterface"
uuid = "427ef5fb-2141-4365-8ed4-44d519970edf"
authors = ["Joaquim Garcia <joaquimgarcia@psr-inc.com>"]
version = "0.1.0"

[deps]
Dualization = "191a621a-6537-11e9-281d-650236a99e60"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
MathOptSetDistances = "3b969827-a86c-476c-9527-bb6f1a8fbad5"

[compat]
julia = "1"
Dualization = "0.3.3"
JuMP = "0.21.6"
MathOptInterface = "0.9.18"
MathOptSetDistances = "0.1.1"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test"]
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
# FeasibilityOptInterface
# FeasibilityOptInterface

This package provides functionality for testing solutions of JuMP and
MathOptInterface.

### Warning

* Non-linear constraints are not being checked for now
21 changes: 20 additions & 1 deletion src/FeasibilityOptInterface.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
module FeasibilityOptInterface

greet() = print("Hello World!")
import Dualization
import JuMP
import LinearAlgebra
import MathOptInterface
import MathOptSetDistances

const MOD = MathOptSetDistances
const MOI = MathOptInterface
const MOIU = MOI.Utilities

const VI = MOI.VariableIndex
const CI = MOI.ConstraintIndex

const CR{F,S} = JuMP.ConstraintRef{JuMP.Model, CI{F,S}}

include("checker.jl")
include("primal.jl")
include("dual.jl")
include("complementarity.jl")
include("jump.jl")

end # module
236 changes: 236 additions & 0 deletions src/checker.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
variable_primal(model) = x -> MOI.get(model, MOI.VariablePrimal(), x)
variable_primal_start(model) = x -> MOI.get(model, MOI.VariablePrimalStart(), x)

constraint_dual(model) = x -> MOI.get(model, MOI.ConstraintDual(), x)
constraint_dual_start(model) = x -> MOI.get(model, MOI.ConstraintDual(), x)

struct EmptyDict{K,V} <: AbstractDict{K,V}
end
EmptyDict() = EmptyDict{Any,Any}()
Base.haskey(::EmptyDict, key) = false
Base.iterate(::EmptyDict) = nothing


"""
options

* `varval` can be used to define a map `vi -> value` where `vi` is a
`MOI.VariableIndex` in the model. This map should be defined for all variables
tha appear in the constraint.

* `distance_map` is an abstract dictionary mapping function-set pairs `(F, S)`
to `distance`s that are instances of `AbstractDistance`. The function
`distance_to_set(distance, v, set)` must be implemented for this type for the
set of the constraint `con`. Is some function-set pair `(F, S)` is not found in
the keys of the dictionay then the MOD.DefaultDistance() is used.

* `tol` is used to ignore violations larger than its value.

* `names`: is false, constraint names are not displayed in the violation report.

* `index`: is false, constraint indexes are not displayed in the violation report.

"""
mutable struct FeasibilityChecker
jump::Union{JuMP.AbstractModel, Nothing}

primal::MOI.ModelLike
check_dual::Bool
dual::Union{Dualization.DualProblem, Nothing}
dual_vi_primal_con::Union{Dict, Nothing}

distance_map::AbstractDict
varval::Function
condual::Function

tol::Number
primal_tol::Number
dual_tol::Number
complement_tol::Number
objective_tol::Number

remove_moi_str::Bool

print_below_tol::Bool
print_names::Bool
print_index::Bool
print_distance::Bool

# add tolerance per constraint type ?
# complement bound is in sum. should it be individual?
end
function FeasibilityChecker(model::Union{
MOI.ModelLike, JuMP.AbstractModel
};
check_dual::Bool = false,

distance_map::AbstractDict = EmptyDict(),
varval::Function = variable_primal(model),
condual::Function = constraint_dual(model),

tol::Real = 0.0,
primal_tol::Real = -1.0,
dual_tol::Real = -1.0,
complement_tol::Real = -1.0,
objective_tol::Real = -1.0,

remove_moi_str::Bool = true,
print_below_tol::Bool = true,
print_names::Bool = true,
print_index::Bool = true,
print_distance::Bool = true,
)
jump_model = _jump_model(model)
varmap = if jump_model ===nothing
varval
else
x->varval(JuMP.VariableRef(model, x))
end
conmap = if jump_model ===nothing
condual
else
x->condual(JuMP.constraint_ref_with_index(model, x))
end
sense = MOI.get(model, MOI.ObjectiveSense())::MOI.OptimizationSense
if sense == MOI.FEASIBILITY_SENSE && check_dual
@warn "Dual checks disabled, because model sense is FEASIBILITY_SENSE"
check_dual = false
end
return FeasibilityChecker(
jump_model,
moi_model(model),
check_dual,
nothing,
nothing,

distance_map,
varmap,
condual,

tol,
primal_tol,
dual_tol,
complement_tol,
objective_tol,
remove_moi_str,
print_below_tol,
print_names,
print_index,
print_distance,
)
end

_jump_model(model::JuMP.AbstractModel) = model
_jump_model(::MOI.ModelLike) = nothing
moi_model(model::JuMP.AbstractModel) = JuMP.backend(model)
moi_model(model::MOI.ModelLike) = model

# lazy loading of dual model
function _load_dual(checker::FeasibilityChecker)
if checker.check_dual && checker.dual === nothing
checker.dual = Dualization.dualize(checker.primal)
checker.dual_vi_primal_con = _build_dual_var_map(checker.dual)
end
end

function _has_dual_model(checker::FeasibilityChecker)
_load_dual(checker)
if checker.dual === nothing
error(
"FeasibilityChecker dual model was disabled with `check_dual`. Re-enable this option if you want to use this function."
)
end
end

function primal_dual_constraint_violation_report(checker::FeasibilityChecker)
_has_dual_model(checker)
return constraint_violation_report(checker) *
dual_constraint_violation_report(checker)
end

function report(checker::FeasibilityChecker)
if checker.check_dual
_has_dual_model(checker)
str = ""
str *= objective_report(checker)
str *= primal_dual_constraint_violation_report(checker)
str *= complement_violation_report(checker)
str *= dual_complement_violation_report(checker)
else
str = ""
str *= objective_report(checker)
str *= constraint_violation_report(checker)
end
return _remove_moi(str, checker)
end

function is_primal_feasible(checker::FeasibilityChecker)
largest, _ = constraint_violation(checker::FeasibilityChecker)
return largest < _primal_tol(checker)
end
function is_dual_feasible(checker::FeasibilityChecker)
_has_dual_model(checker)
largest, _ = dual_constraint_violation(checker::FeasibilityChecker)
return largest < _dual_tol(checker)
end

function is_complement(checker::FeasibilityChecker)
_has_dual_model(checker)
total_p, _, _ = complement_violation(checker)
total_d, _, _ = dual_complement_violation(checker)
return total_p + total_d < _complement_tol(checker)
end

function is_zero_gap(checker::FeasibilityChecker)
_has_dual_model(checker)
return objective_gap(checker) < _objective_tol(checker)
end

function is_optimal(checker::FeasibilityChecker)
_has_dual_model(checker)
return is_primal_feasible(checker) &&
is_dual_feasible(checker) &&
is_complement(checker) &&
is_zero_gap(checker)
end


function _primal_tol(checker::FeasibilityChecker)
if checker.primal_tol < 0
return checker.tol
else
return checker.primal_tol
end
end
function _dual_tol(checker::FeasibilityChecker)
if checker.dual_tol < 0
return checker.tol
else
return checker.dual_tol
end
end
function _complement_tol(checker::FeasibilityChecker)
if checker.complement_tol < 0
return checker.tol
else
return checker.complement_tol
end
end
function _objective_tol(checker::FeasibilityChecker)
if checker.objective_tol < 0
return checker.tol
else
return checker.objective_tol
end
end

function _remove_moi(str, remove::Bool)
if remove
return replace(str, "MathOptInterface." => "")
else
return str
end
end
function _remove_moi(str, checker::FeasibilityChecker)
_remove_moi(str, checker.remove_moi_str)
end
Loading