Skip to content

Move code to MOI and add JuMP layer #19

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

Merged
merged 36 commits into from
May 24, 2025
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4018990
Move code to MOI and add JuMP layer
joaquimg Apr 30, 2025
4f716b0
format
joaquimg May 1, 2025
3185a6f
Improve API
joaquimg May 1, 2025
cea96e6
fixes
joaquimg May 1, 2025
7b0c0a2
name_source -> model
joaquimg May 1, 2025
2c79007
suggestions
joaquimg May 1, 2025
9241ee1
small fix
joaquimg May 1, 2025
9c83751
Merge branch 'main' into jg/moi
joaquimg May 1, 2025
ed42075
feasibility almost ready
joaquimg May 7, 2025
87aacb2
Finish feasibility rewrite
joaquimg May 7, 2025
e378889
Merge branch 'main' into jg/moi
joaquimg May 7, 2025
85e00fb
MOI infeasibility analysis
joaquimg May 8, 2025
38ae419
use MA
joaquimg May 9, 2025
2e39b0c
MOI IIS
joaquimg May 13, 2025
997200a
format
joaquimg May 13, 2025
e059c41
add query functions in numerical
joaquimg May 13, 2025
fc09db0
format
joaquimg May 13, 2025
f0d70fa
Add query tools
joaquimg May 13, 2025
833e6c1
Add query tools
joaquimg May 13, 2025
a88f0e8
add tests
joaquimg May 14, 2025
1cd08a3
add tests
joaquimg May 22, 2025
b5253c7
add tests
joaquimg May 22, 2025
4fc7565
add comments
joaquimg May 22, 2025
5ecd302
simplify api
joaquimg May 22, 2025
ceccb9f
cleanup api
joaquimg May 22, 2025
97cde56
add tests
joaquimg May 22, 2025
0b3e9e4
add tests
joaquimg May 22, 2025
e1288d6
format
joaquimg May 22, 2025
e719b96
more tests
joaquimg May 23, 2025
7e36757
more tests
joaquimg May 23, 2025
90903ee
fix and test
joaquimg May 23, 2025
1426502
add tests and cleanup
joaquimg May 23, 2025
6f641ad
format
joaquimg May 23, 2025
e9035d7
add features and tests
joaquimg May 24, 2025
ad37c02
add tests
joaquimg May 24, 2025
2fc4f3f
add docs
joaquimg May 24, 2025
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
9 changes: 7 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ 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"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"

[weakdeps]
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"

[extensions]
ModelAnalyzerJuMPExt = "JuMP"

[compat]
Dualization = "0.5.9"
Dualization = "0.6.0"
JuMP = "1.24.0"
MathOptInterface = "1.37.0"
6 changes: 5 additions & 1 deletion docs/Project.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[deps]
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
ModelAnalyzer = "d1179b25-476b-425c-b826-c7787f0fff83"
ModelAnalyzer = "d1179b25-476b-425c-b826-c7787f0fff83"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"

[compat]
JuMP = "1.24.0"
2 changes: 1 addition & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Documenter, ModelAnalyzer
using Documenter, ModelAnalyzer, JuMP

makedocs(; sitename = "ModelAnalyzer.jl documentation")

Expand Down
3 changes: 2 additions & 1 deletion docs/src/feasibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ Specifically, the possible issues are:

```@docs
ModelAnalyzer.Feasibility.PrimalViolation
ModelAnalyzer.Feasibility.DualViolation
ModelAnalyzer.Feasibility.DualConstraintViolation
ModelAnalyzer.Feasibility.DualConstrainedVariableViolation
ModelAnalyzer.Feasibility.ComplemetarityViolation
ModelAnalyzer.Feasibility.DualObjectiveMismatch
ModelAnalyzer.Feasibility.PrimalObjectiveMismatch
Expand Down
82 changes: 82 additions & 0 deletions ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Copyright (c) 2025: Joaquim Garcia, Oscar Dowson and contributors
#
# Use of this source code is governed by an MIT-style license that can be found
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.

module ModelAnalyzerJuMPExt

import ModelAnalyzer
import JuMP
import MathOptInterface as MOI

function ModelAnalyzer.analyze(
analyzer::T,
model::JuMP.GenericModel;
kwargs...,
) where {T<:ModelAnalyzer.AbstractAnalyzer}
moi_model = JuMP.backend(model)
result = ModelAnalyzer.analyze(analyzer, moi_model; kwargs...)
return result
end

function ModelAnalyzer._name(

Check warning on line 22 in ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl

View check run for this annotation

Codecov / codecov/patch

ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl#L22

Added line #L22 was not covered by tests
ref::MOI.VariableIndex,
model::JuMP.GenericModel{T},
) where {T}
jump_ref = JuMP.GenericVariableRef{T}(model, ref)
name = JuMP.name(jump_ref)
if !isempty(name)
return name

Check warning on line 29 in ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl

View check run for this annotation

Codecov / codecov/patch

ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl#L26-L29

Added lines #L26 - L29 were not covered by tests
end
return "$jump_ref"

Check warning on line 31 in ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl

View check run for this annotation

Codecov / codecov/patch

ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl#L31

Added line #L31 was not covered by tests
end

function ModelAnalyzer._name(ref::MOI.ConstraintIndex, model::JuMP.GenericModel)
jump_ref = JuMP.constraint_ref_with_index(model, ref)
name = JuMP.name(jump_ref)
if !isempty(name)
return name

Check warning on line 38 in ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl

View check run for this annotation

Codecov / codecov/patch

ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl#L34-L38

Added lines #L34 - L38 were not covered by tests
end
return "$jump_ref"

Check warning on line 40 in ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl

View check run for this annotation

Codecov / codecov/patch

ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl#L40

Added line #L40 was not covered by tests
end

"""
variable(issue::ModelAnalyzer.AbstractIssue, model::JuMP.GenericModel)

Return the **JuMP** variable reference associated to a particular issue.
"""
function ModelAnalyzer.variable(

Check warning on line 48 in ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl

View check run for this annotation

Codecov / codecov/patch

ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl#L48

Added line #L48 was not covered by tests
issue::ModelAnalyzer.AbstractIssue,
model::JuMP.GenericModel{T},
) where {T}
ref = ModelAnalyzer.variable(issue)
return JuMP.GenericVariableRef{T}(model, ref)

Check warning on line 53 in ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl

View check run for this annotation

Codecov / codecov/patch

ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl#L52-L53

Added lines #L52 - L53 were not covered by tests
end

"""
variables(issue::ModelAnalyzer.AbstractIssue, model::JuMP.GenericModel)

Return the **JuMP** variable references associated to a particular issue.
"""
function ModelAnalyzer.variables(

Check warning on line 61 in ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl

View check run for this annotation

Codecov / codecov/patch

ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl#L61

Added line #L61 was not covered by tests
issue::ModelAnalyzer.AbstractIssue,
model::JuMP.GenericModel{T},
) where {T}
refs = ModelAnalyzer.variables(issue)
return JuMP.GenericVariableRef{T}.(model, refs)

Check warning on line 66 in ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl

View check run for this annotation

Codecov / codecov/patch

ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl#L65-L66

Added lines #L65 - L66 were not covered by tests
end

"""
constraint(issue::ModelAnalyzer.AbstractIssue, model::JuMP.GenericModel)

Return the **JuMP** constraint reference associated to a particular issue.
"""
function ModelAnalyzer.constraint(

Check warning on line 74 in ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl

View check run for this annotation

Codecov / codecov/patch

ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl#L74

Added line #L74 was not covered by tests
issue::ModelAnalyzer.AbstractIssue,
model::JuMP.GenericModel,
)
ref = ModelAnalyzer.constraint(issue)
return JuMP.constraint_ref_with_index(model, ref)

Check warning on line 79 in ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl

View check run for this annotation

Codecov / codecov/patch

ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl#L78-L79

Added lines #L78 - L79 were not covered by tests
end

end # module ModelAnalyzerJuMPExt
103 changes: 95 additions & 8 deletions src/ModelAnalyzer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@

module ModelAnalyzer

import MathOptInterface as MOI

abstract type AbstractIssue end

abstract type AbstractData end

abstract type AbstractAnalyzer end

"""
analyze(analyzer::AbstractAnalyzer, model::JuMP.Model; kwargs...)
analyze(analyzer::AbstractAnalyzer, model::JuMP.GenericModel; kwargs...)

Analyze a JuMP model using the specified analyzer.
Depending on the analyzer, this keyword arguments might vary.
Expand All @@ -25,10 +27,12 @@
function analyze end

"""
summarize([io::IO,] AbstractData; verbose = true, max_issues = 10, kwargs...)
summarize([io::IO,] AbstractData; model = nothing, verbose = true, max_issues = 10, kwargs...)

Print a summary of the analysis results contained in `AbstractData` to the
specified IO stream. If no IO stream is provided, it defaults to `stdout`.
The model that led to the issue can be provided to `model`, it will be
used to generate the name of variables and constraints in the issue summary.
The `verbose` flag controls whether to print detailed information about each
issue (if `true`) or a concise summary (if `false`). The `max_issues` argument
controls the maximum number of issues to display in the summary. If there are
Expand All @@ -41,9 +45,11 @@
explaning the issue. In the non-verbose case it will provide just the issue
name.

summarize([io::IO,] issue::AbstractIssue; verbose = true)
summarize([io::IO,] issue::AbstractIssue; model = nothing, verbose = true)

This variant allows summarizing a single issue instance of type `AbstractIssue`.
The model that led to the issue can be provided to `model`, it will be
used to generate the name of variables and constraints in the issue summary.
"""
function summarize end

Expand Down Expand Up @@ -76,11 +82,16 @@
return summarize(stdout, T; kwargs...)
end

function summarize(io::IO, issue::AbstractIssue; verbose = true)
function summarize(
io::IO,
issue::AbstractIssue;
model = nothing,
verbose = true,
)
if verbose
return _verbose_summarize(io, issue)
return _verbose_summarize(io, issue, model)
else
return _summarize(io, issue)
return _summarize(io, issue, model)
end
end

Expand All @@ -93,6 +104,7 @@
function summarize(
io::IO,
issues::Vector{T};
model = nothing,
verbose = true,
max_issues = DEFAULT_MAX_ISSUES,
) where {T<:AbstractIssue}
Expand All @@ -110,7 +122,7 @@
end
for issue in first(issues, max_issues)
print(io, " * ")
summarize(io, issue, verbose = verbose)
summarize(io, issue, verbose = verbose, model = model)
print(io, "\n")
end
return
Expand All @@ -124,11 +136,86 @@
return summarize(stdout, data; kwargs...)
end

"""
value(issue::AbstractIssue)

Return the value associated to a particular issue. The value is a number
with a different meaning depending on the type of issue. For example, for
some numerical issues, it can be the coefficient value.
"""
function value(issue::AbstractIssue, ::Nothing)
return value(issue)

Check warning on line 147 in src/ModelAnalyzer.jl

View check run for this annotation

Codecov / codecov/patch

src/ModelAnalyzer.jl#L146-L147

Added lines #L146 - L147 were not covered by tests
end

function value(issue::AbstractIssue, ::MOI.ModelLike)
return value(issue)

Check warning on line 151 in src/ModelAnalyzer.jl

View check run for this annotation

Codecov / codecov/patch

src/ModelAnalyzer.jl#L150-L151

Added lines #L150 - L151 were not covered by tests
end

"""
variable(issue::AbstractIssue)

Return the variable associated to a particular issue.
"""
function variable(issue::AbstractIssue, ::Nothing)
return variable(issue)

Check warning on line 160 in src/ModelAnalyzer.jl

View check run for this annotation

Codecov / codecov/patch

src/ModelAnalyzer.jl#L159-L160

Added lines #L159 - L160 were not covered by tests
end

function variable(issue::AbstractIssue, ::MOI.ModelLike)
return variable(issue)

Check warning on line 164 in src/ModelAnalyzer.jl

View check run for this annotation

Codecov / codecov/patch

src/ModelAnalyzer.jl#L163-L164

Added lines #L163 - L164 were not covered by tests
end

"""
variables(issue::AbstractIssue)

Return the variables associated to a particular issue.
"""
function variables(issue::AbstractIssue, ::Nothing)
return variables(issue)

Check warning on line 173 in src/ModelAnalyzer.jl

View check run for this annotation

Codecov / codecov/patch

src/ModelAnalyzer.jl#L172-L173

Added lines #L172 - L173 were not covered by tests
end

function variables(issue::AbstractIssue, ::MOI.ModelLike)
return variables(issue)

Check warning on line 177 in src/ModelAnalyzer.jl

View check run for this annotation

Codecov / codecov/patch

src/ModelAnalyzer.jl#L176-L177

Added lines #L176 - L177 were not covered by tests
end

"""
constraint(issue::AbstractIssue)

Return the constraint associated to a particular issue.
"""
function constraint(issue::AbstractIssue, ::Nothing)
return constraint(issue)

Check warning on line 186 in src/ModelAnalyzer.jl

View check run for this annotation

Codecov / codecov/patch

src/ModelAnalyzer.jl#L185-L186

Added lines #L185 - L186 were not covered by tests
end

function constraint(issue::AbstractIssue, ::MOI.ModelLike)
return constraint(issue)

Check warning on line 190 in src/ModelAnalyzer.jl

View check run for this annotation

Codecov / codecov/patch

src/ModelAnalyzer.jl#L189-L190

Added lines #L189 - L190 were not covered by tests
end

function _verbose_summarize end

function _summarize end

function _name(ref::MOI.VariableIndex, model::MOI.ModelLike)
name = MOI.get(model, MOI.VariableName(), ref)
if !isempty(name)
return name

Check warning on line 200 in src/ModelAnalyzer.jl

View check run for this annotation

Codecov / codecov/patch

src/ModelAnalyzer.jl#L197-L200

Added lines #L197 - L200 were not covered by tests
end
return "$ref"

Check warning on line 202 in src/ModelAnalyzer.jl

View check run for this annotation

Codecov / codecov/patch

src/ModelAnalyzer.jl#L202

Added line #L202 was not covered by tests
end

function _name(ref::MOI.ConstraintIndex, model::MOI.ModelLike)
name = MOI.get(model, MOI.ConstraintName(), ref)
if !isempty(name)
return name

Check warning on line 208 in src/ModelAnalyzer.jl

View check run for this annotation

Codecov / codecov/patch

src/ModelAnalyzer.jl#L205-L208

Added lines #L205 - L208 were not covered by tests
end
return "$ref"

Check warning on line 210 in src/ModelAnalyzer.jl

View check run for this annotation

Codecov / codecov/patch

src/ModelAnalyzer.jl#L210

Added line #L210 was not covered by tests
end

function _name(ref, ::Nothing)
return "$ref"
end

include("numerical.jl")
include("feasibility.jl")
include("infeasibility.jl")

end
end # module ModelAnalyzer
31 changes: 31 additions & 0 deletions src/_eval_variables.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Copyright (c) 2025: Joaquim Garcia, Oscar Dowson and contributors
#
# Use of this source code is governed by an MIT-style license that can be found
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.

function _eval_variables end

function _eval_variables(value_fn::Function, t::MOI.ScalarAffineTerm)
return t.coefficient * value_fn(t.variable)
end

function _eval_variables(value_fn::Function, t::MOI.ScalarQuadraticTerm)
out = t.coefficient * value_fn(t.variable_1) * value_fn(t.variable_2)
return t.variable_1 == t.variable_2 ? out / 2 : out

Check warning on line 14 in src/_eval_variables.jl

View check run for this annotation

Codecov / codecov/patch

src/_eval_variables.jl#L12-L14

Added lines #L12 - L14 were not covered by tests
end

_eval_variables(value_fn::Function, f::MOI.VariableIndex) = value_fn(f)

Check warning on line 17 in src/_eval_variables.jl

View check run for this annotation

Codecov / codecov/patch

src/_eval_variables.jl#L17

Added line #L17 was not covered by tests

function _eval_variables(
value_fn::Function,
f::MOI.ScalarAffineFunction{T},
) where {T}
# TODO: this conversion exists in JuMP, but not in MOI
S = Base.promote_op(value_fn, MOI.VariableIndex)
U = Base.promote_op(*, T, S)
out = convert(U, f.constant)
Copy link
Member Author

Choose a reason for hiding this comment

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

@odow and @blegat
Is there a special reason why JuMP has this and MOI does not?
Performance?

An alternative to this would be a method for converting ScalarAffineFunction{T} into ScalarAffineFunction{R}.

Copy link
Member

Choose a reason for hiding this comment

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

I don't get it, what does not exist in MOI ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Ah yes, MOI only works if the return type of the function is the same as the type of f.constant. In JuMP, we try to be more general to be more user-friendly.

for t in f.terms
out += _eval_variables(value_fn, t)
end
return out
end
Loading
Loading