From 692f15fa734bdb4b83b1598556e75a99615d9928 Mon Sep 17 00:00:00 2001 From: odow Date: Wed, 28 May 2025 16:48:03 +1200 Subject: [PATCH] Refactor filenames and move some code around --- .../ModelAnalyzerJuMPExt.jl | 6 +- src/ModelAnalyzer.jl | 6 +- src/_eval_variables.jl | 24 ---- .../Feasibility.jl} | 8 +- .../Infeasibility.jl} | 64 ++++++++-- src/{numerical.jl => analyzers/Numerical.jl} | 4 +- src/intervals.jl | 112 ------------------ test/runtests.jl | 9 +- test/{feasibility.jl => test_Feasibility.jl} | 10 +- ...{infeasibility.jl => test_Ineasibility.jl} | 6 +- test/{numerical.jl => test_Numerical.jl} | 2 +- 11 files changed, 80 insertions(+), 171 deletions(-) delete mode 100644 src/_eval_variables.jl rename src/{feasibility.jl => analyzers/Feasibility.jl} (99%) rename src/{infeasibility.jl => analyzers/Infeasibility.jl} (93%) rename src/{numerical.jl => analyzers/Numerical.jl} (100%) delete mode 100644 src/intervals.jl rename test/{feasibility.jl => test_Feasibility.jl} (99%) rename test/{infeasibility.jl => test_Ineasibility.jl} (99%) rename test/{numerical.jl => test_Numerical.jl} (99%) diff --git a/ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl b/ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl index 25eafae..18d84c9 100644 --- a/ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl +++ b/ext/ModelAnalyzerJuMPExt/ModelAnalyzerJuMPExt.jl @@ -5,15 +5,15 @@ module ModelAnalyzerJuMPExt -import ModelAnalyzer import JuMP import MathOptInterface as MOI +import ModelAnalyzer function ModelAnalyzer.analyze( - analyzer::T, + analyzer::ModelAnalyzer.AbstractAnalyzer, model::JuMP.GenericModel; kwargs..., -) where {T<:ModelAnalyzer.AbstractAnalyzer} +) moi_model = JuMP.backend(model) result = ModelAnalyzer.analyze(analyzer, moi_model; kwargs...) return result diff --git a/src/ModelAnalyzer.jl b/src/ModelAnalyzer.jl index c913b18..9888cf6 100644 --- a/src/ModelAnalyzer.jl +++ b/src/ModelAnalyzer.jl @@ -219,8 +219,8 @@ function _name(ref, ::Nothing) return "$ref" end -include("numerical.jl") -include("feasibility.jl") -include("infeasibility.jl") +include("analyzers/Numerical.jl") +include("analyzers/Feasibility.jl") +include("analyzers/Infeasibility.jl") end # module ModelAnalyzer diff --git a/src/_eval_variables.jl b/src/_eval_variables.jl deleted file mode 100644 index de0bfab..0000000 --- a/src/_eval_variables.jl +++ /dev/null @@ -1,24 +0,0 @@ -# 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 diff --git a/src/feasibility.jl b/src/analyzers/Feasibility.jl similarity index 99% rename from src/feasibility.jl rename to src/analyzers/Feasibility.jl index 654f22f..6c04c83 100644 --- a/src/feasibility.jl +++ b/src/analyzers/Feasibility.jl @@ -5,9 +5,9 @@ module Feasibility -import ModelAnalyzer import Dualization import MathOptInterface as MOI +import ModelAnalyzer import Printf """ @@ -103,7 +103,7 @@ ModelAnalyzer.value(issue::DualConstraintViolation) = issue.violation """ DualConstrainedVariableViolation <: AbstractFeasibilityIssue -The `DualConstrainedVariableViolation` issue is identified when a dual +The `DualConstrainedVariableViolation` issue is identified when a dual constraint, which is a constrained varaible constraint, has a value that is not within the dual constraint's set. During the dualization process, each primal constraint is mapped to a dual @@ -301,7 +301,7 @@ function ModelAnalyzer._verbose_summarize(io::IO, ::Type{PrimalViolation}) ## What - A `PrimalViolation` issue is identified when a constraint has + A `PrimalViolation` issue is identified when a constraint has function , i.e., a left-hand-side value, that is not within the constraint's set. @@ -1025,7 +1025,7 @@ function _dual_point_to_dual_model_ref( # if !(primal_con isa MOI.ConstraintIndex{MOI.VariableIndex,<:MOI.EqualTo} || # primal_con isa MOI.ConstraintIndex{MOI.VectorOfVariables,MOI.Zeros} # SAF in EQ, etc... - #) + #) # error("Problem with dualization, see: $primal_con") # end end diff --git a/src/infeasibility.jl b/src/analyzers/Infeasibility.jl similarity index 93% rename from src/infeasibility.jl rename to src/analyzers/Infeasibility.jl index 2dab25d..5ee7dbd 100644 --- a/src/infeasibility.jl +++ b/src/analyzers/Infeasibility.jl @@ -5,11 +5,57 @@ module Infeasibility -import ModelAnalyzer import MathOptInterface as MOI +import ModelAnalyzer + +# This type and the associated function were inspired by IntervalArithmetic.jl +# Copyright (c) 2014-2021: David P. Sanders & Luis Benet +struct Interval{T<:Real} + lo::T + hi::T + + function Interval(lo::T, hi::T) where {T<:Real} + @assert lo <= hi + return new{T}(lo, hi) + end +end + +Base.convert(::Type{Interval{T}}, x::T) where {T<:Real} = Interval(x, x) + +Base.zero(::Type{Interval{T}}) where {T<:Real} = Interval(zero(T), zero(T)) + +Base.iszero(a::Interval) = iszero(a.hi) && iszero(a.lo) + +function Base.:+(a::Interval{T}, b::Interval{T}) where {T<:Real} + return Interval(a.lo + b.lo, a.hi + b.hi) +end + +function Base.:*(x::T, a::Interval{T}) where {T<:Real} + if iszero(a) || iszero(x) + return Interval(zero(T), zero(T)) + elseif x >= zero(T) + return Interval(a.lo * x, a.hi * x) + end + return Interval(a.hi * x, a.lo * x) +end + +function _eval_variables(value_fn::Function, t::MOI.ScalarAffineTerm) + return t.coefficient * value_fn(t.variable) +end -include("intervals.jl") -include("_eval_variables.jl") +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 """ Analyzer() <: ModelAnalyzer.AbstractAnalyzer @@ -63,7 +109,7 @@ ModelAnalyzer.values(issue::InfeasibleBounds) = [issue.lb, issue.ub] InfeasibleIntegrality{T} <: AbstractInfeasibilitylIssue The `InfeasibleIntegrality` issue is identified when a variable has an -integrality constraint (like `MOI.Integer` or `MOI.ZeroOne`) that is not +integrality constraint (like `MOI.Integer` or `MOI.ZeroOne`) that is not consistent with its bounds. That is, the bounds do not allow for any integer value to be feasible. @@ -580,7 +626,7 @@ function ModelAnalyzer._verbose_summarize( ## Why This can be a sign of a mistake in the model formulation. This error - will lead to infeasibility in the optimization problem. + will lead to infeasibility in the optimization problem. ## How to fix @@ -611,7 +657,7 @@ function ModelAnalyzer._verbose_summarize( ## Why This can be a sign of a mistake in the model formulation. This error - will lead to infeasibility in the optimization problem. + will lead to infeasibility in the optimization problem. ## How to fix @@ -642,7 +688,7 @@ function ModelAnalyzer._verbose_summarize( ## Why This can be a sign of a mistake in the model formulation. This error - will lead to infeasibility in the optimization problem. + will lead to infeasibility in the optimization problem. ## How to fix @@ -667,12 +713,12 @@ function ModelAnalyzer._verbose_summarize( ## What An `IrreducibleInfeasibleSubset` issue is identified when a subset of constraints - cannot be satisfied simultaneously. + cannot be satisfied simultaneously. ## Why This can be a sign of a mistake in the model formulation. This error - will lead to infeasibility in the optimization problem. + will lead to infeasibility in the optimization problem. ## How to fix diff --git a/src/numerical.jl b/src/analyzers/Numerical.jl similarity index 100% rename from src/numerical.jl rename to src/analyzers/Numerical.jl index 8eb904a..f2a7706 100644 --- a/src/numerical.jl +++ b/src/analyzers/Numerical.jl @@ -5,10 +5,10 @@ module Numerical -import ModelAnalyzer import LinearAlgebra -import Printf import MathOptInterface as MOI +import ModelAnalyzer +import Printf """ Analyzer() <: ModelAnalyzer.AbstractAnalyzer diff --git a/src/intervals.jl b/src/intervals.jl deleted file mode 100644 index f54eb84..0000000 --- a/src/intervals.jl +++ /dev/null @@ -1,112 +0,0 @@ -# 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. - -# This file was originally written by Joaquim Garcia and contributors in -# BilevelJuMP.jl, which is licensed under the MIT "Expat" License: - -# The BilevelJuMP.jl package is licensed under the MIT "Expat" License: - -# Copyright (c) 2019 Joaquim Dias Garcia, and contributors - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -# Function in this file are heavily inspired in IntervalArithmetic.jl, -# which is licensed under the MIT "Expat" License: -# -# Copyright (c) 2014-2021: David P. Sanders & Luis Benet -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -struct Interval{T} - lo::T - hi::T -end - -function Interval(lo::T, hi::T) where {T<:Real} - # if hi < lo <= hi + eps(T) - # lo = hi - # end - @assert lo <= hi - return Interval{T}(lo, hi) -end - -function Base.zero(::Type{Interval{T}}) where {T<:Real} - return Interval(zero(T), zero(T)) -end - -function Base.iszero(a::Interval) - return iszero(a.hi) && iszero(a.lo) -end - -# this code is only used for interval += scalar_coef * interval -# so only bivariate + and * are needes - -# Base.:+(a::Interval) = a -# Base.:-(a::Interval) = Interval(-a.hi, -a.lo) - -# function Base.:+(a::Interval{T}, b::T) where {T<:Real} -# return Interval(a.lo + b, a.hi + b) -# end -# Base.:+(b::T, a::Interval{T}) where {T<:Real} = a + b - -# function Base.:-(a::Interval{T}, b::T) where {T<:Real} -# return Interval(a.lo - b, a.hi - b) -# end -# function Base.:-(b::T, a::Interval{T}) where {T<:Real} -# return Interval(b - a.hi, b - a.lo) -# end - -function Base.:+(a::Interval{T}, b::Interval{T}) where {T<:Real} - return Interval(a.lo + b.lo, a.hi + b.hi) -end - -# function Base.:-(a::Interval{T}, b::Interval{T}) where {T<:Real} -# return Interval(a.lo - b.hi, a.hi - b.lo) -# end - -## Multiplication -function Base.:*(x::T, a::Interval{T}) where {T<:Real} - (iszero(a) || iszero(x)) && return Interval(zero(T), zero(T)) - if x ≥ zero(T) - return Interval(a.lo * x, a.hi * x) - else - return Interval(a.hi * x, a.lo * x) - end -end - -# Base.:*(a::Interval{T}, x::T) where {T<:Real} = x * a - -Base.convert(::Type{Interval{T}}, x::T) where {T<:Real} = Interval(x, x) diff --git a/test/runtests.jl b/test/runtests.jl index 12287cd..3c8b08f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -7,11 +7,10 @@ using Test @testset "ModelAnalyzer" begin for file in readdir(@__DIR__) - if !endswith(file, ".jl") || file in ("runtests.jl",) - continue - end - @testset "$file" begin - include(file) + if startswith(file, "test_") && endswith(file, ".jl") + @testset "$file" begin + include(file) + end end end end diff --git a/test/feasibility.jl b/test/test_Feasibility.jl similarity index 99% rename from test/feasibility.jl rename to test/test_Feasibility.jl index 793e56d..3948fab 100644 --- a/test/feasibility.jl +++ b/test/test_Feasibility.jl @@ -3,7 +3,7 @@ # 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 TestDualFeasibilityChecker +module TestFeasibility using ModelAnalyzer using Test @@ -30,7 +30,7 @@ function test_no_solution() model, ) # no dual solutions available - # @test_throws ErrorException + # @test_throws ErrorException data = ModelAnalyzer.analyze( ModelAnalyzer.Feasibility.Analyzer(), model, @@ -195,7 +195,7 @@ function test_no_lb() # the dual is: # Max 0 # Subject to - # y == 1 (as a constraint) + # y == 1 (as a constraint) # y >= 0 (as a bound) # mayber force fail here # @test_throws ErrorException @@ -1066,6 +1066,6 @@ function test_nl_obj() return end -end # module +end # module TestFeasibility -TestDualFeasibilityChecker.runtests() +TestFeasibility.runtests() diff --git a/test/infeasibility.jl b/test/test_Ineasibility.jl similarity index 99% rename from test/infeasibility.jl rename to test/test_Ineasibility.jl index cde2582..425f463 100644 --- a/test/infeasibility.jl +++ b/test/test_Ineasibility.jl @@ -3,7 +3,7 @@ # 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 TestInfeasibilityChecker +module TestInfeasibility import ModelAnalyzer using Test @@ -537,6 +537,6 @@ function test_iis_spare() return end -end # module +end # module TestInfeasibility -TestInfeasibilityChecker.runtests() +TestInfeasibility.runtests() diff --git a/test/numerical.jl b/test/test_Numerical.jl similarity index 99% rename from test/numerical.jl rename to test/test_Numerical.jl index 3d5d6df..5ad2e2f 100644 --- a/test/numerical.jl +++ b/test/test_Numerical.jl @@ -1320,6 +1320,6 @@ function test_more_than_max_issues() return end -end # module +end # module TestNumerical TestNumerical.runtests()