From f6d6ea827a2837b1b20e26f39dbdb4f2d37aaef6 Mon Sep 17 00:00:00 2001 From: joaquimg Date: Thu, 24 Jul 2025 18:22:28 -0300 Subject: [PATCH 1/2] [WIP] dynamic ranges --- src/Numerical/analyze.jl | 161 +++++++++++++++++++++++++++--- src/Numerical/structs.jl | 199 ++++++++++++++++++++++++++++++++++++- src/Numerical/summarize.jl | 195 ++++++++++++++++++++++++++++++++++++ 3 files changed, 537 insertions(+), 18 deletions(-) diff --git a/src/Numerical/analyze.jl b/src/Numerical/analyze.jl index d7f1545..0599ecc 100644 --- a/src/Numerical/analyze.jl +++ b/src/Numerical/analyze.jl @@ -17,17 +17,21 @@ function MathOptAnalyzer.analyze( threshold_dense_entries::Int = 1000, threshold_small::Float64 = 1e-5, threshold_large::Float64 = 1e+5, + threshold_dynamic_range_single::Float64 = 1e+6, + threshold_dynamic_range_matrix::Float64 = 1e+8, ) data = Data() data.threshold_dense_fill_in = threshold_dense_fill_in data.threshold_dense_entries = threshold_dense_entries data.threshold_small = threshold_small data.threshold_large = threshold_large + data.threshold_dynamic_range_single = threshold_dynamic_range_single + data.threshold_dynamic_range_matrix = threshold_dynamic_range_matrix # initialize simples data data.sense = MOI.get(model, MOI.ObjectiveSense()) data.number_of_variables = MOI.get(model, MOI.NumberOfVariables()) - sizehint!(data.variables_in_constraints, data.number_of_variables) + sizehint!(data._variables_in_constraints, data.number_of_variables) # objective pass objective_type = MOI.get(model, MOI.ObjectiveFunctionType()) @@ -56,13 +60,15 @@ function MathOptAnalyzer.analyze( # variable index constraints are not counted in the constraints pass list_of_variables = MOI.get(model, MOI.ListOfVariableIndices()) for var in list_of_variables - if !(var in data.variables_in_constraints) + if !(var in data._variables_in_constraints) push!( data.variables_not_in_constraints, VariableNotInConstraints(var), ) end end + # TODO + # compute ranges and dynamic ranges for variables and matrix sort!(data.dense_rows, by = x -> x.nnz, rev = true) sort!(data.matrix_small, by = x -> abs(x.coefficient)) sort!(data.matrix_large, by = x -> abs(x.coefficient), rev = true) @@ -96,13 +102,14 @@ end function _get_objective_data(data, func::MOI.ScalarAffineFunction) nnz = 0 + _reset_function_range(data) for term in func.terms variable = term.variable coefficient = term.coefficient if iszero(coefficient) continue end - nnz += _update_range(data.objective_range, coefficient) + nnz += _update_function_range(data, coefficient, variable) if abs(coefficient) < data.threshold_small push!( data.objective_small, @@ -115,9 +122,87 @@ function _get_objective_data(data, func::MOI.ScalarAffineFunction) ) end end + range = _function_range(data) + if range > data.threshold_dynamic_range_single + push!( + data.large_dynamic_range_objective, + LargeDynamicRangeObjective( + data._small_coefficient[1][2], + data._large_coefficient[1][2], + range, + ), + ) + end + _reset_function_range(data) + return +end + +function _reset_function_range(data::Data) + empty!(data._large_coefficient) + empty!(data._small_coefficient) + sizehint!(data._large_coefficient, 1) + sizehint!(data._small_coefficient, 1) return end +function _function_range(data::Data) + if isempty(data._large_coefficient) || isempty(data._small_coefficient) + return 0.0 + end + large = data._large_coefficient[1][1] + small = data._small_coefficient[1][1] + return large / small +end + +function _update_function_range(data::Data, value, variable) + if iszero(value) + return 0 + end + value = abs(value) + if isempty(data._large_coefficient) + push!(data._large_coefficient, (value, variable)) + elseif value > data._large_coefficient[1][1] + data._large_coefficient[1] = (value, variable) + end + if isempty(data._small_coefficient) + push!(data._small_coefficient, (value, variable)) + elseif value < data._small_coefficient[1][1] + data._small_coefficient[1] = (value, variable) + end + return 1 +end + +function _update_constraint_range(data::Data, value, variable, ref) + if iszero(value) + return 0 + end + _update_function_range(data, value, variable) + value = abs(value) + # + if isempty(data._large_matrix_coefficient) + push!(data._large_matrix_coefficient, (value, ref, variable)) + elseif value > data._large_matrix_coefficient[1][1] + data._large_matrix_coefficient[1] = (value, ref, variable) + end + if isempty(data._small_matrix_coefficient) + push!(data._small_matrix_coefficient, (value, ref, variable)) + elseif value < data._small_matrix_coefficient[1][1] + data._small_matrix_coefficient[1] = (value, ref, variable) + end + # + if !haskey(data._large_variable_coefficient, variable) + data._large_variable_coefficient[variable] = (value, ref) + elseif value > data._large_matrix_coefficient[variable][1] + data._large_variable_coefficient[variable] = (value, ref) + end + if !haskey(data._small_variable_coefficient, variable) + data._small_variable_coefficient[variable] = (value, ref) + elseif value < data._small_variable_coefficient[variable][1] + data._small_variable_coefficient[variable] = (value, ref) + end + return 1 +end + function _get_objective_data( data, func::MOI.ScalarQuadraticFunction{T}, @@ -231,13 +316,15 @@ function _get_constraint_matrix_data( end end nnz = 0 + _reset_function_range(data) for term in func.terms variable = term.variable coefficient = term.coefficient if iszero(coefficient) continue end - nnz += _update_range(data.matrix_range, coefficient) + # nnz += _update_range(data.matrix_range, coefficient) + nnz += _update_constraint_range(data, coefficient, variable, ref) if abs(coefficient) < data.threshold_small push!( data.matrix_small, @@ -249,7 +336,7 @@ function _get_constraint_matrix_data( LargeMatrixCoefficient(ref, variable, coefficient), ) end - push!(data.variables_in_constraints, variable) + push!(data._variables_in_constraints, variable) end if nnz == 0 if !ignore_extras @@ -261,6 +348,19 @@ function _get_constraint_matrix_data( nnz > data.threshold_dense_entries push!(data.dense_rows, DenseConstraint(ref, nnz)) end + range = _function_range(data) + if range > data.threshold_dynamic_range_single + push!( + data.large_dynamic_range_constraints, + LargeDynamicRangeConstraint( + ref, + data._small_coefficient[1][2], + data._large_coefficient[1][2], + range, + ), + ) + end + _reset_function_range(data) data.matrix_nnz += nnz return end @@ -290,8 +390,8 @@ function _get_constraint_matrix_data( LargeMatrixQuadraticCoefficient(ref, v1, v2, coefficient), ) end - push!(data.variables_in_constraints, v1) - push!(data.variables_in_constraints, v2) + push!(data._variables_in_constraints, v1) + push!(data._variables_in_constraints, v2) end data.has_quadratic_constraints = true _get_constraint_matrix_data( @@ -307,7 +407,10 @@ function _get_constraint_matrix_data( data, ref::MOI.ConstraintIndex, func::MOI.VectorAffineFunction{T}, + ignore_extras::Bool = false, ) where {T} + nnz = 0 + _reset_function_range(data) for term in func.terms variable = term.scalar_term.variable coefficient = term.scalar_term.coefficient @@ -315,7 +418,8 @@ function _get_constraint_matrix_data( if iszero(coefficient) continue end - _update_range(data.matrix_range, coefficient) + # _update_range(data.matrix_range, coefficient) + nnz += _update_constraint_range(data, coefficient, variable, ref) if abs(coefficient) < data.threshold_small push!( data.matrix_small, @@ -327,8 +431,34 @@ function _get_constraint_matrix_data( LargeMatrixCoefficient(ref, variable, coefficient), ) end - push!(data.variables_in_constraints, variable) + push!(data._variables_in_constraints, variable) + end + if nnz == 0 + if !ignore_extras + push!(data.empty_rows, EmptyConstraint(ref)) + end + return + end + # this computation for vector constraint can be more complicated + # as this might need to be per index + # if nnz / data.number_of_variables > data.threshold_dense_fill_in && + # nnz > data.threshold_dense_entries + # push!(data.dense_rows, DenseConstraint(ref, nnz)) + # end + range = _function_range(data) + if range > data.threshold_dynamic_range_single + push!( + data.large_dynamic_range_constraints, + LargeDynamicRangeConstraint( + ref, + data._small_coefficient[1][2], + data._large_coefficient[1][2], + range, + ), + ) end + _reset_function_range(data) + data.matrix_nnz += nnz return end @@ -337,6 +467,7 @@ function _get_constraint_matrix_data( ref::MOI.ConstraintIndex, func::MOI.VectorQuadraticFunction{T}, ) where {T} + nnz = 0 for term in func.quadratic_terms v1 = term.scalar_term.variable_1 v2 = term.scalar_term.variable_2 @@ -344,7 +475,7 @@ function _get_constraint_matrix_data( if iszero(coefficient) continue end - _update_range(data.matrix_quadratic_range, coefficient) + nnz += _update_range(data.matrix_quadratic_range, coefficient) if abs(coefficient) < data.threshold_small push!( data.matrix_quadratic_small, @@ -356,14 +487,14 @@ function _get_constraint_matrix_data( LargeMatrixQuadraticCoefficient(ref, v1, v2, coefficient), ) end - push!(data.variables_in_constraints, v1) - push!(data.variables_in_constraints, v2) + push!(data._variables_in_constraints, v1) + push!(data._variables_in_constraints, v2) end _get_constraint_matrix_data( data, ref, MOI.VectorAffineFunction{T}(func.affine_terms, func.constants), - # ignore_extras = nnz > 0, + ignore_extras = nnz > 0, ) return end @@ -373,7 +504,7 @@ function _get_constraint_matrix_data( ref::MOI.ConstraintIndex, func::MOI.VariableIndex, ) - # push!(data.variables_in_constraints, func) + # push!(data._variables_in_constraints, func) return end @@ -386,7 +517,7 @@ function _get_constraint_matrix_data( return end for var in func.variables - push!(data.variables_in_constraints, var) + push!(data._variables_in_constraints, var) end return end diff --git a/src/Numerical/structs.jl b/src/Numerical/structs.jl index 463465d..38ceb48 100644 --- a/src/Numerical/structs.jl +++ b/src/Numerical/structs.jl @@ -19,6 +19,8 @@ julia> data = MathOptAnalyzer.analyze( threshold_dense_entries = 1000, threshold_small = 1e-5, threshold_large = 1e+5, + threshold_dynamic_range_single = 1e+6, + threshold_dynamic_range_matrix = 1e+8, ) ``` @@ -29,7 +31,11 @@ The additional parameters: constraint to be considered dense. - `threshold_small`: The threshold for small coefficients in the model. - `threshold_large`: The threshold for large coefficients in the model. - +- `threshold_dynamic_range_single`: The threshold for the range of coefficients + in the model with respect with individual columns, rows, objective + coefficients and rhs coefficients. +- `threshold_dynamic_range_matrix`: The threshold for the range of coefficients + in the model with respect to the matrix of coefficients. """ struct Analyzer <: MathOptAnalyzer.AbstractAnalyzer end @@ -428,8 +434,172 @@ julia> MathOptAnalyzer.summarize( struct NonconvexQuadraticConstraint <: AbstractNumericalIssue ref::MOI.ConstraintIndex end + MathOptAnalyzer.constraint(issue::NonconvexQuadraticConstraint) = issue.ref +""" + LargeDynamicRangeConstraint <: AbstractNumericalIssue + +The `LargeDynamicRangeConstraint` issue is identified when the dynamic range of +a constraint is larger than `threshold_dynamic_range_single`. The dynamic range +is defined as the ratio between the largest and smallest coefficients of the +constraint. + +For more information, run: +```julia +julia> MathOptAnalyzer.summarize( + MathOptAnalyzer.Numerical.LargeDynamicRangeConstraint +) +``` +""" +struct LargeDynamicRangeConstraint <: AbstractNumericalIssue + ref::MOI.ConstraintIndex + variable1::MOI.VariableIndex + variable2::MOI.VariableIndex + range::Float64 +end + +function MathOptAnalyzer.constraint(issue::LargeDynamicRangeConstraint) + return issue.ref +end + +function MathOptAnalyzer.variables(issue::LargeDynamicRangeConstraint) + return [issue.variable1, issue.variable2] +end + +function MathOptAnalyzer.value(issue::LargeDynamicRangeConstraint) + return issue.range +end + + +""" + LargeDynamicRangeMatrix <: AbstractNumericalIssue + +The `LargeDynamicRangeMatrix` issue is identified when the dynamic range of the +matrix is larger than `threshold_dynamic_range_matrix`. The dynamic range is +defined as the ratio between the largest and smallest coefficients of the +matrix. + +For more information, run: +```julia +julia> MathOptAnalyzer.summarize( + MathOptAnalyzer.Numerical.LargeDynamicRangeMatrix +) +``` +""" +struct LargeDynamicRangeMatrix <: AbstractNumericalIssue + constraint1::MOI.ConstraintIndex + constraint2::MOI.ConstraintIndex + variable1::MOI.VariableIndex + variable2::MOI.VariableIndex + range::Float64 +end + +function MathOptAnalyzer.constraints(issue::LargeDynamicRangeMatrix) + return [issue.constraint1, issue.constraint2] +end + +function MathOptAnalyzer.variables(issue::LargeDynamicRangeMatrix) + return [issue.variable1, issue.variable2] +end + +function MathOptAnalyzer.value(issue::LargeDynamicRangeMatrix) + return issue.range +end + +""" + LargeDynamicRangeObjective <: AbstractNumericalIssue + +The `LargeDynamicRangeObjective` issue is identified when the dynamic range of +the objective function is larger than `threshold_dynamic_range_single`. The +dynamic range is defined as the ratio between the largest and smallest +coefficients of the objective function. + +For more information, run: +```julia +julia> MathOptAnalyzer.summarize( + MathOptAnalyzer.Numerical.LargeDynamicRangeObjective +) +``` +""" +struct LargeDynamicRangeObjective <: AbstractNumericalIssue + variable1::MOI.VariableIndex + variable2::MOI.VariableIndex + range::Float64 +end + +function MathOptAnalyzer.variables(issue::LargeDynamicRangeObjective) + return [issue.variable1, issue.variable2] +end + +function MathOptAnalyzer.value(issue::LargeDynamicRangeObjective) + return issue.range +end + +# """ +# LargeDynamicRangeRHS <: AbstractNumericalIssue + +# The `LargeDynamicRangeRHS` issue is identified when the dynamic range of the +# right-hand side (RHS) vector is larger than `threshold_dynamic_range_rhs`. The +# dynamic range is defined as the ratio between the largest and smallest +# coefficients of the RHS vector. + +# For more information, run: +# ```julia +# julia> MathOptAnalyzer.summarize( +# MathOptAnalyzer.Numerical.LargeDynamicRangeRHS +# ) +# ``` +# """ +# struct LargeDynamicRangeRHS <: AbstractNumericalIssue +# constraint1::MOI.ConstraintIndex +# constraint2::MOI.ConstraintIndex +# range::Float64 +# end + +# function MathOptAnalyzer.constraints(issue::LargeDynamicRangeRHS) +# return [issue.constraint1, issue.constraint2] +# end + +# function MathOptAnalyzer.value(issue::LargeDynamicRangeRHS) +# return issue.range +# end + + +""" + LargeDynamicRangeVariable <: AbstractNumericalIssue + +The `LargeDynamicRangeVariable` issue is identified when the dynamic range of a +variable is larger than `threshold_dynamic_range_single`. The dynamic range is +defined as the ratio between the largest and smallest coefficients of the +variable inall constraint it appears. + +For more information, run: +```julia +julia> MathOptAnalyzer.summarize( + MathOptAnalyzer.Numerical.LargeDynamicRangeVariable +) +``` +""" +struct LargeDynamicRangeVariable <: AbstractNumericalIssue + variable::MOI.VariableIndex + constraint1::MOI.ConstraintIndex + constraint2::MOI.ConstraintIndex + range::Float64 +end + +function MathOptAnalyzer.variable(issue::LargeDynamicRangeVariable) + return issue.variable +end + +function MathOptAnalyzer.constraints(issue::LargeDynamicRangeVariable) + return [issue.constraint1, issue.constraint2] +end + +function MathOptAnalyzer.value(issue::LargeDynamicRangeVariable) + return issue.range +end + """ Data @@ -443,6 +613,8 @@ Base.@kwdef mutable struct Data <: MathOptAnalyzer.AbstractData threshold_dense_entries::Int = 1000 threshold_small::Float64 = 1e-5 threshold_large::Float64 = 1e+5 + threshold_dynamic_range_single::Float64 = 1e+6 + threshold_dynamic_range_matrix::Float64 = 1e+8 # main numbers number_of_variables::Int = 0 number_of_constraints::Int = 0 @@ -450,13 +622,25 @@ Base.@kwdef mutable struct Data <: MathOptAnalyzer.AbstractData Tuple{DataType,DataType,Int}[] # objective_info::Any matrix_nnz::Int = 0 - # ranges + # ranges # TODO remove after dyn range rewrite matrix_range::Vector{Float64} = sizehint!(Float64[1.0, 1.0], 2) bounds_range::Vector{Float64} = sizehint!(Float64[1.0, 1.0], 2) rhs_range::Vector{Float64} = sizehint!(Float64[1.0, 1.0], 2) objective_range::Vector{Float64} = sizehint!(Float64[1.0, 1.0], 2) # cache data - variables_in_constraints::Set{MOI.VariableIndex} = Set{MOI.VariableIndex}() + _variables_in_constraints::Set{MOI.VariableIndex} = Set{MOI.VariableIndex}() + _large_matrix_coefficient::Vector{Tuple{Float64, MOI.ConstraintIndex, MOI.VariableIndex}} = + sizehint!(Tuple{Float64, MOI.ConstraintIndex, MOI.VariableIndex}[], 1) + _small_matrix_coefficient::Vector{Tuple{Float64, MOI.ConstraintIndex, MOI.VariableIndex}} = + sizehint!(Tuple{Float64, MOI.ConstraintIndex, MOI.VariableIndex}[], 1) + _large_variable_coefficient::Dict{MOI.VariableIndex, Tuple{Float64, MOI.ConstraintIndex}} = + Dict{MOI.VariableIndex, Tuple{Float64, MOI.ConstraintIndex}}() + _small_variable_coefficient::Dict{MOI.VariableIndex, Tuple{Float64, MOI.ConstraintIndex}} = + Dict{MOI.VariableIndex, Tuple{Float64, MOI.ConstraintIndex}}() + _large_coefficient::Vector{Tuple{Float64, MOI.VariableIndex}} = + sizehint!(Tuple{Float64, MOI.VariableIndex}[], 1) + _small_coefficient::Vector{Tuple{Float64, MOI.VariableIndex}} = + sizehint!(Tuple{Float64, MOI.VariableIndex}[], 1) # variables analysis variables_not_in_constraints::Vector{VariableNotInConstraints} = VariableNotInConstraints[] @@ -470,6 +654,15 @@ Base.@kwdef mutable struct Data <: MathOptAnalyzer.AbstractData matrix_large::Vector{LargeMatrixCoefficient} = LargeMatrixCoefficient[] rhs_small::Vector{SmallRHSCoefficient} = SmallRHSCoefficient[] rhs_large::Vector{LargeRHSCoefficient} = LargeRHSCoefficient[] + # dynamic range analysis + large_dynamic_range_constraints::Vector{LargeDynamicRangeConstraint} = + LargeDynamicRangeConstraint[] + large_matrix_dynamic_range::Vector{LargeDynamicRangeMatrix} = + LargeDynamicRangeMatrix[] + large_dynamic_range_objective::Vector{LargeDynamicRangeObjective} = + LargeDynamicRangeObjective[] + # large_dynamic_range_rhs::Vector{LargeDynamicRangeRHS} = + # LargeDynamicRangeRHS[] # quadratic constraints analysis has_quadratic_constraints::Bool = false nonconvex_rows::Vector{NonconvexQuadraticConstraint} = diff --git a/src/Numerical/summarize.jl b/src/Numerical/summarize.jl index 5c971a8..3d4a0dc 100644 --- a/src/Numerical/summarize.jl +++ b/src/Numerical/summarize.jl @@ -90,6 +90,27 @@ function MathOptAnalyzer._summarize( return print(io, "# NonconvexQuadraticConstraint") end +function MathOptAnalyzer._summarize( + io::IO, + ::Type{LargeDynamicRangeConstraint}, +) + return print(io, "# LargeDynamicRangeConstraint") +end + +function MathOptAnalyzer._summarize( + io::IO, + ::Type{LargeDynamicRangeMatrix}, +) + return print(io, "# LargeDynamicRangeMatrix") +end + +function MathOptAnalyzer._summarize( + io::IO, + ::Type{LargeDynamicRangeObjective}, +) + return print(io, "# LargeDynamicRangeObjective") +end + function MathOptAnalyzer._verbose_summarize( io::IO, ::Type{VariableNotInConstraints}, @@ -676,6 +697,143 @@ function MathOptAnalyzer._verbose_summarize( ) end +function MathOptAnalyzer._verbose_summarize( + io::IO, + ::Type{LargeDynamicRangeConstraint}, +) + return print( + io, + """ + # `LargeDynamicRangeConstraint` + + ## What + + A `LargeDynamicRangeConstraint` issue is identified when a constraint + has a large dynamic range, that is, the ratio between the largest and + smallest coefficient is large. + + ## Why + + Large dynamic ranges can lead to numerical instability in the solution + process. + + ## How to fix + + Check if the constraint is correct. Check if the units of variables and + coefficients are correct. Check if the number makes is + reasonable given that solver have tolerances. Sometimes these + coefficients can be replaced by zeros. + + ## More information + + No extra information for this issue. + """, + ) +end + +function MathOptAnalyzer._verbose_summarize( + io::IO, + ::Type{LargeDynamicRangeMatrix}, +) + return print( + io, + """ + # `LargeDynamicRangeMatrix` + + ## What + + A `LargeDynamicRangeMatrix` issue is identified when a matrix has a + large dynamic range, that is, the ratio between the largest and smallest + coefficient is large. + + ## Why + + Large dynamic ranges can lead to numerical instability in the solution + process. + + ## How to fix + + Check if the matrix is correct. Check if the units of variables and + coefficients are correct. Check if the number makes is + reasonable given that solver have tolerances. Sometimes these + coefficients can be replaced by zeros. + + ## More information + + No extra information for this issue. + """, + ) +end + +function MathOptAnalyzer._verbose_summarize( + io::IO, + ::Type{LargeDynamicRangeObjective}, +) + return print( + io, + """ + # `LargeDynamicRangeObjective` + + ## What + + A `LargeDynamicRangeObjective` issue is identified when an objective + function has a large dynamic range, that is, the ratio between the + largest and smallest coefficient is large. + + ## Why + + Large dynamic ranges can lead to numerical instability in the solution + process. + + ## How to fix + + Check if the objective is correct. Check if the units of variables and + coefficients are correct. Check if the number makes is + reasonable given that solver have tolerances. Sometimes these + coefficients can be replaced by zeros. + + ## More information + + No extra information for this issue. + """, + ) +end + +function MathOptAnalyzer._verbose_summarize( + io::IO, + ::Type{LargeDynamicRangeVariable}, +) + return print( + io, + """ + # `LargeDynamicRangeVariable` + + ## What + + A `LargeDynamicRangeVariable` issue is identified when the dynamic range of a + variable is larger than `threshold_dynamic_range_single`. The dynamic range is + defined as the ratio between the largest and smallest coefficients of the + variable in all constraints it appears. + + ## Why + + Large dynamic ranges can lead to numerical instability in the solution + process. + + ## How to fix + + Check if the variable is correct. Check if the units of variables and + coefficients are correct. Check if the number makes is + reasonable given that solver have tolerances. Sometimes these + coefficients can be replaced by zeros. + + ## More information + + No extra information for this issue. + """, + ) +end + function MathOptAnalyzer._summarize( io::IO, issue::VariableNotInConstraints, @@ -872,6 +1030,43 @@ function MathOptAnalyzer._summarize( return print(io, MathOptAnalyzer._name(issue.ref, model)) end +function MathOptAnalyzer._summarize( + io::IO, + issue::LargeDynamicRangeConstraint, + model, +) + return print(io, MathOptAnalyzer._name(issue.ref, model), " : ", issue.range) +end + +function MathOptAnalyzer._summarize( + io::IO, + issue::LargeDynamicRangeMatrix, + model, +) + return print(io, "Matrix has a Large dynamic range : ", issue.range) +end + +function MathOptAnalyzer._summarize( + io::IO, + issue::LargeDynamicRangeObjective, + model, +) + return print(io, "Objective has a Large dynamic range : ", issue.range) +end + +function MathOptAnalyzer._summarize( + io::IO, + issue::LargeDynamicRangeVariable, + model, +) + return print( + io, + MathOptAnalyzer._name(issue.variable, model), + " : ", + issue.range, + ) +end + function MathOptAnalyzer._verbose_summarize( io::IO, issue::VariableNotInConstraints, From 354af48e7a641b0f13c63c7d9fd301d02586065f Mon Sep 17 00:00:00 2001 From: joaquimg Date: Sat, 26 Jul 2025 19:50:37 +0200 Subject: [PATCH 2/2] Wrap up dynamic ranges --- src/Numerical/analyze.jl | 250 ++++++++++++++++++++------ src/Numerical/structs.jl | 194 ++++++++++++-------- src/Numerical/summarize.jl | 352 +++++++++++++++++++++++++++++++++++-- test/test_Numerical.jl | 308 +++++++++++++++++++++++++++++++- 4 files changed, 958 insertions(+), 146 deletions(-) diff --git a/src/Numerical/analyze.jl b/src/Numerical/analyze.jl index 0599ecc..1e97efd 100644 --- a/src/Numerical/analyze.jl +++ b/src/Numerical/analyze.jl @@ -67,8 +67,102 @@ function MathOptAnalyzer.analyze( ) end end - # TODO - # compute ranges and dynamic ranges for variables and matrix + # compute ranges and dynamic range violations + if !isempty(data._large_matrix_coefficient) && + !isempty(data._small_matrix_coefficient) + lower = data._small_matrix_coefficient[1][1] + upper = data._large_matrix_coefficient[1][1] + range = upper / lower + if range > data.threshold_dynamic_range_matrix + push!( + data.large_dynamic_range_matrix, + LargeDynamicRangeMatrix( + data._small_matrix_coefficient[1][2], + data._large_matrix_coefficient[1][2], + data._small_matrix_coefficient[1][3], + data._large_matrix_coefficient[1][3], + lower, + upper, + ), + ) + end + data.matrix_range = [lower, upper] + end + if !isempty(data._large_rhs_coefficient) && + !isempty(data._small_rhs_coefficient) + lower = data._small_rhs_coefficient[1][1] + upper = data._large_rhs_coefficient[1][1] + range = upper / lower + if range > data.threshold_dynamic_range_single + push!( + data.large_dynamic_range_rhs, + LargeDynamicRangeRHS( + data._small_rhs_coefficient[1][2], + data._large_rhs_coefficient[1][2], + lower, + upper, + ), + ) + end + data.rhs_range = [lower, upper] + end + if !isempty(data._large_bound_coefficient) && + !isempty(data._small_bound_coefficient) + lower = data._small_bound_coefficient[1][1] + upper = data._large_bound_coefficient[1][1] + range = upper / lower + if range > data.threshold_dynamic_range_single + push!( + data.large_dynamic_range_bounds, + LargeDynamicRangeBound( + data._small_bound_coefficient[1][2], + data._large_bound_coefficient[1][2], + lower, + upper, + ), + ) + end + data.bounds_range = [lower, upper] + end + if !isempty(data._large_objective_coefficient) && + !isempty(data._small_objective_coefficient) + lower = data._small_objective_coefficient[1][1] + upper = data._large_objective_coefficient[1][1] + range = upper / lower + if range > data.threshold_dynamic_range_single + push!( + data.large_dynamic_range_objective, + LargeDynamicRangeObjective( + data._small_objective_coefficient[1][2], + data._large_objective_coefficient[1][2], + lower, + upper, + ), + ) + end + data.objective_range = [lower, upper] + end + + for var in list_of_variables + if haskey(data._large_variable_coefficient, var) + lower = data._small_variable_coefficient[var][1] + upper = data._large_variable_coefficient[var][1] + range = upper / lower + if range > data.threshold_dynamic_range_single + push!( + data.large_dynamic_range_variables, + LargeDynamicRangeVariable( + var, + data._small_variable_coefficient[var][2], + data._large_variable_coefficient[var][2], + lower, + upper, + ), + ) + end + end + end + sort!(data.dense_rows, by = x -> x.nnz, rev = true) sort!(data.matrix_small, by = x -> abs(x.coefficient)) sort!(data.matrix_large, by = x -> abs(x.coefficient), rev = true) @@ -87,6 +181,16 @@ function MathOptAnalyzer.analyze( by = x -> abs(x.coefficient), rev = true, ) + sort!( + data.large_dynamic_range_constraints, + by = x -> (x.upper / x.lower), + rev = true, + ) + sort!( + data.large_dynamic_range_variables, + by = x -> (x.upper / x.lower), + rev = true, + ) return data end @@ -102,14 +206,13 @@ end function _get_objective_data(data, func::MOI.ScalarAffineFunction) nnz = 0 - _reset_function_range(data) for term in func.terms variable = term.variable coefficient = term.coefficient if iszero(coefficient) continue end - nnz += _update_function_range(data, coefficient, variable) + nnz += _update_objective_range(data, coefficient, variable) if abs(coefficient) < data.threshold_small push!( data.objective_small, @@ -122,52 +225,49 @@ function _get_objective_data(data, func::MOI.ScalarAffineFunction) ) end end - range = _function_range(data) - if range > data.threshold_dynamic_range_single - push!( - data.large_dynamic_range_objective, - LargeDynamicRangeObjective( - data._small_coefficient[1][2], - data._large_coefficient[1][2], - range, - ), - ) - end - _reset_function_range(data) return end function _reset_function_range(data::Data) - empty!(data._large_coefficient) - empty!(data._small_coefficient) - sizehint!(data._large_coefficient, 1) - sizehint!(data._small_coefficient, 1) + empty!(data._large_constraint_coefficient) + empty!(data._small_constraint_coefficient) + sizehint!(data._large_constraint_coefficient, 1) + sizehint!(data._small_constraint_coefficient, 1) return end -function _function_range(data::Data) - if isempty(data._large_coefficient) || isempty(data._small_coefficient) - return 0.0 +function _update_objective_range(data::Data, value, variable) + if iszero(value) + return 0 end - large = data._large_coefficient[1][1] - small = data._small_coefficient[1][1] - return large / small + value = abs(value) + if isempty(data._large_objective_coefficient) + push!(data._large_objective_coefficient, (value, variable)) + elseif value > data._large_objective_coefficient[1][1] + data._large_objective_coefficient[1] = (value, variable) + end + if isempty(data._small_objective_coefficient) + push!(data._small_objective_coefficient, (value, variable)) + elseif value < data._small_objective_coefficient[1][1] + data._small_objective_coefficient[1] = (value, variable) + end + return 1 end -function _update_function_range(data::Data, value, variable) +function _update_bound_range(data::Data, value, variable) if iszero(value) return 0 end value = abs(value) - if isempty(data._large_coefficient) - push!(data._large_coefficient, (value, variable)) - elseif value > data._large_coefficient[1][1] - data._large_coefficient[1] = (value, variable) + if isempty(data._large_bound_coefficient) + push!(data._large_bound_coefficient, (value, variable)) + elseif value > data._large_bound_coefficient[1][1] + data._large_bound_coefficient[1] = (value, variable) end - if isempty(data._small_coefficient) - push!(data._small_coefficient, (value, variable)) - elseif value < data._small_coefficient[1][1] - data._small_coefficient[1] = (value, variable) + if isempty(data._small_bound_coefficient) + push!(data._small_bound_coefficient, (value, variable)) + elseif value < data._small_bound_coefficient[1][1] + data._small_bound_coefficient[1] = (value, variable) end return 1 end @@ -176,9 +276,19 @@ function _update_constraint_range(data::Data, value, variable, ref) if iszero(value) return 0 end - _update_function_range(data, value, variable) value = abs(value) # + if isempty(data._large_constraint_coefficient) + push!(data._large_constraint_coefficient, (value, variable)) + elseif value > data._large_constraint_coefficient[1][1] + data._large_constraint_coefficient[1] = (value, variable) + end + if isempty(data._small_constraint_coefficient) + push!(data._small_constraint_coefficient, (value, variable)) + elseif value < data._small_constraint_coefficient[1][1] + data._small_constraint_coefficient[1] = (value, variable) + end + # if isempty(data._large_matrix_coefficient) push!(data._large_matrix_coefficient, (value, ref, variable)) elseif value > data._large_matrix_coefficient[1][1] @@ -192,7 +302,7 @@ function _update_constraint_range(data::Data, value, variable, ref) # if !haskey(data._large_variable_coefficient, variable) data._large_variable_coefficient[variable] = (value, ref) - elseif value > data._large_matrix_coefficient[variable][1] + elseif value > data._large_variable_coefficient[variable][1] data._large_variable_coefficient[variable] = (value, ref) end if !haskey(data._small_variable_coefficient, variable) @@ -203,6 +313,24 @@ function _update_constraint_range(data::Data, value, variable, ref) return 1 end +function _update_rhs_range(data::Data, value, constraint) + if iszero(value) + return 0 + end + # + if isempty(data._large_rhs_coefficient) + push!(data._large_rhs_coefficient, (value, constraint)) + elseif value > data._large_rhs_coefficient[1][1] + data._large_rhs_coefficient[1] = (value, constraint) + end + if isempty(data._small_rhs_coefficient) + push!(data._small_rhs_coefficient, (value, constraint)) + elseif value < data._small_rhs_coefficient[1][1] + data._small_rhs_coefficient[1] = (value, constraint) + end + return 1 +end + function _get_objective_data( data, func::MOI.ScalarQuadraticFunction{T}, @@ -323,7 +451,6 @@ function _get_constraint_matrix_data( if iszero(coefficient) continue end - # nnz += _update_range(data.matrix_range, coefficient) nnz += _update_constraint_range(data, coefficient, variable, ref) if abs(coefficient) < data.threshold_small push!( @@ -348,15 +475,21 @@ function _get_constraint_matrix_data( nnz > data.threshold_dense_entries push!(data.dense_rows, DenseConstraint(ref, nnz)) end - range = _function_range(data) + range = 1.0 + if !isempty(data._large_constraint_coefficient) + upper = data._large_constraint_coefficient[1][1] + lower = data._small_constraint_coefficient[1][1] + range = upper / lower + end if range > data.threshold_dynamic_range_single push!( data.large_dynamic_range_constraints, LargeDynamicRangeConstraint( ref, - data._small_coefficient[1][2], - data._large_coefficient[1][2], - range, + data._small_constraint_coefficient[1][2], + data._large_constraint_coefficient[1][2], + lower, + upper, ), ) end @@ -406,7 +539,7 @@ end function _get_constraint_matrix_data( data, ref::MOI.ConstraintIndex, - func::MOI.VectorAffineFunction{T}, + func::MOI.VectorAffineFunction{T}; ignore_extras::Bool = false, ) where {T} nnz = 0 @@ -418,7 +551,6 @@ function _get_constraint_matrix_data( if iszero(coefficient) continue end - # _update_range(data.matrix_range, coefficient) nnz += _update_constraint_range(data, coefficient, variable, ref) if abs(coefficient) < data.threshold_small push!( @@ -445,15 +577,21 @@ function _get_constraint_matrix_data( # nnz > data.threshold_dense_entries # push!(data.dense_rows, DenseConstraint(ref, nnz)) # end - range = _function_range(data) + range = 1.0 + if !isempty(data._large_constraint_coefficient) + upper = data._large_constraint_coefficient[1][1] + lower = data._small_constraint_coefficient[1][1] + range = upper / lower + end if range > data.threshold_dynamic_range_single push!( data.large_dynamic_range_constraints, LargeDynamicRangeConstraint( ref, - data._small_coefficient[1][2], - data._large_coefficient[1][2], - range, + data._small_constraint_coefficient[1][2], + data._large_constraint_coefficient[1][2], + lower, + upper, ), ) end @@ -532,7 +670,7 @@ function _get_constraint_data( if iszero(coefficient) return end - _update_range(data.rhs_range, coefficient) + _update_rhs_range(data, coefficient, ref) if abs(coefficient) < data.threshold_small push!(data.rhs_small, SmallRHSCoefficient(ref, coefficient)) elseif abs(coefficient) > data.threshold_large @@ -553,7 +691,7 @@ function _get_constraint_data( if iszero(coefficient) continue end - _update_range(data.rhs_range, coefficient) + _update_rhs_range(data, coefficient, ref) if abs(coefficient) < data.threshold_small push!(data.rhs_small, SmallRHSCoefficient(ref, coefficient)) elseif abs(coefficient) > data.threshold_large @@ -609,7 +747,7 @@ function _get_constraint_data( if iszero(coefficient) return end - _update_range(data.rhs_range, coefficient) + _update_rhs_range(data, coefficient, ref) if abs(coefficient) < data.threshold_small push!(data.rhs_small, SmallRHSCoefficient(ref, coefficient)) elseif abs(coefficient) > data.threshold_large @@ -664,7 +802,7 @@ function _get_constraint_data( if iszero(coefficient) return end - _update_range(data.rhs_range, coefficient) + _update_rhs_range(data, coefficient, ref) if abs(coefficient) < data.threshold_small push!(data.rhs_small, SmallRHSCoefficient(ref, coefficient)) elseif abs(coefficient) > data.threshold_large @@ -715,7 +853,7 @@ function _get_constraint_data( if iszero(coefficient) return end - _update_range(data.rhs_range, coefficient) + _update_rhs_range(data, coefficient, ref) if abs(coefficient) < data.threshold_small push!(data.rhs_small, SmallRHSCoefficient(ref, coefficient)) elseif abs(coefficient) > data.threshold_large @@ -732,7 +870,7 @@ function _get_constraint_data( ) coefficient = set.upper - func.constant if !(iszero(coefficient)) - _update_range(data.rhs_range, coefficient) + _update_rhs_range(data, coefficient, ref) if abs(coefficient) < data.threshold_small push!(data.rhs_small, SmallRHSCoefficient(ref, coefficient)) elseif abs(coefficient) > data.threshold_large @@ -743,7 +881,7 @@ function _get_constraint_data( if iszero(coefficient) return end - _update_range(data.rhs_range, coefficient) + _update_rhs_range(data, coefficient, ref) if abs(coefficient) < data.threshold_small push!(data.rhs_small, SmallRHSCoefficient(ref, coefficient)) elseif abs(coefficient) > data.threshold_large @@ -799,7 +937,7 @@ end function _get_variable_data(data, variable, coefficient::Number) if !(iszero(coefficient)) - _update_range(data.bounds_range, coefficient) + _update_bound_range(data, coefficient, variable) if abs(coefficient) < data.threshold_small push!( data.bounds_small, diff --git a/src/Numerical/structs.jl b/src/Numerical/structs.jl index 38ceb48..40f487c 100644 --- a/src/Numerical/structs.jl +++ b/src/Numerical/structs.jl @@ -454,9 +454,10 @@ julia> MathOptAnalyzer.summarize( """ struct LargeDynamicRangeConstraint <: AbstractNumericalIssue ref::MOI.ConstraintIndex - variable1::MOI.VariableIndex - variable2::MOI.VariableIndex - range::Float64 + variable_lower::MOI.VariableIndex + variable_upper::MOI.VariableIndex + lower::Float64 + upper::Float64 end function MathOptAnalyzer.constraint(issue::LargeDynamicRangeConstraint) @@ -464,14 +465,13 @@ function MathOptAnalyzer.constraint(issue::LargeDynamicRangeConstraint) end function MathOptAnalyzer.variables(issue::LargeDynamicRangeConstraint) - return [issue.variable1, issue.variable2] + return [issue.variable_lower, issue.variable_upper] end -function MathOptAnalyzer.value(issue::LargeDynamicRangeConstraint) - return issue.range +function MathOptAnalyzer.values(issue::LargeDynamicRangeConstraint) + return [issue.lower, issue.upper] end - """ LargeDynamicRangeMatrix <: AbstractNumericalIssue @@ -488,23 +488,24 @@ julia> MathOptAnalyzer.summarize( ``` """ struct LargeDynamicRangeMatrix <: AbstractNumericalIssue - constraint1::MOI.ConstraintIndex - constraint2::MOI.ConstraintIndex - variable1::MOI.VariableIndex - variable2::MOI.VariableIndex - range::Float64 + constraint_lower::MOI.ConstraintIndex + constraint_upper::MOI.ConstraintIndex + variable_lower::MOI.VariableIndex + variable_upper::MOI.VariableIndex + lower::Float64 + upper::Float64 end function MathOptAnalyzer.constraints(issue::LargeDynamicRangeMatrix) - return [issue.constraint1, issue.constraint2] + return [issue.constraint_lower, issue.constraint_upper] end function MathOptAnalyzer.variables(issue::LargeDynamicRangeMatrix) - return [issue.variable1, issue.variable2] + return [issue.variable_lower, issue.variable_upper] end -function MathOptAnalyzer.value(issue::LargeDynamicRangeMatrix) - return issue.range +function MathOptAnalyzer.values(issue::LargeDynamicRangeMatrix) + return [issue.lower, issue.upper] end """ @@ -523,48 +524,49 @@ julia> MathOptAnalyzer.summarize( ``` """ struct LargeDynamicRangeObjective <: AbstractNumericalIssue - variable1::MOI.VariableIndex - variable2::MOI.VariableIndex - range::Float64 + variable_lower::MOI.VariableIndex + variable_upper::MOI.VariableIndex + lower::Float64 + upper::Float64 end function MathOptAnalyzer.variables(issue::LargeDynamicRangeObjective) - return [issue.variable1, issue.variable2] + return [issue.variable_lower, issue.variable_upper] end -function MathOptAnalyzer.value(issue::LargeDynamicRangeObjective) - return issue.range +function MathOptAnalyzer.values(issue::LargeDynamicRangeObjective) + return [issue.lower, issue.upper] end -# """ -# LargeDynamicRangeRHS <: AbstractNumericalIssue - -# The `LargeDynamicRangeRHS` issue is identified when the dynamic range of the -# right-hand side (RHS) vector is larger than `threshold_dynamic_range_rhs`. The -# dynamic range is defined as the ratio between the largest and smallest -# coefficients of the RHS vector. +""" + LargeDynamicRangeRHS <: AbstractNumericalIssue -# For more information, run: -# ```julia -# julia> MathOptAnalyzer.summarize( -# MathOptAnalyzer.Numerical.LargeDynamicRangeRHS -# ) -# ``` -# """ -# struct LargeDynamicRangeRHS <: AbstractNumericalIssue -# constraint1::MOI.ConstraintIndex -# constraint2::MOI.ConstraintIndex -# range::Float64 -# end +The `LargeDynamicRangeRHS` issue is identified when the dynamic range of the +right-hand side (RHS) vector is larger than `threshold_dynamic_range_rhs`. The +dynamic range is defined as the ratio between the largest and smallest +coefficients of the RHS vector. -# function MathOptAnalyzer.constraints(issue::LargeDynamicRangeRHS) -# return [issue.constraint1, issue.constraint2] -# end +For more information, run: +```julia +julia> MathOptAnalyzer.summarize( + MathOptAnalyzer.Numerical.LargeDynamicRangeRHS +) +``` +""" +struct LargeDynamicRangeRHS <: AbstractNumericalIssue + constraint_lower::MOI.ConstraintIndex + constraint_upper::MOI.ConstraintIndex + lower::Float64 + upper::Float64 +end -# function MathOptAnalyzer.value(issue::LargeDynamicRangeRHS) -# return issue.range -# end +function MathOptAnalyzer.constraints(issue::LargeDynamicRangeRHS) + return [issue.constraint_lower, issue.constraint_upper] +end +function MathOptAnalyzer.values(issue::LargeDynamicRangeRHS) + return [issue.lower, issue.upper] +end """ LargeDynamicRangeVariable <: AbstractNumericalIssue @@ -583,9 +585,10 @@ julia> MathOptAnalyzer.summarize( """ struct LargeDynamicRangeVariable <: AbstractNumericalIssue variable::MOI.VariableIndex - constraint1::MOI.ConstraintIndex - constraint2::MOI.ConstraintIndex - range::Float64 + constraint_lower::MOI.ConstraintIndex + constraint_upper::MOI.ConstraintIndex + lower::Float64 + upper::Float64 end function MathOptAnalyzer.variable(issue::LargeDynamicRangeVariable) @@ -593,11 +596,40 @@ function MathOptAnalyzer.variable(issue::LargeDynamicRangeVariable) end function MathOptAnalyzer.constraints(issue::LargeDynamicRangeVariable) - return [issue.constraint1, issue.constraint2] + return [issue.constraint_lower, issue.constraint_upper] +end + +function MathOptAnalyzer.values(issue::LargeDynamicRangeVariable) + return [issue.lower, issue.upper] +end + +""" + LargeDynamicRangeBound <: AbstractNumericalIssue + +The `LargeDynamicRangeBound` issue is identified when the dynamic range of the +variables bounds is larger than `threshold_dynamic_range_single`. The dynamic +range is defined as the ratio between the largest and smallest bounds of all +variables. +For more information, run: +```julia +julia> MathOptAnalyzer.summarize( + MathOptAnalyzer.Numerical.LargeDynamicRangeBound +) +``` +""" +struct LargeDynamicRangeBound <: AbstractNumericalIssue + variable_lower::MOI.VariableIndex + variable_upper::MOI.VariableIndex + lower::Float64 + upper::Float64 +end + +function MathOptAnalyzer.variables(issue::LargeDynamicRangeBound) + return [issue.variable_lower, issue.variable_upper] end -function MathOptAnalyzer.value(issue::LargeDynamicRangeVariable) - return issue.range +function MathOptAnalyzer.values(issue::LargeDynamicRangeBound) + return [issue.lower, issue.upper] end """ @@ -622,25 +654,45 @@ Base.@kwdef mutable struct Data <: MathOptAnalyzer.AbstractData Tuple{DataType,DataType,Int}[] # objective_info::Any matrix_nnz::Int = 0 - # ranges # TODO remove after dyn range rewrite + # ranges are filled after the caches for large and small coefficients matrix_range::Vector{Float64} = sizehint!(Float64[1.0, 1.0], 2) bounds_range::Vector{Float64} = sizehint!(Float64[1.0, 1.0], 2) rhs_range::Vector{Float64} = sizehint!(Float64[1.0, 1.0], 2) objective_range::Vector{Float64} = sizehint!(Float64[1.0, 1.0], 2) # cache data _variables_in_constraints::Set{MOI.VariableIndex} = Set{MOI.VariableIndex}() - _large_matrix_coefficient::Vector{Tuple{Float64, MOI.ConstraintIndex, MOI.VariableIndex}} = - sizehint!(Tuple{Float64, MOI.ConstraintIndex, MOI.VariableIndex}[], 1) - _small_matrix_coefficient::Vector{Tuple{Float64, MOI.ConstraintIndex, MOI.VariableIndex}} = - sizehint!(Tuple{Float64, MOI.ConstraintIndex, MOI.VariableIndex}[], 1) - _large_variable_coefficient::Dict{MOI.VariableIndex, Tuple{Float64, MOI.ConstraintIndex}} = - Dict{MOI.VariableIndex, Tuple{Float64, MOI.ConstraintIndex}}() - _small_variable_coefficient::Dict{MOI.VariableIndex, Tuple{Float64, MOI.ConstraintIndex}} = - Dict{MOI.VariableIndex, Tuple{Float64, MOI.ConstraintIndex}}() - _large_coefficient::Vector{Tuple{Float64, MOI.VariableIndex}} = - sizehint!(Tuple{Float64, MOI.VariableIndex}[], 1) - _small_coefficient::Vector{Tuple{Float64, MOI.VariableIndex}} = - sizehint!(Tuple{Float64, MOI.VariableIndex}[], 1) + # for computing dynamic ranges + _large_matrix_coefficient::Vector{ + Tuple{Float64,MOI.ConstraintIndex,MOI.VariableIndex}, + } = sizehint!(Tuple{Float64,MOI.ConstraintIndex,MOI.VariableIndex}[], 1) + _small_matrix_coefficient::Vector{ + Tuple{Float64,MOI.ConstraintIndex,MOI.VariableIndex}, + } = sizehint!(Tuple{Float64,MOI.ConstraintIndex,MOI.VariableIndex}[], 1) + _large_variable_coefficient::Dict{ + MOI.VariableIndex, + Tuple{Float64,MOI.ConstraintIndex}, + } = Dict{MOI.VariableIndex,Tuple{Float64,MOI.ConstraintIndex}}() + _small_variable_coefficient::Dict{ + MOI.VariableIndex, + Tuple{Float64,MOI.ConstraintIndex}, + } = Dict{MOI.VariableIndex,Tuple{Float64,MOI.ConstraintIndex}}() + _large_rhs_coefficient::Vector{Tuple{Float64,MOI.ConstraintIndex}} = + sizehint!(Tuple{Float64,MOI.ConstraintIndex}[], 1) + _small_rhs_coefficient::Vector{Tuple{Float64,MOI.ConstraintIndex}} = + sizehint!(Tuple{Float64,MOI.ConstraintIndex}[], 1) + _large_objective_coefficient::Vector{Tuple{Float64,MOI.VariableIndex}} = + sizehint!(Tuple{Float64,MOI.VariableIndex}[], 1) + _small_objective_coefficient::Vector{Tuple{Float64,MOI.VariableIndex}} = + sizehint!(Tuple{Float64,MOI.VariableIndex}[], 1) + _large_bound_coefficient::Vector{Tuple{Float64,MOI.VariableIndex}} = + sizehint!(Tuple{Float64,MOI.VariableIndex}[], 1) + _small_bound_coefficient::Vector{Tuple{Float64,MOI.VariableIndex}} = + sizehint!(Tuple{Float64,MOI.VariableIndex}[], 1) + # the two below are used to compute dynamic ranges of constraint functions + _large_constraint_coefficient::Vector{Tuple{Float64,MOI.VariableIndex}} = + sizehint!(Tuple{Float64,MOI.VariableIndex}[], 1) + _small_constraint_coefficient::Vector{Tuple{Float64,MOI.VariableIndex}} = + sizehint!(Tuple{Float64,MOI.VariableIndex}[], 1) # variables analysis variables_not_in_constraints::Vector{VariableNotInConstraints} = VariableNotInConstraints[] @@ -657,12 +709,16 @@ Base.@kwdef mutable struct Data <: MathOptAnalyzer.AbstractData # dynamic range analysis large_dynamic_range_constraints::Vector{LargeDynamicRangeConstraint} = LargeDynamicRangeConstraint[] - large_matrix_dynamic_range::Vector{LargeDynamicRangeMatrix} = + large_dynamic_range_matrix::Vector{LargeDynamicRangeMatrix} = LargeDynamicRangeMatrix[] large_dynamic_range_objective::Vector{LargeDynamicRangeObjective} = LargeDynamicRangeObjective[] - # large_dynamic_range_rhs::Vector{LargeDynamicRangeRHS} = - # LargeDynamicRangeRHS[] + large_dynamic_range_rhs::Vector{LargeDynamicRangeRHS} = + LargeDynamicRangeRHS[] + large_dynamic_range_variables::Vector{LargeDynamicRangeVariable} = + LargeDynamicRangeVariable[] + large_dynamic_range_bounds::Vector{LargeDynamicRangeBound} = + LargeDynamicRangeBound[] # quadratic constraints analysis has_quadratic_constraints::Bool = false nonconvex_rows::Vector{NonconvexQuadraticConstraint} = diff --git a/src/Numerical/summarize.jl b/src/Numerical/summarize.jl index 3d4a0dc..fe1edae 100644 --- a/src/Numerical/summarize.jl +++ b/src/Numerical/summarize.jl @@ -90,26 +90,29 @@ function MathOptAnalyzer._summarize( return print(io, "# NonconvexQuadraticConstraint") end -function MathOptAnalyzer._summarize( - io::IO, - ::Type{LargeDynamicRangeConstraint}, -) +function MathOptAnalyzer._summarize(io::IO, ::Type{LargeDynamicRangeConstraint}) return print(io, "# LargeDynamicRangeConstraint") end -function MathOptAnalyzer._summarize( - io::IO, - ::Type{LargeDynamicRangeMatrix}, -) +function MathOptAnalyzer._summarize(io::IO, ::Type{LargeDynamicRangeMatrix}) return print(io, "# LargeDynamicRangeMatrix") -end +end -function MathOptAnalyzer._summarize( - io::IO, - ::Type{LargeDynamicRangeObjective}, -) +function MathOptAnalyzer._summarize(io::IO, ::Type{LargeDynamicRangeObjective}) return print(io, "# LargeDynamicRangeObjective") -end +end + +function MathOptAnalyzer._summarize(io::IO, ::Type{LargeDynamicRangeRHS}) + return print(io, "# LargeDynamicRangeRHS") +end + +function MathOptAnalyzer._summarize(io::IO, ::Type{LargeDynamicRangeVariable}) + return print(io, "# LargeDynamicRangeVariable") +end + +function MathOptAnalyzer._summarize(io::IO, ::Type{LargeDynamicRangeBound}) + return print(io, "# LargeDynamicRangeBound") +end function MathOptAnalyzer._verbose_summarize( io::IO, @@ -799,6 +802,40 @@ function MathOptAnalyzer._verbose_summarize( ) end +function MathOptAnalyzer._verbose_summarize( + io::IO, + ::Type{LargeDynamicRangeRHS}, +) + return print( + io, + """ + # `LargeDynamicRangeRHS` + + ## What + + A `LargeDynamicRangeRHS` issue is identified when the constraints + right-hand-side has a large dynamic range, that is, the ratio between + the largest and smallest rhs is large. + + ## Why + + Large dynamic ranges can lead to numerical instability in the solution + process. + + ## How to fix + + Check if the right-hand-side is correct. Check if the units of variables + and coefficients are correct. Check if the number makes is + reasonable given that solver have tolerances. Sometimes these + right-hand-sides can be replaced by zeros. + + ## More information + + No extra information for this issue. + """, + ) +end + function MathOptAnalyzer._verbose_summarize( io::IO, ::Type{LargeDynamicRangeVariable}, @@ -834,6 +871,41 @@ function MathOptAnalyzer._verbose_summarize( ) end +function MathOptAnalyzer._verbose_summarize( + io::IO, + ::Type{LargeDynamicRangeBound}, +) + return print( + io, + """ + # `LargeDynamicRangeBound` + + ## What + + A `LargeDynamicRangeBound` issue is identified when the dynamic range of + the bound is larger than `threshold_dynamic_range_single`. The dynamic range is + defined as the ratio between the largest and smallest bounds of all + variables + + ## Why + + Large dynamic ranges can lead to numerical instability in the solution + process. + + ## How to fix + + Check if the bound is correct. Check if the units of variables and + coefficients are correct. Check if the number makes is + reasonable given that solver have tolerances. Sometimes these + coefficients can be replaced by zeros. + + ## More information + + No extra information for this issue. + """, + ) +end + function MathOptAnalyzer._summarize( io::IO, issue::VariableNotInConstraints, @@ -1035,7 +1107,18 @@ function MathOptAnalyzer._summarize( issue::LargeDynamicRangeConstraint, model, ) - return print(io, MathOptAnalyzer._name(issue.ref, model), " : ", issue.range) + range = issue.upper / issue.lower + return print( + io, + MathOptAnalyzer._name(issue.ref, model), + " : ", + range, + " , [", + issue.lower, + ", ", + issue.upper, + "]", + ) end function MathOptAnalyzer._summarize( @@ -1043,7 +1126,17 @@ function MathOptAnalyzer._summarize( issue::LargeDynamicRangeMatrix, model, ) - return print(io, "Matrix has a Large dynamic range : ", issue.range) + range = issue.upper / issue.lower + return print( + io, + "Matrix dynamic range : ", + range, + " , [", + issue.lower, + ", ", + issue.upper, + "]", + ) end function MathOptAnalyzer._summarize( @@ -1051,7 +1144,31 @@ function MathOptAnalyzer._summarize( issue::LargeDynamicRangeObjective, model, ) - return print(io, "Objective has a Large dynamic range : ", issue.range) + range = issue.upper / issue.lower + return print( + io, + "Objective dynamic range : ", + range, + " , [", + issue.lower, + ", ", + issue.upper, + "]", + ) +end + +function MathOptAnalyzer._summarize(io::IO, issue::LargeDynamicRangeRHS, model) + range = issue.upper / issue.lower + return print( + io, + "RHS dynamic range : ", + range, + " , [", + issue.lower, + ", ", + issue.upper, + "]", + ) end function MathOptAnalyzer._summarize( @@ -1059,11 +1176,38 @@ function MathOptAnalyzer._summarize( issue::LargeDynamicRangeVariable, model, ) + range = issue.upper / issue.lower return print( io, MathOptAnalyzer._name(issue.variable, model), " : ", - issue.range, + range, + " , [", + issue.lower, + ", ", + issue.upper, + "]", + ) +end + +function MathOptAnalyzer._summarize( + io::IO, + issue::LargeDynamicRangeBound, + model, +) + range = issue.upper / issue.lower + return print( + io, + MathOptAnalyzer._name(issue.variable_lower, model), + " -- ", + MathOptAnalyzer._name(issue.variable_upper, model), + " : ", + range, + " , [", + issue.lower, + ", ", + issue.upper, + "]", ) end @@ -1306,6 +1450,118 @@ function MathOptAnalyzer._verbose_summarize( return print(io, "Constraint: ", MathOptAnalyzer._name(issue.ref, model)) end +function MathOptAnalyzer._verbose_summarize( + io::IO, + issue::LargeDynamicRangeConstraint, + model, +) + range = issue.upper / issue.lower + return print( + io, + "Constraint: ", + MathOptAnalyzer._name(issue.ref, model), + " with dynamic range ", + range, + " , [", + issue.lower, + ", ", + issue.upper, + "]", + ) +end + +function MathOptAnalyzer._verbose_summarize( + io::IO, + issue::LargeDynamicRangeMatrix, + model, +) + range = issue.upper / issue.lower + return print( + io, + "Matrix dynamic range: ", + range, + " , [", + issue.lower, + ", ", + issue.upper, + "]", + ) +end + +function MathOptAnalyzer._verbose_summarize( + io::IO, + issue::LargeDynamicRangeObjective, + model, +) + range = issue.upper / issue.lower + return print( + io, + "Objective dynamic range: ", + range, + " , [", + issue.lower, + ", ", + issue.upper, + "]", + ) +end + +function MathOptAnalyzer._verbose_summarize( + io::IO, + issue::LargeDynamicRangeRHS, + model, +) + range = issue.upper / issue.lower + return print( + io, + "RHS dynamic range: ", + range, + " , [", + issue.lower, + ", ", + issue.upper, + "]", + ) +end + +function MathOptAnalyzer._verbose_summarize( + io::IO, + issue::LargeDynamicRangeVariable, + model, +) + range = issue.upper / issue.lower + return print( + io, + "Variable: ", + MathOptAnalyzer._name(issue.variable, model), + " with dynamic range ", + range, + " , [", + issue.lower, + ", ", + issue.upper, + "]", + ) +end + +function MathOptAnalyzer._verbose_summarize( + io::IO, + issue::LargeDynamicRangeBound, + model, +) + range = issue.upper / issue.lower + return print( + io, + "Bounds with dynamic range: ", + range, + " , [", + issue.lower, + ", ", + issue.upper, + "]", + ) +end + function MathOptAnalyzer.list_of_issues( data::Data, ::Type{VariableNotInConstraints}, @@ -1420,6 +1676,48 @@ function MathOptAnalyzer.list_of_issues( return data.nonconvex_rows end +function MathOptAnalyzer.list_of_issues( + data::Data, + ::Type{LargeDynamicRangeConstraint}, +) + return data.large_dynamic_range_constraints +end + +function MathOptAnalyzer.list_of_issues( + data::Data, + ::Type{LargeDynamicRangeMatrix}, +) + return data.large_dynamic_range_matrix +end + +function MathOptAnalyzer.list_of_issues( + data::Data, + ::Type{LargeDynamicRangeObjective}, +) + return data.large_dynamic_range_objective +end + +function MathOptAnalyzer.list_of_issues( + data::Data, + ::Type{LargeDynamicRangeRHS}, +) + return data.large_dynamic_range_rhs +end + +function MathOptAnalyzer.list_of_issues( + data::Data, + ::Type{LargeDynamicRangeVariable}, +) + return data.large_dynamic_range_variables +end + +function MathOptAnalyzer.list_of_issues( + data::Data, + ::Type{LargeDynamicRangeBound}, +) + return data.large_dynamic_range_bounds +end + function MathOptAnalyzer.list_of_issue_types(data::Data) ret = Type[] for type in ( @@ -1441,6 +1739,12 @@ function MathOptAnalyzer.list_of_issue_types(data::Data) LargeMatrixQuadraticCoefficient, NonconvexQuadraticConstraint, NonconvexQuadraticObjective, + LargeDynamicRangeConstraint, + LargeDynamicRangeMatrix, + LargeDynamicRangeObjective, + LargeDynamicRangeRHS, + LargeDynamicRangeVariable, + LargeDynamicRangeBound, ) if !isempty(MathOptAnalyzer.list_of_issues(data, type)) push!(ret, type) @@ -1455,6 +1759,18 @@ function summarize_configurations(io::IO, data::Data) print(io, " Dense entries threshold: ", data.threshold_dense_entries, "\n") print(io, " Small coefficient threshold: ", data.threshold_small, "\n") print(io, " Large coefficient threshold: ", data.threshold_large, "\n") + print( + io, + " Individual dynamic range threshold: ", + data.threshold_dynamic_range_single, + "\n", + ) + print( + io, + " Matrix dynamic range threshold: ", + data.threshold_dynamic_range_matrix, + "\n", + ) return end diff --git a/test/test_Numerical.jl b/test/test_Numerical.jl index d7b650e..0a74f4f 100644 --- a/test/test_Numerical.jl +++ b/test/test_Numerical.jl @@ -30,7 +30,7 @@ function test_variable_bounds() @variable(model, zg == 4e+11) data = MathOptAnalyzer.analyze(MathOptAnalyzer.Numerical.Analyzer(), model) list = MathOptAnalyzer.list_of_issue_types(data) - @test length(list) == 3 + @test length(list) == 4 ret = MathOptAnalyzer.list_of_issues( data, MathOptAnalyzer.Numerical.VariableNotInConstraints, @@ -46,6 +46,11 @@ function test_variable_bounds() MathOptAnalyzer.Numerical.LargeBoundCoefficient, ) @test length(ret) == 3 + ret = MathOptAnalyzer.list_of_issues( + data, + MathOptAnalyzer.Numerical.LargeDynamicRangeBound, + ) + @test length(ret) == 1 buf = IOBuffer() MathOptAnalyzer.summarize( @@ -1152,7 +1157,7 @@ function test_vector_functions() @constraint(model, c6, [2 * x[1] * x[1]] in Zeros()) data = MathOptAnalyzer.analyze(MathOptAnalyzer.Numerical.Analyzer(), model) list = MathOptAnalyzer.list_of_issue_types(data) - @test length(list) == 6 + @test length(list) == 8 # ret = MathOptAnalyzer.list_of_issues( data, @@ -1196,6 +1201,23 @@ function test_vector_functions() ) @test length(ret) == 1 @test MathOptAnalyzer.variable(ret[], model) == x[3] + # + ret = MathOptAnalyzer.list_of_issues( + data, + MathOptAnalyzer.Numerical.LargeDynamicRangeMatrix, + ) + @test length(ret) == 1 + @test MathOptAnalyzer.variables(ret[], model) == [x[1], x[1]] + @test MathOptAnalyzer.values(ret[]) == [1e-9, 1e+9] + # + ret = MathOptAnalyzer.list_of_issues( + data, + MathOptAnalyzer.Numerical.LargeDynamicRangeVariable, + ) + @test length(ret) == 1 + @test MathOptAnalyzer.variable(ret[], model) == x[1] + @test MathOptAnalyzer.constraints(ret[], model) == [c1, c2] + @test MathOptAnalyzer.values(ret[]) == [1e-9, 1e+9] return end @@ -1205,7 +1227,7 @@ function test_variable_interval() @objective(model, Min, x) data = MathOptAnalyzer.analyze(MathOptAnalyzer.Numerical.Analyzer(), model) list = MathOptAnalyzer.list_of_issue_types(data) - @test length(list) == 3 + @test length(list) == 4 ret = MathOptAnalyzer.list_of_issues( data, MathOptAnalyzer.Numerical.SmallBoundCoefficient, @@ -1228,6 +1250,13 @@ function test_variable_interval() ) @test length(ret) == 1 @test MathOptAnalyzer.variable(ret[], model) == x + # + ret = MathOptAnalyzer.list_of_issues( + data, + MathOptAnalyzer.Numerical.LargeDynamicRangeBound, + ) + @test length(ret) == 1 + @test MathOptAnalyzer.variables(ret[], model) == [x, x] return end @@ -1357,6 +1386,279 @@ function test_more_than_max_issues() return end +function test_dyn_range_constraint_and_matrix() + model = Model() + @variable(model, x) + @variable(model, y) + @constraint(model, c, 1e-4 * x + 7e4 * y <= 4) + data = MathOptAnalyzer.analyze(MathOptAnalyzer.Numerical.Analyzer(), model) + list = MathOptAnalyzer.list_of_issue_types(data) + @test length(list) == 2 + ret = MathOptAnalyzer.list_of_issues( + data, + MathOptAnalyzer.Numerical.LargeDynamicRangeConstraint, + ) + @test length(ret) == 1 + @test MathOptAnalyzer.constraint(ret[], model) == c + @test MathOptAnalyzer.variables(ret[], model) == [x, y] + @test MathOptAnalyzer.values(ret[]) == [1e-4, 7e4] + # + buf = IOBuffer() + MathOptAnalyzer.summarize( + buf, + MathOptAnalyzer.Numerical.LargeDynamicRangeConstraint, + ) + str = String(take!(buf)) + @test startswith(str, "# `LargeDynamicRangeConstraint`") + MathOptAnalyzer.summarize( + buf, + MathOptAnalyzer.Numerical.LargeDynamicRangeConstraint, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# LargeDynamicRangeConstraint" + # + MathOptAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "Constraint: ") + @test contains(str, " with dynamic range ") + MathOptAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test contains(str, " : ") + @test contains(str, ", [") + # + ret = MathOptAnalyzer.list_of_issues( + data, + MathOptAnalyzer.Numerical.LargeDynamicRangeMatrix, + ) + @test length(ret) == 1 + @test MathOptAnalyzer.variables(ret[], model) == [x, y] + @test MathOptAnalyzer.values(ret[]) == [1e-4, 7e4] + # buf = IOBuffer() + MathOptAnalyzer.summarize( + buf, + MathOptAnalyzer.Numerical.LargeDynamicRangeMatrix, + ) + str = String(take!(buf)) + @test startswith(str, "# `LargeDynamicRangeMatrix`") + MathOptAnalyzer.summarize( + buf, + MathOptAnalyzer.Numerical.LargeDynamicRangeMatrix, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# LargeDynamicRangeMatrix" + # + MathOptAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "Matrix dynamic range") + MathOptAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test contains(str, "[") + return +end + +function test_dyn_range_variable_and_matrix() + model = Model() + @variable(model, x) + @variable(model, y) + @constraint(model, c1, 1e-4 * x + y <= 4) + @constraint(model, c2, 7e4 * x + y <= 4) + data = MathOptAnalyzer.analyze(MathOptAnalyzer.Numerical.Analyzer(), model) + list = MathOptAnalyzer.list_of_issue_types(data) + @test length(list) == 2 + ret = MathOptAnalyzer.list_of_issues( + data, + MathOptAnalyzer.Numerical.LargeDynamicRangeVariable, + ) + @test length(ret) == 1 + @test MathOptAnalyzer.variable(ret[], model) == x + @test MathOptAnalyzer.constraints(ret[], model) == [c1, c2] + @test MathOptAnalyzer.values(ret[]) == [1e-4, 7e4] + # + buf = IOBuffer() + MathOptAnalyzer.summarize( + buf, + MathOptAnalyzer.Numerical.LargeDynamicRangeVariable, + ) + str = String(take!(buf)) + @test startswith(str, "# `LargeDynamicRangeVariable`") + MathOptAnalyzer.summarize( + buf, + MathOptAnalyzer.Numerical.LargeDynamicRangeVariable, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# LargeDynamicRangeVariable" + # + MathOptAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "Variable:") + @test contains(str, " with dynamic range ") + MathOptAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test contains(str, " : ") + @test contains(str, ", [") + # + ret = MathOptAnalyzer.list_of_issues( + data, + MathOptAnalyzer.Numerical.LargeDynamicRangeMatrix, + ) + @test length(ret) == 1 + @test MathOptAnalyzer.variables(ret[], model) == [x, x] + @test MathOptAnalyzer.constraints(ret[], model) == [c1, c2] + @test MathOptAnalyzer.values(ret[]) == [1e-4, 7e4] + # buf = IOBuffer() + MathOptAnalyzer.summarize( + buf, + MathOptAnalyzer.Numerical.LargeDynamicRangeMatrix, + ) + str = String(take!(buf)) + @test startswith(str, "# `LargeDynamicRangeMatrix`") + MathOptAnalyzer.summarize( + buf, + MathOptAnalyzer.Numerical.LargeDynamicRangeMatrix, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# LargeDynamicRangeMatrix" + # + MathOptAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "Matrix dynamic range") + MathOptAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test contains(str, "[") + return +end + +function test_dyn_range_objective() + model = Model() + @variable(model, x) + @variable(model, y) + @constraint(model, c1, 1 * x + y <= 4) + @constraint(model, c2, 2 * x + y <= 4) + @objective(model, Min, 1e-4 * x + 7e4 * y) + data = MathOptAnalyzer.analyze(MathOptAnalyzer.Numerical.Analyzer(), model) + list = MathOptAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = MathOptAnalyzer.list_of_issues( + data, + MathOptAnalyzer.Numerical.LargeDynamicRangeObjective, + ) + @show ret + @test length(ret) == 1 + @test MathOptAnalyzer.variables(ret[], model) == [x, y] + @test MathOptAnalyzer.values(ret[]) == [1e-4, 7e4] + # + buf = IOBuffer() + MathOptAnalyzer.summarize( + buf, + MathOptAnalyzer.Numerical.LargeDynamicRangeObjective, + ) + str = String(take!(buf)) + @test startswith(str, "# `LargeDynamicRangeObjective`") + MathOptAnalyzer.summarize( + buf, + MathOptAnalyzer.Numerical.LargeDynamicRangeObjective, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# LargeDynamicRangeObjective" + # + MathOptAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "Objective dynamic range") + MathOptAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test contains(str, "[") + return +end + +function test_dyn_range_rhs() + model = Model() + @variable(model, x) + @variable(model, y) + @constraint(model, c1, x + y <= 1e-4) + @constraint(model, c2, x + y <= 7e4) + data = MathOptAnalyzer.analyze(MathOptAnalyzer.Numerical.Analyzer(), model) + list = MathOptAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = MathOptAnalyzer.list_of_issues( + data, + MathOptAnalyzer.Numerical.LargeDynamicRangeRHS, + ) + @test length(ret) == 1 + @test MathOptAnalyzer.constraints(ret[], model) == [c1, c2] + @test MathOptAnalyzer.values(ret[]) == [1e-4, 7e4] + # + buf = IOBuffer() + MathOptAnalyzer.summarize( + buf, + MathOptAnalyzer.Numerical.LargeDynamicRangeRHS, + ) + str = String(take!(buf)) + @test startswith(str, "# `LargeDynamicRangeRHS`") + MathOptAnalyzer.summarize( + buf, + MathOptAnalyzer.Numerical.LargeDynamicRangeRHS, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# LargeDynamicRangeRHS" + # + MathOptAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "RHS dynamic range:") + MathOptAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test contains(str, " : ") + @test contains(str, ", [") + return +end + +function test_dyn_range_bounds() + model = Model() + @variable(model, x <= 1e-4) + @variable(model, y >= 7e4) + @constraint(model, c1, x + y <= 4) + @constraint(model, c2, x - y >= 3) + data = MathOptAnalyzer.analyze(MathOptAnalyzer.Numerical.Analyzer(), model) + list = MathOptAnalyzer.list_of_issue_types(data) + @test length(list) == 1 + ret = MathOptAnalyzer.list_of_issues( + data, + MathOptAnalyzer.Numerical.LargeDynamicRangeBound, + ) + @test length(ret) == 1 + @test MathOptAnalyzer.variables(ret[], model) == [x, y] + @test MathOptAnalyzer.values(ret[]) == [1e-4, 7e4] + # + buf = IOBuffer() + MathOptAnalyzer.summarize( + buf, + MathOptAnalyzer.Numerical.LargeDynamicRangeBound, + ) + str = String(take!(buf)) + @test startswith(str, "# `LargeDynamicRangeBound`") + MathOptAnalyzer.summarize( + buf, + MathOptAnalyzer.Numerical.LargeDynamicRangeBound, + verbose = false, + ) + str = String(take!(buf)) + @test str == "# LargeDynamicRangeBound" + # + MathOptAnalyzer.summarize(buf, ret[1], verbose = true) + str = String(take!(buf)) + @test startswith(str, "Bounds with dynamic range:") + MathOptAnalyzer.summarize(buf, ret[1], verbose = false) + str = String(take!(buf)) + @test contains(str, " : ") + @test contains(str, ", [") + return +end + end # module TestNumerical TestNumerical.runtests()