Skip to content

Conflict tools? #116

Description

@acostarelli

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions