Sienna-Platform / Sienna-Platform/InfrastructureOptimizationModels.jl

Conflict tools?

Open
#116 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
Julia
Stars
1
Forks
0
Avg merge
8h 51m
Merged PRs (30d)
9

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

The issue proposes a Julia helper module for diagnosing infeasible optimization models, with native conflict-refiner and penalty-relaxation paths. No repository files, tests, or entry points are named, and the desired API and acceptance criteria are not decided; first review the package structure and existing JuMP/MOI integrations before defining the scope.

Written by the indexing model from the issue text.

Assessment

Tech stack
julia
Domain
tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.