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 all 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
12 changes: 12 additions & 0 deletions docs/src/analyzer.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,15 @@ and summarize them individually. The following functions are useful for this:
ModelAnalyzer.list_of_issue_types
ModelAnalyzer.list_of_issues
```

It is possible to extract data from the issues with the methods:

```@docs
ModelAnalyzer.variables
ModelAnalyzer.variable
ModelAnalyzer.constraints
ModelAnalyzer.constraint
ModelAnalyzer.set
ModelAnalyzer.values
ModelAnalyzer.value
```
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
14 changes: 14 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,17 @@ ModelAnalyzer.summarize(issues)
# individual issues can also be summarized
ModelAnalyzer.summarize(issues[1])
```

### Non JuMP (or MOI) models

If you dont have a JuMP (or MOI) model, you can still use this package reading from a file.

```julia
model = Model();
@variable(model, x >= 0);
@objective(model, Min, 2 * x + 1);
filename = joinpath(mktempdir(), "model.mps");
write_to_file(model, filename; generic_names = true)
new_model = read_from_file(filename; use_nlp_block = false)
print(new_model)
```
95 changes: 95 additions & 0 deletions ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# 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(
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
end
return "$jump_ref"
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
end
return "$jump_ref"
end

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

Return the **JuMP** variable reference associated to a particular issue.
"""
function ModelAnalyzer.variable(
issue::ModelAnalyzer.AbstractIssue,
model::JuMP.GenericModel{T},
) where {T}
ref = ModelAnalyzer.variable(issue)
return JuMP.GenericVariableRef{T}(model, ref)
end

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

Return the **JuMP** variable references associated to a particular issue.
"""
function ModelAnalyzer.variables(
issue::ModelAnalyzer.AbstractIssue,
model::JuMP.GenericModel{T},
) where {T}
refs = ModelAnalyzer.variables(issue)
return JuMP.GenericVariableRef{T}.(model, refs)
end

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

Return the **JuMP** constraint reference associated to a particular issue.
"""
function ModelAnalyzer.constraint(
issue::ModelAnalyzer.AbstractIssue,
model::JuMP.GenericModel,
)
ref = ModelAnalyzer.constraint(issue)
return JuMP.constraint_ref_with_index(model, ref)
end

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

Return the **JuMP** constraints reference associated to a particular issue.
"""
function ModelAnalyzer.constraints(
issue::ModelAnalyzer.AbstractIssue,
model::JuMP.GenericModel,
)
ref = ModelAnalyzer.constraints(issue)
return JuMP.constraint_ref_with_index.(model, ref)
end

end # module ModelAnalyzerJuMPExt
108 changes: 100 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 @@ See [`summarize`](@ref), [`list_of_issues`](@ref), and
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 @@ be a subtype of `AbstractIssue`). In the verbose case it will provide a text
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 @@ function summarize(::Type{T}; kwargs...) where {T<:AbstractIssue}
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 @@ const DEFAULT_MAX_ISSUES = 10
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 @@ function summarize(
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,91 @@ function summarize(data::AbstractData; kwargs...)
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 end

"""
values(issue::AbstractIssue)

Return the values associated to a particular issue.
"""
function values end

"""
variable(issue::AbstractIssue)

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

"""
variables(issue::AbstractIssue)

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

"""
constraint(issue::AbstractIssue)

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

"""
constraints(issue::AbstractIssue)

Return the constraints associated to a particular issue.
"""
function constraints(issue::AbstractIssue, ::MOI.ModelLike)
return constraints(issue)
end

"""
set(issue::AbstractIssue)

Return the set associated to a particular issue.
"""
function set 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
end
return "$ref"
end

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

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

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

end
end # module ModelAnalyzer
24 changes: 24 additions & 0 deletions src/_eval_variables.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# 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,
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 = MOI.MA.promote_operation(*, T, S)
out = convert(U, f.constant)
for t in f.terms
out += _eval_variables(value_fn, t)
end
return out
end
Loading
Loading