Skip to content

Commit 8ba404c

Browse files
authored
Move code to MOI and add JuMP layer (#19)
* Move code to MOI and add JuMP layer * format * Improve API * fixes * name_source -> model * suggestions * small fix * feasibility almost ready * Finish feasibility rewrite * MOI infeasibility analysis * use MA * MOI IIS * format * add query functions in numerical * format * Add query tools * Add query tools * add tests * add tests * add tests * add comments * simplify api * cleanup api * add tests * add tests * format * more tests * more tests * fix and test * add tests and cleanup * format * add features and tests * add tests * add docs
1 parent c65e636 commit 8ba404c

16 files changed

+2948
-938
lines changed

Project.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@ version = "0.1.0"
44

55
[deps]
66
Dualization = "191a621a-6537-11e9-281d-650236a99e60"
7-
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
87
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
98
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
109
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
1110

11+
[weakdeps]
12+
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
13+
14+
[extensions]
15+
ModelAnalyzerJuMPExt = "JuMP"
16+
1217
[compat]
13-
Dualization = "0.5.9"
18+
Dualization = "0.6.0"
1419
JuMP = "1.24.0"
1520
MathOptInterface = "1.37.0"

docs/Project.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
[deps]
22
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
3-
ModelAnalyzer = "d1179b25-476b-425c-b826-c7787f0fff83"
3+
ModelAnalyzer = "d1179b25-476b-425c-b826-c7787f0fff83"
4+
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
5+
6+
[compat]
7+
JuMP = "1.24.0"

docs/make.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Documenter, ModelAnalyzer
1+
using Documenter, ModelAnalyzer, JuMP
22

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

docs/src/analyzer.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,15 @@ and summarize them individually. The following functions are useful for this:
2222
ModelAnalyzer.list_of_issue_types
2323
ModelAnalyzer.list_of_issues
2424
```
25+
26+
It is possible to extract data from the issues with the methods:
27+
28+
```@docs
29+
ModelAnalyzer.variables
30+
ModelAnalyzer.variable
31+
ModelAnalyzer.constraints
32+
ModelAnalyzer.constraint
33+
ModelAnalyzer.set
34+
ModelAnalyzer.values
35+
ModelAnalyzer.value
36+
```

docs/src/feasibility.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ Specifically, the possible issues are:
1717

1818
```@docs
1919
ModelAnalyzer.Feasibility.PrimalViolation
20-
ModelAnalyzer.Feasibility.DualViolation
20+
ModelAnalyzer.Feasibility.DualConstraintViolation
21+
ModelAnalyzer.Feasibility.DualConstrainedVariableViolation
2122
ModelAnalyzer.Feasibility.ComplemetarityViolation
2223
ModelAnalyzer.Feasibility.DualObjectiveMismatch
2324
ModelAnalyzer.Feasibility.PrimalObjectiveMismatch

docs/src/index.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,17 @@ ModelAnalyzer.summarize(issues)
129129
# individual issues can also be summarized
130130
ModelAnalyzer.summarize(issues[1])
131131
```
132+
133+
### Non JuMP (or MOI) models
134+
135+
If you dont have a JuMP (or MOI) model, you can still use this package reading from a file.
136+
137+
```julia
138+
model = Model();
139+
@variable(model, x >= 0);
140+
@objective(model, Min, 2 * x + 1);
141+
filename = joinpath(mktempdir(), "model.mps");
142+
write_to_file(model, filename; generic_names = true)
143+
new_model = read_from_file(filename; use_nlp_block = false)
144+
print(new_model)
145+
```
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Copyright (c) 2025: Joaquim Garcia, Oscar Dowson and contributors
2+
#
3+
# Use of this source code is governed by an MIT-style license that can be found
4+
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.
5+
6+
module ModelAnalyzerJuMPExt
7+
8+
import ModelAnalyzer
9+
import JuMP
10+
import MathOptInterface as MOI
11+
12+
function ModelAnalyzer.analyze(
13+
analyzer::T,
14+
model::JuMP.GenericModel;
15+
kwargs...,
16+
) where {T<:ModelAnalyzer.AbstractAnalyzer}
17+
moi_model = JuMP.backend(model)
18+
result = ModelAnalyzer.analyze(analyzer, moi_model; kwargs...)
19+
return result
20+
end
21+
22+
function ModelAnalyzer._name(
23+
ref::MOI.VariableIndex,
24+
model::JuMP.GenericModel{T},
25+
) where {T}
26+
jump_ref = JuMP.GenericVariableRef{T}(model, ref)
27+
name = JuMP.name(jump_ref)
28+
if !isempty(name)
29+
return name
30+
end
31+
return "$jump_ref"
32+
end
33+
34+
function ModelAnalyzer._name(ref::MOI.ConstraintIndex, model::JuMP.GenericModel)
35+
jump_ref = JuMP.constraint_ref_with_index(model, ref)
36+
name = JuMP.name(jump_ref)
37+
if !isempty(name)
38+
return name
39+
end
40+
return "$jump_ref"
41+
end
42+
43+
"""
44+
variable(issue::ModelAnalyzer.AbstractIssue, model::JuMP.GenericModel)
45+
46+
Return the **JuMP** variable reference associated to a particular issue.
47+
"""
48+
function ModelAnalyzer.variable(
49+
issue::ModelAnalyzer.AbstractIssue,
50+
model::JuMP.GenericModel{T},
51+
) where {T}
52+
ref = ModelAnalyzer.variable(issue)
53+
return JuMP.GenericVariableRef{T}(model, ref)
54+
end
55+
56+
"""
57+
variables(issue::ModelAnalyzer.AbstractIssue, model::JuMP.GenericModel)
58+
59+
Return the **JuMP** variable references associated to a particular issue.
60+
"""
61+
function ModelAnalyzer.variables(
62+
issue::ModelAnalyzer.AbstractIssue,
63+
model::JuMP.GenericModel{T},
64+
) where {T}
65+
refs = ModelAnalyzer.variables(issue)
66+
return JuMP.GenericVariableRef{T}.(model, refs)
67+
end
68+
69+
"""
70+
constraint(issue::ModelAnalyzer.AbstractIssue, model::JuMP.GenericModel)
71+
72+
Return the **JuMP** constraint reference associated to a particular issue.
73+
"""
74+
function ModelAnalyzer.constraint(
75+
issue::ModelAnalyzer.AbstractIssue,
76+
model::JuMP.GenericModel,
77+
)
78+
ref = ModelAnalyzer.constraint(issue)
79+
return JuMP.constraint_ref_with_index(model, ref)
80+
end
81+
82+
"""
83+
constraintss(issue::ModelAnalyzer.AbstractIssue, model::JuMP.GenericModel)
84+
85+
Return the **JuMP** constraints reference associated to a particular issue.
86+
"""
87+
function ModelAnalyzer.constraints(
88+
issue::ModelAnalyzer.AbstractIssue,
89+
model::JuMP.GenericModel,
90+
)
91+
ref = ModelAnalyzer.constraints(issue)
92+
return JuMP.constraint_ref_with_index.(model, ref)
93+
end
94+
95+
end # module ModelAnalyzerJuMPExt

src/ModelAnalyzer.jl

Lines changed: 100 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@
55

66
module ModelAnalyzer
77

8+
import MathOptInterface as MOI
9+
810
abstract type AbstractIssue end
911

1012
abstract type AbstractData end
1113

1214
abstract type AbstractAnalyzer end
1315

1416
"""
15-
analyze(analyzer::AbstractAnalyzer, model::JuMP.Model; kwargs...)
17+
analyze(analyzer::AbstractAnalyzer, model::JuMP.GenericModel; kwargs...)
1618
1719
Analyze a JuMP model using the specified analyzer.
1820
Depending on the analyzer, this keyword arguments might vary.
@@ -25,10 +27,12 @@ See [`summarize`](@ref), [`list_of_issues`](@ref), and
2527
function analyze end
2628

2729
"""
28-
summarize([io::IO,] AbstractData; verbose = true, max_issues = 10, kwargs...)
30+
summarize([io::IO,] AbstractData; model = nothing, verbose = true, max_issues = 10, kwargs...)
2931
3032
Print a summary of the analysis results contained in `AbstractData` to the
3133
specified IO stream. If no IO stream is provided, it defaults to `stdout`.
34+
The model that led to the issue can be provided to `model`, it will be
35+
used to generate the name of variables and constraints in the issue summary.
3236
The `verbose` flag controls whether to print detailed information about each
3337
issue (if `true`) or a concise summary (if `false`). The `max_issues` argument
3438
controls the maximum number of issues to display in the summary. If there are
@@ -41,9 +45,11 @@ be a subtype of `AbstractIssue`). In the verbose case it will provide a text
4145
explaning the issue. In the non-verbose case it will provide just the issue
4246
name.
4347
44-
summarize([io::IO,] issue::AbstractIssue; verbose = true)
48+
summarize([io::IO,] issue::AbstractIssue; model = nothing, verbose = true)
4549
4650
This variant allows summarizing a single issue instance of type `AbstractIssue`.
51+
The model that led to the issue can be provided to `model`, it will be
52+
used to generate the name of variables and constraints in the issue summary.
4753
"""
4854
function summarize end
4955

@@ -76,11 +82,16 @@ function summarize(::Type{T}; kwargs...) where {T<:AbstractIssue}
7682
return summarize(stdout, T; kwargs...)
7783
end
7884

79-
function summarize(io::IO, issue::AbstractIssue; verbose = true)
85+
function summarize(
86+
io::IO,
87+
issue::AbstractIssue;
88+
model = nothing,
89+
verbose = true,
90+
)
8091
if verbose
81-
return _verbose_summarize(io, issue)
92+
return _verbose_summarize(io, issue, model)
8293
else
83-
return _summarize(io, issue)
94+
return _summarize(io, issue, model)
8495
end
8596
end
8697

@@ -93,6 +104,7 @@ const DEFAULT_MAX_ISSUES = 10
93104
function summarize(
94105
io::IO,
95106
issues::Vector{T};
107+
model = nothing,
96108
verbose = true,
97109
max_issues = DEFAULT_MAX_ISSUES,
98110
) where {T<:AbstractIssue}
@@ -110,7 +122,7 @@ function summarize(
110122
end
111123
for issue in first(issues, max_issues)
112124
print(io, " * ")
113-
summarize(io, issue, verbose = verbose)
125+
summarize(io, issue, verbose = verbose, model = model)
114126
print(io, "\n")
115127
end
116128
return
@@ -124,11 +136,91 @@ function summarize(data::AbstractData; kwargs...)
124136
return summarize(stdout, data; kwargs...)
125137
end
126138

139+
"""
140+
value(issue::AbstractIssue)
141+
142+
Return the value associated to a particular issue. The value is a number
143+
with a different meaning depending on the type of issue. For example, for
144+
some numerical issues, it can be the coefficient value.
145+
"""
146+
function value end
147+
148+
"""
149+
values(issue::AbstractIssue)
150+
151+
Return the values associated to a particular issue.
152+
"""
153+
function values end
154+
155+
"""
156+
variable(issue::AbstractIssue)
157+
158+
Return the variable associated to a particular issue.
159+
"""
160+
function variable(issue::AbstractIssue, ::MOI.ModelLike)
161+
return variable(issue)
162+
end
163+
164+
"""
165+
variables(issue::AbstractIssue)
166+
167+
Return the variables associated to a particular issue.
168+
"""
169+
function variables(issue::AbstractIssue, ::MOI.ModelLike)
170+
return variables(issue)
171+
end
172+
173+
"""
174+
constraint(issue::AbstractIssue)
175+
176+
Return the constraint associated to a particular issue.
177+
"""
178+
function constraint(issue::AbstractIssue, ::MOI.ModelLike)
179+
return constraint(issue)
180+
end
181+
182+
"""
183+
constraints(issue::AbstractIssue)
184+
185+
Return the constraints associated to a particular issue.
186+
"""
187+
function constraints(issue::AbstractIssue, ::MOI.ModelLike)
188+
return constraints(issue)
189+
end
190+
191+
"""
192+
set(issue::AbstractIssue)
193+
194+
Return the set associated to a particular issue.
195+
"""
196+
function set end
197+
127198
function _verbose_summarize end
199+
128200
function _summarize end
129201

202+
function _name(ref::MOI.VariableIndex, model::MOI.ModelLike)
203+
name = MOI.get(model, MOI.VariableName(), ref)
204+
if !isempty(name)
205+
return name
206+
end
207+
return "$ref"
208+
end
209+
210+
function _name(ref::MOI.ConstraintIndex, model::MOI.ModelLike)
211+
name = MOI.get(model, MOI.ConstraintName(), ref)
212+
if !isempty(name)
213+
return name
214+
end
215+
return "$ref"
216+
end
217+
218+
function _name(ref, ::Nothing)
219+
return "$ref"
220+
end
221+
130222
include("numerical.jl")
131223
include("feasibility.jl")
132224
include("infeasibility.jl")
133225

134-
end
226+
end # module ModelAnalyzer

src/_eval_variables.jl

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Copyright (c) 2025: Joaquim Garcia, Oscar Dowson and contributors
2+
#
3+
# Use of this source code is governed by an MIT-style license that can be found
4+
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.
5+
6+
function _eval_variables end
7+
8+
function _eval_variables(value_fn::Function, t::MOI.ScalarAffineTerm)
9+
return t.coefficient * value_fn(t.variable)
10+
end
11+
12+
function _eval_variables(
13+
value_fn::Function,
14+
f::MOI.ScalarAffineFunction{T},
15+
) where {T}
16+
# TODO: this conversion exists in JuMP, but not in MOI
17+
S = Base.promote_op(value_fn, MOI.VariableIndex)
18+
U = MOI.MA.promote_operation(*, T, S)
19+
out = convert(U, f.constant)
20+
for t in f.terms
21+
out += _eval_variables(value_fn, t)
22+
end
23+
return out
24+
end

0 commit comments

Comments
 (0)