Would it make sense to add some user-helper functions for diagnosing conflicts more easily? Something that prints out conflicting constraints? I asked Claude what this might look non-IOM-specific:
module ConflictDebug
using JuMP
export print_conflict, find_conflict
"""
find_conflict(model; verbose=true) -> Vector{ConstraintRef}
Return the constraints involved in the infeasibility of `model`.
Strategy:
1. If the solver implements a conflict refiner (Gurobi/CPLEX/Xpress),
use `compute_conflict!` to get the exact IIS.
2. Otherwise fall back to `relax_with_penalty!` and report constraints
whose slack is nonzero ("approximate" conflict — works on any solver).
You can force/override behaviour per solver by adding a method to
`supports_native_conflict(::MOI.AbstractOptimizer)` (see bottom of file).
"""
function find_conflict(model::Model; verbose::Bool = true, tol::Float64 = 1e-6)
if termination_status(model) == OPTIMIZE_NOT_CALLED
optimize!(model)
end
st = termination_status(model)
if st ∉ (INFEASIBLE, LOCALLY_INFEASIBLE, INFEASIBLE_OR_UNBOUNDED)
@warn "Model is not infeasible (status = $st); conflict may be meaningless."
end
if _try_native_conflict(model)
verbose && @info "Using native solver conflict refiner (exact IIS)."
return _collect_native_conflict(model; verbose)
else
verbose && @info "Solver has no conflict refiner; using penalty relaxation (approximate)."
return _collect_relaxed_conflict(model; verbose, tol)
end
end
"""
print_conflict(model)
Convenience wrapper: compute the conflict and pretty-print the offending
constraints (and, when available, a minimal infeasible sub-model).
"""
function print_conflict(model::Model; kwargs...)
cons = find_conflict(model; kwargs...)
if isempty(cons)
println("No conflicting constraints identified.")
return cons
end
println("\n=== Conflicting constraints ($(length(cons))) ===")
for c in cons
name = JuMP.name(c)
label = isempty(name) ? string(c) : name
println(" • ", label, " : ", _show_constraint(c))
end
println("="^40)
return cons
end
# ---------------------------------------------------------------------------
# Native IIS path
# ---------------------------------------------------------------------------
function _try_native_conflict(model::Model)::Bool
supports_native_conflict(unsafe_backend(model)) || return false
try
compute_conflict!(model)
return get_attribute(model, MOI.ConflictStatus()) == MOI.CONFLICT_FOUND
catch err
@debug "Native conflict computation failed; falling back." exception = err
return false
end
end
function _collect_native_conflict(model::Model; verbose::Bool)
out = JuMP.ConstraintRef[]
for (F, S) in list_of_constraint_types(model)
for con in all_constraints(model, F, S)
status = MOI.get(model, MOI.ConstraintConflictStatus(), con)
if status == MOI.IN_CONFLICT
push!(out, con)
end
end
end
return out
end
# ---------------------------------------------------------------------------
# Penalty-relaxation fallback (any solver)
# ---------------------------------------------------------------------------
function _collect_relaxed_conflict(model::Model; verbose::Bool, tol::Float64)
# relax_with_penalty! mutates the model, so work on a copy.
work, ref_map = copy_model(model)
set_optimizer(work, solver_constructor(model))
penalty_map = relax_with_penalty!(work)
optimize!(work)
if termination_status(work) ∉ (OPTIMAL, LOCALLY_SOLVED, ALMOST_OPTIMAL)
@warn "Relaxed model did not solve cleanly (status = $(termination_status(work)))."
end
out = JuMP.ConstraintRef[]
for (con_in_copy, penalty_var) in penalty_map
v = value(penalty_var)
if abs(v) > tol
# map the copied constraint back to the original model's ref
orig = _reverse_lookup(ref_map, con_in_copy)
push!(out, something(orig, con_in_copy))
verbose && println(" violated by $(round(v; digits=6)): ",
_show_constraint(con_in_copy))
end
end
return out
end
# copy_model doesn't carry the optimizer; remember how to rebuild it.
# Users can override this if their constructor needs arguments/attributes.
solver_constructor(model::Model) = MOI.get(model, MOI.Name()) === nothing ?
error("Set `ConflictDebug.solver_constructor(::Model)` to your optimizer constructor, " *
"e.g. `() -> HiGHS.Optimizer()`.") :
error("override solver_constructor")
function _reverse_lookup(ref_map, con)
# ReferenceMap maps original -> copy; we need copy -> original.
for (orig, copied) in ref_map.con_map
copied == con && return orig
end
return nothing
end
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
function _show_constraint(c)
try
return string(constraint_object(c))
catch
return string(c)
end
end
# ---------------------------------------------------------------------------
# Per-solver extension point
# ---------------------------------------------------------------------------
# Default: assume no refiner. Add a method returning `true` for solvers that
# implement MOI.compute_conflict!. These are loaded lazily so you don't need
# the package installed unless you use it.
supports_native_conflict(::MOI.AbstractOptimizer) = false
end # module
Would it make sense to add some user-helper functions for diagnosing conflicts more easily? Something that prints out conflicting constraints? I asked Claude what this might look non-IOM-specific: