Skip to content

Commit 4ce1b4e

Browse files
authored
Finding minimum sets (#15)
* finding minimum sets * fix tests
1 parent 711ae40 commit 4ce1b4e

File tree

13 files changed

+227
-45
lines changed

13 files changed

+227
-45
lines changed

docs/src/ref.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ nflavor
3434
is_independent_set
3535
mis_compactify!
3636
37+
is_maximal_independent_set
38+
3739
cut_size
3840
cut_assign
3941
@@ -48,10 +50,13 @@ is_good_vertex_coloring
4850
SizeMax
4951
CountingAll
5052
CountingMax
53+
CountingMin
5154
GraphPolynomial
5255
SingleConfigMax
56+
SingleConfigMin
5357
ConfigsAll
5458
ConfigsMax
59+
ConfigsMin
5560
```
5661

5762
## Element Algebras

examples/IndependentSet.jl

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,21 @@ timespacereadwrite_complexity(problem)
6262
# ### Maximum independent set size ``\alpha(G)``
6363
maximum_independent_set_size = solve(problem, SizeMax())[]
6464

65+
# Function [`solve`](@ref) takes two positional arguments, the problem instance and the wanted property.
66+
# Here [`SizeMax`](@ref) means finding the solution with maximum set size.
67+
6568
# ### Counting properties
6669
# ##### counting all independent sets
6770
count_all_independent_sets = solve(problem, CountingAll())[]
6871

69-
# Function [`solve`](@ref) takes two positional arguments, the problem instance and the wanted property.
72+
# Here [`CountingAll`](@ref) means finding all possible solutions.
7073

7174
# ##### counting independent sets with sizes ``\alpha(G)`` and ``\alpha(G)-1``
7275
count_max2_independent_sets = solve(problem, CountingMax(2))[]
7376

77+
# Here [`CountingMax`](@ref) means finding solutions with largest-2 sizes,
78+
# the positional argument has default value `1`.
79+
7480
# ##### independence polynomial
7581
# The graph polynomial for the independent set problem is known as the independence polynomial.
7682
# ```math
@@ -85,7 +91,7 @@ independence_polynomial = solve(problem, GraphPolynomial(; method=:finitefield))
8591

8692
# ### Configuration properties
8793
# ##### finding one maximum independent set (MIS)
88-
# There are two approaches to find one of the best solution.
94+
# One can use [`SingleConfigMax`](@ref) to find one of the solution with largest set size, and it has two implementations.
8995
# The unbounded (default) version uses [`ConfigSampler`](@ref) to sample one of the best solutions directly.
9096
# The bounded version uses the binary gradient back-propagation (see our paper) to compute the gradients.
9197
# It requires caching intermediate states, but is often faster (on CPU) because it can use [`TropicalGEMM`](https://github.com/TensorBFS/TropicalGEMM.jl) (see [Performance Tips](@ref)).
@@ -98,7 +104,8 @@ show_graph(graph; locs=locations, vertex_colors=
98104
[iszero(max_config.c.data[i]) ? "white" : "red" for i=1:nv(graph)])
99105

100106
# ##### enumeration of all MISs
101-
# There are two approaches to enumerate all best-K solutions.
107+
# One can use [`ConfigsMax`](@ref) to find all solutions with largest-K set sizes.
108+
# It also has two implementations.
102109
# The bounded (default) version is always prefered because it can significantly use the memory usage.
103110
all_max_configs = solve(problem, ConfigsMax(1; bounded=true))[]
104111

@@ -117,6 +124,7 @@ Compose.compose(context(),
117124
ntuple(k->(context((k-1)/m, 0.0, 1.2/m, 1.0), imgs[k]), m)...)
118125

119126
# ##### enumeration of all IS configurations
127+
# One can use [`ConfigsAll`](@ref) to enumerate all sets satisfying the problem constraint.
120128
all_independent_sets = solve(problem, ConfigsAll())[]
121129

122130
# To save/read a set of configuration to disk, one can type the following
@@ -130,6 +138,14 @@ loaded_sets = load_configs(filename; format=:binary, bitlength=10)
130138
# When loading data, one needs to provide the `bitlength` if the data is saved in binary format.
131139
# Because the bitstring length is not stored.
132140

141+
# !!! note
142+
# Check section [Maximal independent set problem](@ref) for examples of finding graph properties related to minimum sizes:
143+
# * [`SizeMin`](@ref) for enumerating minimum set size,
144+
# * [`CountingMin`](@ref) for enumerating minimum set size,
145+
# * [`SingleConfigMin`](@ref) for enumerating minimum set size,
146+
# * [`ConfigsMin`](@ref) for enumerating minimum set size,
147+
148+
133149
# ## Weights and open vertices
134150
# [`IndependentSet`](@ref) accepts weights as a key word argument.
135151
# The following code computes the weighted MIS problem.

examples/MaximalIS.jl

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,24 @@ timespacereadwrite_complexity(problem)
5757
# ```
5858
# where ``b_k`` is the number of maximal independent sets of size ``k`` in graph ``G=(V, E)``.
5959

60-
max_config = solve(problem, GraphPolynomial())[]
60+
maximal_indenpendence_polynomial = solve(problem, GraphPolynomial())[]
6161

6262
# One can see the first several coefficients are 0, because it only counts the maximal independent sets,
63+
# The minimum maximal independent set size is also known as the independent domination number.
64+
# It can be computed with the [`SizeMin`](@ref) property:
65+
independent_domination_number = solve(problem, SizeMin())[]
66+
67+
# Similarly, we have its counting [`CountingMin`](@ref):
68+
counting_min_maximal_independent_set = solve(problem, CountingMin())[]
6369

6470
# ### Configuration properties
6571
# ##### finding all maximal independent set
6672
maximal_configs = solve(problem, ConfigsAll())[]
6773

74+
all(c->is_maximal_independent_set(g, i), maximal_configs)
75+
76+
#
77+
6878
imgs = ntuple(k->show_graph(graph;
6979
locs=locations, scale=0.25,
7080
vertex_colors=[iszero(maximal_configs[k][i]) ? "white" : "red"
@@ -76,4 +86,19 @@ Compose.set_default_graphic_size(18cm, 12cm); Compose.compose(context(),
7686
# This result should be consistent with that given by the [Bron Kerbosch algorithm](https://en.wikipedia.org/wiki/Bron%E2%80%93Kerbosch_algorithm) on the complement of Petersen graph.
7787
cliques = maximal_cliques(complement(graph))
7888

79-
# For sparse graphs, the generic tensor network approach is usually much faster and memory efficient than the Bron Kerbosch algorithm.
89+
# For sparse graphs, the generic tensor network approach is usually much faster and memory efficient than the Bron Kerbosch algorithm.
90+
91+
# ##### finding minimum maximal independent set
92+
# It is the [`ConfigsMin`](@ref) property in the program.
93+
minimum_maximal_configs = solve(problem, ConfigsMin())[]
94+
95+
imgs2 = ntuple(k->show_graph(graph;
96+
locs=locations, scale=0.25,
97+
vertex_colors=[iszero(minimum_maximal_configs[k][i]) ? "white" : "red"
98+
for i=1:nv(graph)]), length(minimum_maximal_configs));
99+
100+
Compose.set_default_graphic_size(15cm, 12cm); Compose.compose(context(),
101+
ntuple(k->(context((mod1(k,4)-1)/4, ((k-1)÷5)/3, 1.2/4, 1.0/3), imgs2[k]), 10)...)
102+
103+
# Similarly, if one is only interested in computing one of the minimum sets,
104+
# one can use the graph property [`SingleConfigMin`](@ref).

src/GraphTensorNetworks.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export bestk_solutions
2424
export contractx, contractf, graph_polynomial, max_size, max_size_count
2525

2626
# Graphs
27-
export random_regular_graph, diagonal_coupled_graph, is_independent_set
27+
export random_regular_graph, diagonal_coupled_graph, is_independent_set, is_maximal_independent_set
2828
export square_lattice_graph, unit_disk_graph, random_diagonal_coupled_graph, random_square_lattice_graph
2929
export line_graph
3030

@@ -35,7 +35,7 @@ export mis_compactify!, cut_assign, cut_size, num_paint_shop_color_switch, paint
3535
export is_good_vertex_coloring
3636

3737
# Interfaces
38-
export solve, SizeMax, CountingAll, CountingMax, GraphPolynomial, SingleConfigMax, ConfigsAll, ConfigsMax
38+
export solve, SizeMax, SizeMin, CountingAll, CountingMax, CountingMin, GraphPolynomial, SingleConfigMax, SingleConfigMin, ConfigsAll, ConfigsMax, ConfigsMin
3939

4040
# Utilities
4141
export save_configs, load_configs

src/arithematics.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ struct ConfigEnumerator{N,S,C}
193193
end
194194

195195
Base.length(x::ConfigEnumerator{N}) where N = length(x.data)
196+
Base.iterate(x::ConfigEnumerator{N}) where N = iterate(x.data)
197+
Base.iterate(x::ConfigEnumerator{N}, state) where N = iterate(x.data, state)
196198
Base.getindex(x::ConfigEnumerator, i) = x.data[i]
197199
Base.:(==)(x::ConfigEnumerator{N,S,C}, y::ConfigEnumerator{N,S,C}) where {N,S,C} = Set(x.data) == Set(y.data)
198200

src/bounding.jl

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -155,29 +155,36 @@ function solution_ad(code::Union{NestedEinsum,SlicedEinsum}, @nospecialize(xsa),
155155
# compute masks from cached tensors
156156
@debug "generating masked tree..."
157157
mt = generate_masktree(SingleConfig(), code, c, ymask, size_dict)
158-
n, read_config!(code, mt, Dict())
158+
config = read_config!(code, mt, Dict())
159+
if length(config) !== length(labels(code)) # equal to the # of degree of freedoms
160+
error("configuration `$(config)` is not fully determined!")
161+
end
162+
n, config
159163
end
160164

161165
# get the solution configuration from gradients.
162166
function read_config!(code::SlicedEinsum, mt, out)
163167
read_config!(code.eins, mt, out)
164168
end
169+
165170
function read_config!(code::NestedEinsum, mt, out)
166171
for (arg, ix, sibling) in zip(code.args, getixs(code.eins), mt.siblings)
167172
if OMEinsum.isleaf(arg)
168-
assign = convert(Array, sibling.content) # note: the content can be CuArray
169-
if length(ix) == 1
170-
if !assign[1] && assign[2]
171-
out[ix[1]] = 1
172-
elseif !assign[2] && assign[1]
173-
out[ix[1]] = 0
174-
else
175-
error("invalid assign $(assign)")
173+
mask = convert(Array, sibling.content) # note: the content can be CuArray
174+
for ci in CartesianIndices(mask)
175+
if mask[ci]
176+
for k in 1:ndims(mask)
177+
if haskey(out, ix[k])
178+
@assert out[ix[k]] == ci.I[k] - 1
179+
else
180+
out[ix[k]] = ci.I[k] - 1
181+
end
182+
end
176183
end
177184
end
178185
else # nested
179186
read_config!(arg, sibling, out)
180187
end
181188
end
182189
return out
183-
end
190+
end

src/configurations.jl

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,37 @@
11
"""
2-
best_solutions(problem; all=false, usecuda=false)
2+
best_solutions(problem; all=false, usecuda=false, invert=false)
33
44
Find optimal solutions with bounding.
55
66
* When `all` is true, the program will use set for enumerate all possible solutions, otherwise, it will return one solution for each size.
77
* `usecuda` can not be true if you want to use set to enumerate all possible solutions.
8+
* If `invert` is true, find the minimum.
89
"""
9-
function best_solutions(gp::GraphProblem; all=false, usecuda=false)
10+
function best_solutions(gp::GraphProblem; all=false, usecuda=false, invert=false)
1011
if all && usecuda
1112
throw(ArgumentError("ConfigEnumerator can not be computed on GPU!"))
1213
end
13-
xst = generate_tensors(l->TropicalF64.(get_weights(gp, l)), gp)
14+
xst = generate_tensors(l->(vs = TropicalF64.(get_weights(gp, l)); invert ? pre_invert_exponent.(vs) : vs), gp)
1415
ymask = trues(fill(2, length(getiyv(gp.code)))...)
1516
if usecuda
1617
xst = CuArray.(xst)
1718
ymask = CuArray(ymask)
1819
end
1920
if all
2021
# we use `Float64` types because we want to support weighted graphs.
21-
xs = generate_tensors(fx_solutions(gp, CountingTropical{Float64,Float64}, all), gp)
22-
return bounding_contract(AllConfigs{1}(), gp.code, xst, ymask, xs)
22+
xs = generate_tensors(fx_solutions(gp, CountingTropical{Float64,Float64}, all, invert), gp)
23+
ret = bounding_contract(AllConfigs{1}(), gp.code, xst, ymask, xs)
24+
return invert ? post_invert_exponent.(ret) : ret
2325
else
2426
@assert ndims(ymask) == 0
2527
t, res = solution_ad(gp.code, xst, ymask)
26-
return fill(CountingTropical(asscalar(t).n, ConfigSampler(StaticBitVector(map(l->res[l], 1:length(res))))))
28+
ret = fill(CountingTropical(asscalar(t).n, ConfigSampler(StaticBitVector(map(l->res[l], 1:length(res))))))
29+
return invert ? post_invert_exponent.(ret) : ret
2730
end
2831
end
2932

3033
"""
31-
solutions(problem, basetype; all, usecuda=false)
34+
solutions(problem, basetype; all, usecuda=false, invert=false)
3235
3336
General routine to find solutions without bounding,
3437
@@ -39,25 +42,27 @@ General routine to find solutions without bounding,
3942
* When `all` is true, the program will use set for enumerate all possible solutions, otherwise, it will return one solution for each size.
4043
* `usecuda` can not be true if you want to use set to enumerate all possible solutions.
4144
"""
42-
function solutions(gp::GraphProblem, ::Type{BT}; all, usecuda=false) where BT
45+
function solutions(gp::GraphProblem, ::Type{BT}; all::Bool, usecuda::Bool=false, invert::Bool=false) where BT
4346
if all && usecuda
4447
throw(ArgumentError("ConfigEnumerator can not be computed on GPU!"))
4548
end
46-
return contractf(fx_solutions(gp, BT, all), gp; usecuda=usecuda)
49+
ret = contractf(fx_solutions(gp, BT, all, invert), gp; usecuda=usecuda)
50+
return invert ? post_invert_exponent.(ret) : ret
4751
end
4852

4953
"""
50-
best2_solutions(problem; all=true, usecuda=false)
54+
best2_solutions(problem; all=true, usecuda=false, invert=false)
5155
5256
Finding optimal and suboptimal solutions.
5357
"""
54-
best2_solutions(gp::GraphProblem; all=true, usecuda=false) = solutions(gp, Max2Poly{Float64,Float64}; all=all, usecuda=usecuda)
58+
best2_solutions(gp::GraphProblem; all=true, usecuda=false, invert::Bool=false) = solutions(gp, Max2Poly{Float64,Float64}; all, usecuda, invert)
5559

56-
function bestk_solutions(gp::GraphProblem, k::Int)
60+
function bestk_solutions(gp::GraphProblem, k::Int; invert::Bool=false)
5761
xst = generate_tensors(l->TropicalF64.(get_weights(gp, l)), gp)
5862
ymask = trues(fill(2, length(getiyv(gp.code)))...)
59-
xs = generate_tensors(fx_solutions(gp, TruncatedPoly{k,Float64,Float64}, true), gp)
60-
return bounding_contract(AllConfigs{k}(), gp.code, xst, ymask, xs)
63+
xs = generate_tensors(fx_solutions(gp, TruncatedPoly{k,Float64,Float64}, true, invert), gp)
64+
ret = bounding_contract(AllConfigs{k}(), gp.code, xst, ymask, xs)
65+
return invert ? post_invert_exponent.(ret) : ret
6166
end
6267

6368
"""
@@ -69,12 +74,13 @@ e.g. when the problem is [`MaximalIS`](@ref), it computes all maximal independen
6974
all_solutions(gp::GraphProblem) = solutions(gp, Polynomial{Float64,:x}, all=true, usecuda=false)
7075

7176
# return a mapping from label to onehot bitstrings (degree of freedoms).
72-
function fx_solutions(gp::GraphProblem, ::Type{BT}, all::Bool) where {BT}
77+
function fx_solutions(gp::GraphProblem, ::Type{BT}, all::Bool, invert::Bool) where {BT}
7378
syms = symbols(gp)
7479
T = (all ? set_type : sampler_type)(BT, length(syms), nflavor(gp))
7580
vertex_index = Dict([s=>i for (i, s) in enumerate(syms)])
7681
return function (l)
77-
_onehotv.(Ref(T), vertex_index[l], flavors(gp), get_weights(gp, l))
82+
ret = _onehotv.(Ref(T), vertex_index[l], flavors(gp), get_weights(gp, l))
83+
invert ? pre_invert_exponent.(ret) : ret
7884
end
7985
end
8086
function _onehotv(::Type{Polynomial{BS,X}}, x, v, w) where {BS,X}

0 commit comments

Comments
 (0)