Skip to content
12 changes: 11 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,31 @@ BitBasis = "50ba71b6-fa0f-514d-ae9a-0916efc90dcf"
DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
MLStyle = "d8e11817-5142-5d16-987a-aa16d5891078"
PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"

[weakdeps]
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"

[extensions]
IPSolverExt = "JuMP"

[compat]
BitBasis = "0.9"
DocStringExtensions = "0.9"
Graphs = "1"
InteractiveUtils = "1"
JuMP = "1"
LinearAlgebra = "1"
MLStyle = "0.4"
PrettyTables = "2"
julia = "1.10"

[extras]
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
SCIP = "82193955-e24f-5292-bf16-6f2c5261a85f"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test", "Documenter"]
test = ["Test", "Documenter", "JuMP", "SCIP"]
104 changes: 104 additions & 0 deletions ext/IPSolverExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
module IPSolverExt

import JuMP
using ProblemReductions
using LinearAlgebra

function Base.findmin(problem::AbstractProblem, solver::IPSolver)
return _find(problem, solver,true)
end
function Base.findmax(problem::AbstractProblem, solver::IPSolver)
return _find(problem, solver,false)
end

# tag: true for min, false for max
function _find(problem::AbstractProblem, solver::IPSolver,tag::Bool)
@assert num_flavors(problem) == 2 "IPSolver only supports boolean variables"
cons = constraints(problem)
nsc = ProblemReductions.num_variables(problem)
maxN = maximum([length(c.variables) for c in cons])
combs = [ProblemReductions.combinations(2,i) for i in 1:maxN]

objs = objectives(problem)
@assert all(length(obj.variables) <= 1 for obj in objs) "IPSolver only supports objectives with at most 1 variables"

# IP by JuMP
model = JuMP.Model(solver.optimizer)
!solver.verbose && JuMP.set_silent(model)

JuMP.@variable(model, 0 <= x[i = 1:nsc] <= 1, Int)

for con in cons
f_vec = findall(!,con.specification)
num_vars = length(con.variables)
for f in f_vec
JuMP.@constraint(model, sum(j-> iszero(combs[num_vars][f][j]) ? (1 - x[con.variables[j]]) : x[con.variables[j]], 1:num_vars) <= num_vars -1)
end
end

obj_sum = 0
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unstable type.

for obj in objs
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Objectives can have more than one variables. The current code will scilently break for Spinglass.

obj_sum += (1-x[obj.variables[1]])*obj.specification[1] + x[obj.variables[1]]*obj.specification[2]
end

tag ? JuMP.@objective(model, Min, obj_sum) : JuMP.@objective(model, Max, obj_sum)

JuMP.optimize!(model)
@assert JuMP.is_solved_and_feasible(model)
return round.(Int, JuMP.value.(x))
end

function minimal_constraints(nflavor::Int, set::Vector)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is not tested.

subsets = [covering_items(c, totalset) for c in clauses]
return filter(set -> issubset(set, coverset), subsets)

Check warning on line 53 in ext/IPSolverExt.jl

View check run for this annotation

Codecov / codecov/patch

ext/IPSolverExt.jl#L51-L53

Added lines #L51 - L53 were not covered by tests
end

"""
minimal_set_cover(coverset::Vector{Int}, subsets::Vector{Vector{Int}}, optimizer)
Solve the set cover problem: all elements in the coverset must be covered by the subsets.
The objective is to minimize the number of subsets used.
# Arguments
- `coverset::Vector{Int}`: The set of all elements to be covered.
- `subsets::Vector{Vector{Int}}`: The set of subsets to choose from.
- `optimizer`: The optimizer to use, e.g. SCIP.Optimizer or HiGHS.Optimizer
# Returns
- `Vector{Int}`: The indices of the subsets to choose.
"""
function minimal_set_cover(coverset::Vector{Int}, subsets::Vector{Vector{Int}}, optimizer, verbose::Bool=false)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not need it anymore. Maybe we can dispatch it to set covering problem solver.

# Remove subsets that cover not existing elements in the coverset
@assert all(set -> issubset(set, coverset), subsets) "subsets ($subsets) must not cover any elements absent in the coverset ($coverset)"

# Create a JuMP model for exact set cover
model = JuMP.Model(optimizer)
!verbose && JuMP.set_silent(model)

# Define binary variables for each subset (1 if selected, 0 if not)
n = length(subsets)
JuMP.@variable(model, x[1:n], Bin)

# Each element in the coverset must be covered exactly once
for element in coverset
# Find all subsets containing this element
covering_subsets = [i for i in 1:n if element in subsets[i]]

# Each element must be covered exactly once
JuMP.@constraint(model, sum(x[i] for i in covering_subsets) >= 1)
end

# Minimize the number of subsets used (optional objective)
JuMP.@objective(model, Min, sum(x))
# Solve the model
JuMP.optimize!(model)

# Return the solution if feasible
if JuMP.termination_status(model) == JuMP.MOI.OPTIMAL
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use @assert is_solved_and_feasible(model) "error message..."?

return [i for i in 1:n if JuMP.value(x[i]) > 0.5]
else
error("No solution found")

Check warning on line 100 in ext/IPSolverExt.jl

View check run for this annotation

Codecov / codecov/patch

ext/IPSolverExt.jl#L100

Added line #L100 was not covered by tests
end
end

end
4 changes: 2 additions & 2 deletions src/ProblemReductions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export LogicGadget, truth_table
export ReductionSATTo3SAT
export ReductionCircuitToSpinGlass, ReductionMaxCutToSpinGlass, ReductionSpinGlassToMaxCut, ReductionVertexCoveringToSetCovering, ReductionSatToColoring,
ReductionSpinGlassToQUBO, ReductionQUBOToSpinGlass
export findbest, BruteForce
export findbest, BruteForce, AbstractSolver, IPSolver
export CNF
export ReductionSATToIndependentSet, ReductionSATToDominatingSet
export ReductionIndependentSetToSetPacking, ReductionSetPackingToIndependentSet
Expand All @@ -57,7 +57,7 @@ include("topology.jl")
include("bitvector.jl")
include("models/models.jl")
include("rules/rules.jl")
include("bruteforce.jl")
include("solvers.jl")
include("reduction_path.jl")
include("deprecated.jl")

Expand Down
11 changes: 10 additions & 1 deletion src/bruteforce.jl → src/solvers.jl
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
abstract type AbstractSolver end

"""
$TYPEDEF

A brute force method to find the best configuration of a problem.
"""
Base.@kwdef struct BruteForce{T}
Base.@kwdef struct BruteForce{T} <: AbstractSolver
atol::T=eps(Float64)
rtol::T=eps(Float64)
end
Expand Down Expand Up @@ -38,3 +40,10 @@ function _find!(compare, best_configs, configs, sizes, initial, atol, rtol)
end
end
end

# Interface for IPSolver
Base.@kwdef struct IPSolver <: AbstractSolver
optimizer # e.g. HiGHS.Optimizer
max_itr::Int = 20
verbose::Bool = false
end
53 changes: 53 additions & 0 deletions test/IPSolverExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using Test
using JuMP
using ProblemReductions
using SCIP
using Graphs

@testset "IPSolverExt" begin
# Test exact_set_cover with HiGHS optimizer
optimizer = SCIP.Optimizer
nflavor = 5
subsets = [[1, 2], [2, 3], [3, 4], [4, 5]]
coverset = [1, 2, 3, 4, 5]

# Test exact_set_cover with HiGHS optimizer
Ext = Base.get_extension(ProblemReductions, :IPSolverExt)
result = Ext.minimal_set_cover(coverset, subsets, optimizer)
@test result == [1, 2, 4] || result == [1, 3, 4]
end

@testset "IPSolver" begin
graph = smallgraph(:petersen)
problem = MaximalIS(graph)
@test findmin(problem, IPSolver(SCIP.Optimizer,20,false)) ∈ findmin(problem, BruteForce())

problem = IndependentSet(graph)
@test findmax(problem, IPSolver(SCIP.Optimizer,20,false)) ∈ findmax(problem, BruteForce())

fact3 = Factoring(2, 1, 3)
res3 = reduceto(CircuitSAT, fact3)
problem = CircuitSAT(res3.circuit.circuit; use_constraints=true)
@test findmin(problem, IPSolver(SCIP.Optimizer,20,false)) ∈ findmin(problem, BruteForce())
best_config3 = findmin(problem, IPSolver(SCIP.Optimizer,20,false))
assignment3 = Dict(zip(res3.circuit.symbols, best_config3))
@test (2* assignment3[:p2]+ assignment3[:p1]) * assignment3[:q1] == 3

m1 = Matching(graph)
@test findmax(m1, IPSolver(SCIP.Optimizer,20,false)) ∈ findbest(m1, BruteForce())
end

@testset "Factoring" begin
function factoring(m,n,N,solver)
fact3 = Factoring(m, n, N)
res3 = reduceto(CircuitSAT, fact3)
problem = CircuitSAT(res3.circuit.circuit; use_constraints=true)
vals = findmin(problem, IPSolver(solver,20,true))
return ProblemReductions.read_solution(fact3, [vals[res3.p]...,vals[res3.q]...])
end
a,b = factoring(5,5,899,SCIP.Optimizer)
@test a*b == 899

# using BenchmarkTools
# @btime factoring(10,10,1040399) # 37.499 ms (505559 allocations: 22.91 MiB)
end
7 changes: 7 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ using Documenter
include("bitvector.jl")
end

@testset "solvers" begin
include("solvers.jl")
end

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IPSolverExt.jl should be included.

@testset "models" begin
include("models/models.jl")
end
Expand All @@ -30,5 +34,8 @@ end
include("deprecated.jl")
end

@testset "IPSolverExt" begin
include("IPSolverExt.jl")
end
DocMeta.setdocmeta!(ProblemReductions, :DocTestSetup, :(using ProblemReductions); recursive=true)
Documenter.doctest(ProblemReductions; manual=false, fix=false)
12 changes: 12 additions & 0 deletions test/solvers.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Test, ProblemReductions, Graphs

@testset "BruteForce" begin
graph = smallgraph(:petersen)
problem = IndependentSet(graph)
solver = BruteForce()
res = findbest(problem, solver)
@test res == [[0, 0, 1, 0, 1, 1, 1, 0, 0, 0], [1, 0, 0, 1, 0, 0, 1, 1, 0, 0], [0, 1, 0, 0, 1, 0, 0, 1, 1, 0], [0, 1, 0, 1, 0, 1, 0, 0, 0, 1], [1, 0, 1, 0, 0, 0, 0, 0, 1, 1]]
solver = BruteForce()
res = findbest(problem, solver)
@test res == [[0, 0, 1, 0, 1, 1, 1, 0, 0, 0], [1, 0, 0, 1, 0, 0, 1, 1, 0, 0], [0, 1, 0, 0, 1, 0, 0, 1, 1, 0], [0, 1, 0, 1, 0, 1, 0, 0, 0, 1], [1, 0, 1, 0, 0, 0, 0, 0, 1, 1]]
end
Loading