Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
fail-fast: false
matrix:
version:
- '1.10'
- '1'
- 'nightly'
os:
- ubuntu-latest
Expand Down
19 changes: 5 additions & 14 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,13 @@ A model problem is a subclass of [`AbstractProblem`](@ref) that defines the ener
Facts affecting the computational complexity classification of the problem also include the topology of the problem and the domain of the variables.

The required interfaces are:
- [`variables`](@ref): The degrees of freedoms in the problem.
e.g. for the maximum independent set problems, they are the indices of vertices: 1, 2, 3...,
while for the max cut problem, they are the edges.
- [`flavors`](@ref): A vector of integers as the flavors (or domain) of a degree of freedom.
e.g. for the maximum independent set problems, the flavors are [0, 1], where 0 means the vertex is not in the set and 1 means the vertex is in the set.
- [`weights`](@ref): Energies associated with constraints.
- [`energy`](@ref): Energy of a given configuration.
- [`problem_size`](@ref): The size of the problem, which is the number of variables.
- [`num_variables`](@ref): The number of variables in the problem, the variables are `1:num_variables`.
- [`flavors`](@ref): A tuple of integers as the flavors (or domain) of a degree of freedom.
e.g. for the maximum independent set problems, the flavors are `(0, 1)`, where `0` means the vertex is not in the set and `1` means the vertex is in the set.
- [`energy`](@ref): Energy of a given configuration, the smaller the better. If a configuration is invalid, the energy should be `Inf`.

Optional functions include:
- [`num_variables`](@ref): The number of variables in the problem.
- [`num_flavors`](@ref): The number of flavors in the problem.
- [`set_weights`](@ref): Change the weights for the `problem` and return a new problem instance.
- [`weight_type`](@ref): The data type of weights.
- [`findbest`](@ref): Find the best configurations in the computational problem.
- [`problem_size`](@ref): The size of the problem, which is the number of variables.

The following code lists all problems defined in ProblemReductions:
```@repl reduction_graph
Expand Down Expand Up @@ -97,7 +89,6 @@ A problem reduction rule is a function that reduces a problem to another problem

Optional functions include:
- [`extract_multiple_solutions`](@ref): Extract a set of solutions to the target problem back to the original problem. You may want to implement this when you want to make extracting multiple solutions faster.
- [`reduce_size`](@ref): Infer the size of the target problem from the source problem size.

The [`reduction_graph`](@ref) function returns the reduction graph of the problems that induced by the reduction rules defined in ProblemReductions:
```@repl reduction_graph
Expand Down
8 changes: 4 additions & 4 deletions src/ProblemReductions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ export @bit_str
export TruthTable
export HyperGraph, UnitDiskGraph, GridGraph, PlanarGraph, SimpleGraph
export @bv_str, StaticElementVector, StaticBitVector, statictrues, staticfalses, onehotv
export num_variables, num_flavors, variables, flavors, weights, set_weights, is_weighted, energy, weight_type, problem_size, configuration_space_size, constraints
export num_variables, num_flavors, variables, flavors, weights, set_weights, is_weighted, energy, weight_type, problem_size, configuration_space_size
export UnitWeight

# models
export BooleanExpr, Circuit, Assignment, simple_form, CircuitSAT, @circuit, booleans, ¬, ∨, ∧, ⊻, is_literal, is_cnf, is_dnf
export SpinGlass, spinglass_gadget
export Coloring, coloring_energy, is_vertex_coloring
export SetCovering, is_set_covering, set_covering_energy
export Coloring, is_vertex_coloring
export SetCovering, is_set_covering
export BoolVar, CNFClause, CNF, Satisfiability, is_kSAT, satisfiable, KSatisfiability
export MaxCut
export IndependentSet
export VertexCovering, is_vertex_covering, vertex_covering_energy
export VertexCovering, is_vertex_covering
export SetPacking, is_set_packing
export DominatingSet
export QUBO
Expand Down
28 changes: 17 additions & 11 deletions src/bruteforce.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,23 @@ A brute force method to find the best configuration of a problem.
struct BruteForce end

function findbest(problem::AbstractProblem, ::BruteForce; atol=eps(Float64), rtol=eps(Float64))
best_size = Inf
best_configs = Vector{Int}[]
configs = Iterators.product([flavors(problem) for i in 1:num_variables(problem)]...)
for (size, config) in energy_multi(problem, configs)
if isapprox(size, best_size; atol, rtol)
push!(best_configs, collect(config))
elseif size < best_size[1]
best_size = Float64(size)
empty!(best_configs)
push!(best_configs, collect(config))
flvs = flavors(problem)
best_ids = NTuple{num_variables(problem), Int}[]
configs = Iterators.product([1:length(flvs) for i in 1:num_variables(problem)]...)
energies = energy_eval_byid_multiple(problem, configs)
_findbest!(best_ids, configs, energies, atol, rtol)
return [collect(id_to_config(problem, id)) for id in best_ids]
end

function _findbest!(best_ids, configs, energies, atol, rtol)
best_energy = Inf
for (id, energy) in zip(configs, energies)
if isapprox(energy, best_energy; atol, rtol)
push!(best_ids, id)
elseif energy < best_energy
best_energy = energy
empty!(best_ids)
push!(best_ids, id)
end
end
return best_configs
end
30 changes: 14 additions & 16 deletions src/models/Circuit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,6 @@ function print_statements(io::IO, exprs)
end
Base.show(io::IO, ::MIME"text/plain", x::Circuit) = show(io, x)

function symbols(expr)
vars = Symbol[]
extract_symbols!(expr, vars)
return unique!(vars)
end

function extract_symbols!(c::Circuit, vars::Vector{Symbol})
for ex in c.exprs
extract_symbols!(ex, vars)
Expand Down Expand Up @@ -236,9 +230,7 @@ julia> sat.symbols
:d

julia> flavors(sat)
2-element Vector{Int64}:
0
1
(0, 1)

julia> energy(sat, [true, false, true, true, false, false, true])
3
Expand Down Expand Up @@ -278,8 +270,8 @@ end
Base.show(io::IO, ::MIME"text/plain", x::CircuitSAT) = show(io, x)

# variables interface
variables(c::CircuitSAT) = collect(1:length(c.symbols))
flavors(::Type{<:CircuitSAT}) = [0, 1]
num_variables(c::CircuitSAT) = length(c.symbols)
flavors(::Type{<:CircuitSAT}) = (0, 1)
problem_size(c::CircuitSAT) = (; num_exprs=length(c.circuit.exprs), num_variables=length(c.symbols))

# weights interface
Expand All @@ -288,18 +280,24 @@ set_weights(c::CircuitSAT, weights) = CircuitSAT(c.circuit, weights, c.symbols)

# constraints interface
@nohard_constraints CircuitSAT
function energy_terms(c::CircuitSAT)
function soft_constraints(c::CircuitSAT)
syms = symbols(c.circuit)
return [LocalConstraint([findfirst(==(s), c.symbols) for s in syms], syms=>expr) for expr in c.circuit.exprs]
return [SoftConstraint([findfirst(==(s), c.symbols) for s in syms], syms=>expr, w) for (w, expr) in zip(c.weights, c.circuit.exprs)]
end

function local_energy(::Type{<:CircuitSAT{T}}, spec::LocalConstraint, config) where {T}
function local_energy(::Type{<:CircuitSAT{T}}, spec::SoftConstraint{WT}, config) where {T, WT}
@assert length(config) == num_variables(spec)
syms, ex = spec.specification
dict = Dict(syms[i]=>Bool(c) for (i, c) in enumerate(config))
for o in ex.outputs
@assert haskey(dict, o) "The output variable `$o` is not in the configuration"
dict[o] != evaluate_expr(ex.expr, dict) && return 1 # this is the loss!
dict[o] != evaluate_expr(ex.expr, dict) && return spec.weight # this is the loss!
end
return 0
return zero(WT)
end

function symbols(expr::Union{Assignment, BooleanExpr, Circuit})
vars = Symbol[]
extract_symbols!(expr, vars)
return unique!(vars)
end
29 changes: 8 additions & 21 deletions src/models/Coloring.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,10 @@ julia> coloring = Coloring{3}(g) # 3 colors
Coloring{3, Int64, UnitWeight}(SimpleGraph{Int64}(15, [[2, 5, 6], [1, 3, 7], [2, 4, 8], [3, 5, 9], [1, 4, 10], [1, 8, 9], [2, 9, 10], [3, 6, 10], [4, 6, 7], [5, 7, 8]]), [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])

julia> variables(coloring)
10-element Vector{Int64}:
1
2
3
4
5
6
7
8
9
10
1:10

julia> flavors(coloring)
3-element Vector{Int64}:
0
1
2
(0, 1, 2)

julia> is_vertex_coloring(coloring.graph,[1,2,3,1,3,2,1,2,3,1]) #random assignment
false
Expand All @@ -56,8 +43,8 @@ Base.:(==)(a::Coloring, b::Coloring) = a.graph == b.graph && a.weights == b.weig
problem_size(c::Coloring) = (; num_vertices=nv(c.graph), num_edges=ne(c.graph))

# variables interface
variables(gp::Coloring{K}) where K = collect(1:nv(gp.graph))
flavors(::Type{<:Coloring{K}}) where K = collect(0:K-1) # colors
num_variables(gp::Coloring{K}) where K = nv(gp.graph)
flavors(::Type{<:Coloring{K}}) where K = ntuple(i->i-1, K) # colors
num_flavors(::Type{<:Coloring{K}}) where K = K # number of colors

# weights interface
Expand All @@ -66,15 +53,15 @@ set_weights(c::Coloring{K}, weights) where K = Coloring{K}(c.graph, weights)

# constraints interface
@nohard_constraints Coloring
function energy_terms(c::Coloring)
function soft_constraints(c::Coloring)
# constraints on edges
return [LocalConstraint(_vec(e), :coloring) for e in edges(c.graph)]
return [SoftConstraint(_vec(e), :coloring, w) for (w, e) in zip(weights(c), edges(c.graph))]
end

function local_energy(::Type{<:Coloring{K, T}}, spec::LocalConstraint, config) where {K, T}
function local_energy(::Type{<:Coloring{K, T}}, spec::SoftConstraint{WT}, config) where {K, T, WT}
@assert length(config) == num_variables(spec)
a, b = config
return a == b ? one(T) : zero(T)
return a == b ? spec.weight : zero(WT)
end

"""
Expand Down
35 changes: 17 additions & 18 deletions src/models/DominatingSet.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,10 @@ julia> DS = DominatingSet(graph)
DominatingSet{SimpleGraph{Int64}, Int64, UnitWeight}(SimpleGraph{Int64}(4, [[2], [1, 3], [2, 4], [3, 5], [4]]), [1, 1, 1, 1, 1])

julia> variables(DS) # degrees of freedom
5-element Vector{Int64}:
1
2
3
4
5
1:5

julia> flavors(DS) # flavors of the vertices
2-element Vector{Int64}:
0
1
(0, 1)

julia> energy(DS, [0, 1, 0, 1, 0]) # Positive sample: (size) of a dominating set
2
Expand All @@ -60,23 +53,29 @@ end
Base.:(==)(a::DominatingSet, b::DominatingSet) = ( a.graph == b.graph )

# Variables Interface
variables(gp::DominatingSet) = [1:nv(gp.graph)...]
flavors(::Type{<:DominatingSet}) = [0, 1]
num_variables(gp::DominatingSet) = nv(gp.graph)
flavors(::Type{<:DominatingSet}) = (0, 1)
problem_size(c::DominatingSet) = (; num_vertices=nv(c.graph), num_edges=ne(c.graph))

# Weights Interface
weights(c::DominatingSet) = c.weights
set_weights(c::DominatingSet, weights) = DominatingSet(c.graph, weights)

# Constraints Interface
@nohard_constraints DominatingSet
function energy_terms(c::DominatingSet)
function hard_constraints(c::DominatingSet)
return [HardConstraint(vcat(v, neighbors(c.graph, v)), :dominance) for v in vertices(c.graph)]
end
function is_satisfied(::Type{<:DominatingSet}, spec::HardConstraint, config)
@assert length(config) == num_variables(spec)
return count(isone, config) >= 1
end

function soft_constraints(c::DominatingSet)
# constraints on vertex and its neighbours
return [LocalConstraint(vcat(v, neighbors(c.graph, v)), :dominating) for v in vertices(c.graph)]
return [SoftConstraint([v], :num_vertex, w) for (w, v) in zip(weights(c), vertices(c.graph))]
end

function local_energy(::Type{<:DominatingSet{GT, T}}, spec::LocalConstraint, config) where {GT, T}
@assert length(config) == num_variables(spec)
nselect = count(isone, config)
return nselect < 1 ? energy_max(T) : T(first(config))
function local_energy(::Type{<:DominatingSet{GT, T}}, spec::SoftConstraint{WT}, config) where {GT, T, WT}
@assert length(config) == num_variables(spec) == 1
return WT(first(config)) * spec.weight
end
26 changes: 10 additions & 16 deletions src/models/Factoring.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,10 @@ julia> factoring = Factoring(2,2,6)
Factoring(2, 2, 6)

julia> variables(factoring) # return the sum of factors' bit size
4-element Vector{Int64}:
1
2
3
4
1:4

julia> flavors(factoring)
2-element Vector{Int64}:
0
1
(0, 1)

julia> energy(factoring,[0,1,1,1]) # 01 -> 2, 11 -> 3
0
Expand All @@ -41,17 +35,17 @@ struct Factoring <: AbstractProblem
end

# variables interface
variables(f::Factoring) = collect(1:f.m+f.n)
flavors(::Type{Factoring}) = [0, 1]
num_variables(f::Factoring) = f.m+f.n
flavors(::Type{Factoring}) = (0, 1)
problem_size(f::Factoring) = (; num_bits_first=f.m, num_bits_second=f.n)

# utilities
function energy_multi(f::Factoring, configs)
@assert all(config->length(config) == num_variables(f), configs)
return Iterators.map(configs) do config
input1 = BitStr(config[1:f.m]).buf
input2 = BitStr(config[f.m+1:f.m+f.n]).buf
return (input1 * input2 == f.input ? 0 : 1, config)
function energy_eval_byid_multiple(f::Factoring, config_ids)
@assert all(id->length(id) == num_variables(f), config_ids)
return Iterators.map(config_ids) do id
input1 = BitStr(id[1:f.m] .- 1).buf
input2 = BitStr(id[f.m+1:f.m+f.n] .- 1).buf
return (input1 * input2 == f.input ? 0 : 1)
end
end

Expand Down
30 changes: 12 additions & 18 deletions src/models/IndependentSet.jl
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,17 @@ julia> graph = SimpleGraph(Graphs.SimpleEdge.([(1, 2), (1, 3), (3, 4), (2, 3)]))
julia> IS = IndependentSet(graph)
IndependentSet{SimpleGraph{Int64}, Int64, UnitWeight}(SimpleGraph{Int64}(4, [[2, 3], [1, 3], [1, 2, 4], [3]]), [1, 1, 1, 1])

julia> variables(IS) # degrees of freedom
4-element Vector{Int64}:
1
2
3
4
julia> num_variables(IS) # degrees of freedom
4

julia> flavors(IS) # flavors of the vertices
2-element Vector{Int64}:
0
1
(0, 1)

julia> energy(IS, [1, 0, 0, 1]) # Positive sample: -(size) of an independent set
-2

julia> energy(IS, [0, 1, 1, 0]) # Negative sample: 0
3037000500
3037000498

julia> findbest(IS, BruteForce()) # solve the problem with brute force
2-element Vector{Vector{Int64}}:
Expand All @@ -58,8 +52,8 @@ end
Base.:(==)(a::IndependentSet, b::IndependentSet) = a.graph == b.graph && a.weights == b.weights

# Variables Interface
variables(gp::IndependentSet) = [1:nv(gp.graph)...]
flavors(::Type{<:IndependentSet}) = [0, 1]
num_variables(gp::IndependentSet) = nv(gp.graph)
flavors(::Type{<:IndependentSet}) = (0, 1)
problem_size(c::IndependentSet) = (; num_vertices=nv(c.graph), num_edges=ne(c.graph))

# weights interface
Expand All @@ -68,18 +62,18 @@ set_weights(c::IndependentSet, weights) = IndependentSet(c.graph, weights)

# constraints interface
function hard_constraints(c::IndependentSet)
return [LocalConstraint(_vec(e), nothing) for e in edges(c.graph)]
return [HardConstraint(_vec(e), :independence) for e in edges(c.graph)]
end
function is_satisfied(::Type{<:IndependentSet}, spec::LocalConstraint, config)
function is_satisfied(::Type{<:IndependentSet}, spec::HardConstraint, config)
@assert length(config) == num_variables(spec)
return count(!iszero, config) <= 1
end

function energy_terms(c::IndependentSet)
return [LocalConstraint([i], nothing) for i in 1:nv(c.graph)]
function soft_constraints(c::IndependentSet)
return [SoftConstraint([i], :num_vertex, w) for (w, i) in zip(weights(c), 1:nv(c.graph))]
end

function local_energy(::Type{<:IndependentSet{GT, T}}, spec::LocalConstraint, config) where {GT, T}
function local_energy(::Type{<:IndependentSet{GT, T}}, spec::SoftConstraint{WT}, config) where {GT, T, WT}
@assert length(config) == num_variables(spec) == 1
return T(-first(config))
return WT(-first(config)) * spec.weight
end
Loading
Loading