Unit Disk Mapping
Generic Unweighted Mapping
The generic unweighted mapping aims to reduce a generic unweighted Maximum Independent Set (MIS) problem to one on a defected King's graph. Check our paper (link to be added) for the mapping scheme.
Let the source graph be the Petersen graph.
using UnitDiskMapping, Graphs, GenericTensorNetworks, LinearAlgebraVisualization setup. To make the plots dark-mode friendly, we use white-background color.
using LuxorGraphPlot.Luxor, LuxorGraphPlot
graph = smallgraph(:petersen)
LuxorGraphPlot.show_graph(graph)We can use the map_graph function to map the unweighted MIS problem on the Petersen graph to one on a defected King's graph.
unweighted_res = map_graph(graph; vertex_order=MinhThiTrick());Here, the keyword argument vertex_order can be a vector of vertices in a specified order, or the method to compute the path decomposition that generates an order. The MinhThiTrick() method is an exact path decomposition solver, which is suited for small graphs (where number of vertices <= 50). The Greedy() method finds the vertex order much faster and works in all cases, but may not be optimal. A good vertex order can reduce the depth of the mapped graph.
The return value contains the following fields:
fieldnames(unweighted_res |> typeof)(:grid_graph, :lines, :padding, :mapping_history, :mis_overhead)The field grid_graph is the mapped grid graph.
LuxorGraphPlot.show_graph(unweighted_res.grid_graph)
unweighted_res.grid_graph.size(29, 41)The field lines is a vector of copy gadgets arranged in a ⊢ shape. These copy gadgets form a crossing lattice, in which two copy lines cross each other whenever their corresponding vertices in the source graph are connected by an edge.
vslot
↓
| ← vstart
|
|------- ← hslot
| ↑ ← vstop
hstopunweighted_res.lines10-element Vector{UnitDiskMapping.CopyLine}:
UnitDiskMapping.CopyLine 10: vslot → [1:1,1], hslot → [1,1:6]
UnitDiskMapping.CopyLine 9: vslot → [2:2,2], hslot → [2,2:7]
UnitDiskMapping.CopyLine 8: vslot → [1:3,3], hslot → [3,3:8]
UnitDiskMapping.CopyLine 7: vslot → [1:4,4], hslot → [4,4:9]
UnitDiskMapping.CopyLine 6: vslot → [2:5,5], hslot → [5,5:10]
UnitDiskMapping.CopyLine 5: vslot → [1:6,6], hslot → [6,6:10]
UnitDiskMapping.CopyLine 4: vslot → [1:6,7], hslot → [1,7:8]
UnitDiskMapping.CopyLine 3: vslot → [1:3,8], hslot → [2,8:9]
UnitDiskMapping.CopyLine 2: vslot → [1:4,9], hslot → [1,9:10]
UnitDiskMapping.CopyLine 1: vslot → [1:6,10], hslot → [2,10:10]The field mapping_history contains the rewrite rules applied to the crossing lattice. They contain important information for mapping a solution back.
unweighted_res.mapping_history36-element Vector{Tuple{Pattern, Int64, Int64}}:
(⋅ ● ⋅ ⋅
⋅ ● ● ⋅
⋅ ● ● ⋅
⋅ ● ⋅ ⋅
↓
⋅ ● ⋅ ⋅
⋅ ● ⋅ ⋅
⋅ ● ⋅ ⋅
⋅ ● ⋅ ⋅ , 7, 38)
(◆ ⋅
⋅ ◆
↓
● ⋅
⋅ ● , 4, 38)
(⋅ ◆
◆ ⋅
↓
⋅ ●
● ⋅ , 23, 38)
(⋅ ◆ ⋅ ⋅
◆ ● ⋅ ⋅
⋅ ● ⋅ ⋅
↓
⋅ ● ⋅ ⋅
● ⋅ ● ⋅
⋅ ● ⋅ ⋅ , 19, 38)
(⋅ ⋅ ⋅ ⋅
⋅ ⋅ ● ●
⋅ ● ● ⋅
⋅ ● ⋅ ⋅
↓
⋅ ⋅ ⋅ ⋅
⋅ ⋅ ⋅ ●
⋅ ⋅ ● ⋅
⋅ ● ⋅ ⋅ , 3, 34)
(⋅ ◆ ⋅ ⋅
◆ ● ⋅ ⋅
⋅ ● ⋅ ⋅
↓
⋅ ● ⋅ ⋅
● ⋅ ● ⋅
⋅ ● ⋅ ⋅ , 7, 34)
(⋅ ◆
◆ ⋅
↓
⋅ ●
● ⋅ , 15, 34)
(⋅ ● ⋅ ⋅
⋅ ● ⋅ ⋅
⋅ ● ● ●
⋅ ● ● ⋅
⋅ ● ⋅ ⋅
↓
⋅ ● ⋅ ⋅
⋅ ⋅ ● ⋅
⋅ ● ⋅ ●
⋅ ⋅ ● ⋅
⋅ ● ⋅ ⋅ , 6, 30)
(◆ ⋅
⋅ ◆
↓
● ⋅
⋅ ● , 4, 30)
(⋅ ◆
◆ ⋅
↓
⋅ ●
● ⋅ , 11, 30)
⋮
(⋅ ⋅ ● ⋅ ⋅
● ● ◉ ● ●
⋅ ⋅ ● ⋅ ⋅
⋅ ⋅ ● ⋅ ⋅
↓
⋅ ⋅ ● ⋅ ⋅
● ● ● ● ●
⋅ ● ● ● ⋅
⋅ ⋅ ● ⋅ ⋅ , 11, 13)
(⋅ ◆ ⋅
◆ ◉ ●
⋅ ● ⋅
↓
⋅ ● ⋅
● ● ●
⋅ ● ⋅ , 7, 14)
(⋅ ⋅ ⋅
⋅ ⋅ ⋅
◆ ● ●
⋅ ◆ ⋅
↓
⋅ ⋅ ⋅
⋅ ● ⋅
● ⋅ ●
⋅ ● ⋅ , 2, 14)
(⋅ ● ⋅ ⋅
⋅ ● ⋅ ⋅
⋅ ● ● ●
⋅ ⋅ ⋅ ⋅
↓
⋅ ● ⋅ ⋅
⋅ ⋅ ● ⋅
⋅ ⋅ ⋅ ●
⋅ ⋅ ⋅ ⋅ , 10, 10)
(⋅ ⋅ ● ⋅ ⋅
● ● ◉ ● ●
⋅ ⋅ ● ⋅ ⋅
⋅ ⋅ ● ⋅ ⋅
↓
⋅ ⋅ ● ⋅ ⋅
● ● ● ● ●
⋅ ● ● ● ⋅
⋅ ⋅ ● ⋅ ⋅ , 7, 9)
(⋅ ⋅ ⋅
⋅ ⋅ ⋅
◆ ● ●
⋅ ◆ ⋅
↓
⋅ ⋅ ⋅
⋅ ● ⋅
● ⋅ ●
⋅ ● ⋅ , 2, 10)
(⋅ ⋅ ⋅ ⋅
⋅ ● ● ●
⋅ ⋅ ⋅ ⋅
↓
⋅ ⋅ ⋅ ⋅
⋅ ⋅ ⋅ ●
⋅ ⋅ ⋅ ⋅ , 3, 3)
(⋅ ⋅ ⋅ ⋅
⋅ ● ● ●
⋅ ⋅ ⋅ ⋅
↓
⋅ ⋅ ⋅ ⋅
⋅ ⋅ ⋅ ●
⋅ ⋅ ⋅ ⋅ , 3, 5)
(⋅ ⋅ ⋅ ⋅
⋅ ● ● ●
⋅ ⋅ ⋅ ⋅
↓
⋅ ⋅ ⋅ ⋅
⋅ ⋅ ⋅ ●
⋅ ⋅ ⋅ ⋅ , 3, 7)The field mis_overhead is the difference between $\alpha(G_M) - \alpha(G_S)$, where $G_M$ and $G_S$ are the mapped and source graph.
unweighted_res.mis_overhead88We can solve the mapped graph with GenericTensorNetworks.
res = solve(GenericTensorNetwork(IndependentSet(SimpleGraph(unweighted_res.grid_graph))), SingleConfigMax())[](92.0, GenericTensorNetworks.ConfigSampler{218, 1, 4}(01000110101000001110000110101010100000111100000111101010100000001110100001000101101000101010001000010001011010001001010110010101000100010001001010111000001111000100011111000011110001010100011110001110101010101010100101))ₜYou might want to read the documentation page of GenericTensorNetworks for a detailed explanation on this function. Here, we just visually check the solution configuration.
show_config(unweighted_res.grid_graph, res.c.data)By mapping the result back, we get a solution for the original Petersen graph. Its maximum independent set size is 4.
The solution obtained by solving the mapped graph
original_configs = map_config_back(unweighted_res, res.c.data)10-element Vector{Int64}:
0
0
1
0
1
1
1
0
0
0Confirm that the solution from the mapped graph gives us a maximum independent set for the original graph
UnitDiskMapping.is_independent_set(graph, original_configs)trueGeneric Weighted Mapping
A Maximum Weight Independent Set (MWIS) problem on a general graph can be mapped to one on the defected King's graph. The first step is to do the same mapping as above but adding a new positional argument Weighted() as the first argument of map_graph. Let us still use the Petersen graph as an example.
weighted_res = map_graph(Weighted(), graph; vertex_order=MinhThiTrick());The return value is similar to that for the unweighted mapping generated above, except each node in the mapped graph can have a weight 1, 2 or 3. Note here, we haven't added the weights in the original graph.
show_grayscale(weighted_res.grid_graph)The "pins" of the mapped graph have a one-to-one correspondence to the vertices in the source graph.
show_pins(weighted_res)The weights in the original graph can be added to the pins of this grid graph using the map_weights function. The added weights must be smaller than 1.
source_weights = rand(10)
mapped_weights = map_weights(weighted_res, source_weights)218-element Vector{Float64}:
1.3043397509738668
2.0
1.3337215554656183
2.0
2.0
2.0
1.0
2.0
2.0
2.0
⋮
2.0
2.0
2.0
2.0
2.0
2.0
2.0
1.0
2.0Now that we have both the graph and the weights, let us solve the mapped problem!
wmap_config = let
graph, _ = graph_and_weights(weighted_res.grid_graph)
collect(Int,
solve(GenericTensorNetwork(IndependentSet(graph, mapped_weights)), SingleConfigMax())[].c.data
)
end
show_config(weighted_res.grid_graph, wmap_config)By reading the configurations of the pins, we obtain a solution for the source graph.
The solution obtained by solving the mapped graph
map_config_back(weighted_res, wmap_config)10-element Vector{Int64}:
1
0
0
1
0
0
1
1
0
0Directly solving the source graph
collect(Int,
solve(GenericTensorNetwork(IndependentSet(graph, source_weights)), SingleConfigMax())[].c.data
)10-element Vector{Int64}:
1
0
0
1
0
0
1
1
0
0QUBO problem
Generic QUBO mapping
A QUBO problem can be specified as the following energy model:
\[E(z) = -\sum_{i<j} J_{ij} z_i z_j + \sum_i h_i z_i\]
n = 6
J = triu(randn(n, n) * 0.001, 1); J += J'
h = randn(n) * 0.0016-element Vector{Float64}:
-0.0004617706724297522
-0.0010288812841391364
0.0004385477824202696
0.0006200745685316863
0.0012349430275641802
-0.0009189170527576681Now, let us do the mapping on an $n \times n$ crossing lattice.
qubo = UnitDiskMapping.map_qubo(J, h);The mapping result contains two fields, the grid_graph and the pins. After finding the ground state of the mapped independent set problem, the configuration of the spin glass can be read directly from the pins. The following graph plots the pins in red color.
qubo_graph, qubo_weights = UnitDiskMapping.graph_and_weights(qubo.grid_graph)
show_pins(qubo)One can also check the weights using the gray-scale plot.
show_grayscale(qubo.grid_graph)By solving this maximum independent set problem, we will get the following configuration.
qubo_mapped_solution = collect(Int, solve(GenericTensorNetwork(IndependentSet(qubo_graph, qubo_weights)), SingleConfigMax())[].c.data)
show_config(qubo.grid_graph, qubo_mapped_solution)This solution can be mapped to a solution for the source graph by reading the configurations on the pins.
The solution obtained by solving the mapped graph
map_config_back(qubo, collect(Int, qubo_mapped_solution))6-element Vector{Int64}:
1
0
0
1
0
1This solution is consistent with the exact solution:
Directly solving the source graph, due to the convention issue, we flip the signs of J and h
collect(Int, solve(GenericTensorNetwork(spin_glass_from_matrix(-J, -h)), SingleConfigMax())[].c.data)6-element Vector{Int64}:
0
1
1
0
1
0QUBO problem on a square lattice
We define some coupling strengths and onsite energies on a $n \times n$ square lattice.
square_coupling = [[(i,j,i,j+1,0.01*randn()) for i=1:n, j=1:n-1]...,
[(i,j,i+1,j,0.01*randn()) for i=1:n-1, j=1:n]...];
square_onsite = vec([(i, j, 0.01*randn()) for i=1:n, j=1:n]);Then we use map_qubo_square to reduce the QUBO problem on a square lattice to the MIS problem on a grid graph.
qubo_square = UnitDiskMapping.map_qubo_square(square_coupling, square_onsite);
show_grayscale(qubo_square.grid_graph)You can see each coupling is replaced by the following XOR gadget
show_grayscale(UnitDiskMapping.gadget_qubo_square(Int), texts=["x$('₀'+i)" for i=1:8])Where dark nodes have weight 2 and light nodes have weight 1. It corresponds to the boolean equation $x_8 = \neg (x_1 \veebar x_5)$; hence we can add ferromagnetic couplings as negative weights and anti-ferromagnetic couplings as positive weights. On-site terms are added directly to the pins.
show_pins(qubo_square)Let us solve the independent set problem on the mapped graph.
square_graph, square_weights = UnitDiskMapping.graph_and_weights(qubo_square.grid_graph);
config_square = collect(Int, solve(GenericTensorNetwork(IndependentSet(square_graph, square_weights)), SingleConfigMax())[].c.data);We will get the following configuration.
show_config(qubo_square.grid_graph, config_square)By reading out the configurations at pins, we can get a solution of the source QUBO problem.
r1 = map_config_back(qubo_square, config_square)36-element Vector{Int64}:
1
1
1
0
1
0
1
1
0
0
⋮
1
0
0
1
1
0
1
1
0It can be easily checked by examining the exact result.
let
# solve QUBO directly
g2 = SimpleGraph(n*n)
Jd = Dict{Tuple{Int,Int}, Float64}()
for (i,j,i2,j2,J) in square_coupling
edg = (i+(j-1)*n, i2+(j2-1)*n)
Jd[edg] = J
add_edge!(g2, edg...)
end
Js, hs = Float64[], zeros(Float64, nv(g2))
for e in edges(g2)
push!(Js, Jd[(e.src, e.dst)])
end
for (i,j,h) in square_onsite
hs[i+(j-1)*n] = h
end
collect(Int, solve(GenericTensorNetwork(SpinGlass(g2, -Js, -hs)), SingleConfigMax())[].c.data)
end36-element Vector{Int64}:
0
0
0
1
0
1
0
0
1
1
⋮
0
1
1
0
0
1
0
0
1Factorization problem
The building block of the array multiplier can be mapped to the following gadget:
let
graph, pins = UnitDiskMapping.multiplier()
texts = fill("", length(graph.nodes))
texts[pins] .= ["x$i" for i=1:length(pins)]
show_grayscale(graph)
endLet us denote the input and output pins as $x_{1-8} \in \{0, 1\}$. The above gadget implements the following equations:
\[\begin{align} x_1 + x_2 x_3 + x_4 = x_5 + 2 x_7,\\ x_2 = x_6,\\ x_3 = x_8. \end{align}\]
One can call map_factoring(M, N) to map a factoring problem to the array multiplier grid of size (M, N). In the following example of (2, 2) array multiplier, the input integers are $p = 2p_2+p_1$ and $q= 2q_2+q_1$, and the output integer is $m = 4m_3+2m_2+m_1$. The maximum independent set corresponds to the solution of $pq = m$
mres = UnitDiskMapping.map_factoring(2, 2);
show_pins(mres)To solve this factoring problem, one can use the following statement:
multiplier_output = UnitDiskMapping.solve_factoring(mres, 6) do g, ws
collect(Int, solve(GenericTensorNetwork(IndependentSet(g, ws)), SingleConfigMax())[].c.data)
end(3, 2)This function consists of the following steps:
- We first modify the graph by inspecting the fixed values, i.e., the output
mand0s:- If a vertex is fixed to 1, remove it and its neighbors,
- If a vertex is fixed to 0, remove this vertex.
The resulting grid graph is
mapped_grid_graph, remaining_vertices = let
g, ws = graph_and_weights(mres.grid_graph)
mg, vmap = UnitDiskMapping.set_target(g, [mres.pins_zeros..., mres.pins_output...], 6 << length(mres.pins_zeros))
GridGraph(mres.grid_graph.size, mres.grid_graph.nodes[vmap], mres.grid_graph.radius), vmap
end;
show_graph(mapped_grid_graph)- Then, we solve this new grid graph.
config_factoring6 = let
mg, mw = graph_and_weights(mapped_grid_graph)
solve(GenericTensorNetwork(IndependentSet(mg, mw)), SingleConfigMax())[].c.data
end;
show_config(mapped_grid_graph, config_factoring6)- It is straightforward to read out the results from the above configuration. The solution should be either (2, 3) or (3, 2).
let
cfg = zeros(Int, length(mres.grid_graph.nodes))
cfg[remaining_vertices] .= config_factoring6
bitvectors = cfg[mres.pins_input1], cfg[mres.pins_input2]
UnitDiskMapping.asint.(bitvectors)
end(2, 3)Logic Gates
- NOT gate: $y_1 =\neg x_1$
show_pins(Gate(:NOT))show_grayscale(gate_gadget(Gate(:NOT))[1], wmax=2)- NXOR gate: $y_1 =\neg (x_1 \veebar x_2)$. Notice this negated XOR gate is used in the square lattice QUBO mapping.
show_pins(Gate(:NXOR))show_grayscale(gate_gadget(Gate(:NXOR))[1], wmax=2)- NOR gate: $y_1 =\neg (x_1 \vee x_2)$
show_pins(Gate(:NOR))show_grayscale(gate_gadget(Gate(:NOR))[1], wmax=2)- AND gate: $y_1 =x_1 \wedge x_2$
show_pins(Gate(:AND))show_grayscale(gate_gadget(Gate(:AND))[1], wmax=2)Since most logic gates have 3 pins, it is natural to embed a circuit to a 3D unit disk graph by taking the z direction as the time. In a 2D grid, one needs to do the general weighted mapping in order to create a unit disk boolean circuit.
This page was generated using Literate.jl.