Skip to content
Open
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
3 changes: 2 additions & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ makedocs(;
"Home" => "index.md",
"Examples" => [
"generated/tutorial.md",
"generated/unweighted.md"
"generated/unweighted.md",
"generated/weighted_lattice_comparison.md"
],
"Reference" => "ref.md",
],
Expand Down
102 changes: 102 additions & 0 deletions examples/weighted_lattice_comparison.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# # Weighted Mapping on Different Lattices

# This page demonstrates weighted version of the Maximum Independent Set (MIS) or Maximum Weighted Independent Set (MWIS) mapping techniques from the paper "Embedding computationally hard problems in triangular Rydberg atom arrays". We compare two mapping approaches using the K₂,₃ bipartite graph as an example:
#
# 1. **King's Subgraph (KSG) mapping** on square lattices - the traditional approach
# 2. **Triangular lattice mapping** - a more experimental-promising alternative
#
# Both methods preserve the optimal solution while enabling implementation on quantum hardware with different connectivity constraints.

using UnitDiskMapping, Graphs, GenericTensorNetworks

# ## Problem Setup: K₂,₃ Graph

# We use the complete bipartite graph K₂,₃ as our test case. This graph has two groups of vertices: {1,2} and {3,4,5}, where every vertex in the first group connects to every vertex in the second group.

k23_graph = SimpleGraph(5)
add_edge!(k23_graph, 1, 3)
add_edge!(k23_graph, 1, 4)
add_edge!(k23_graph, 1, 5)
add_edge!(k23_graph, 2, 3)
add_edge!(k23_graph, 2, 4)
add_edge!(k23_graph, 2, 5)

show_graph(k23_graph)

# For the MIS problem, we assign equal weights to all vertices.
source_weights = [0.5, 0.5, 0.5, 0.5, 0.5]

# ## Square Lattice Mapping (KSG)

# The KSG-based approach creates a regular square grid where vertices can only connect to their nearest and next nearest neighbors (i.e. diagonals).

square_result = map_graph(Weighted(), k23_graph; vertex_order=MinhThiTrick())

println("Square lattice grid size: ", square_result.grid_graph.size)
println("Number of vertices: ", nv(square_result.grid_graph))
println("MIS overhead: ", square_result.mis_overhead)

# Visualize the mapping
show_graph(square_result.grid_graph; show_number=false)
show_grayscale(square_result.grid_graph) # show weights in gray scale
show_pins(square_result) # show pins in red

# Solve the mapped problem
square_mapped_weights = map_weights(square_result, source_weights)
square_grid_graph, _ = graph_and_weights(square_result.grid_graph)
square_solution = solve(GenericTensorNetwork(IndependentSet(square_grid_graph, square_mapped_weights)),
SingleConfigMax())[].c.data

# Map back to source
square_source_solution = map_config_back(square_result, collect(Int, square_solution))
println("Square solution: ", square_source_solution)

# ## Triangular Lattice Mapping

# While King's subgraphs provide a systematic approach for encoding problems on square lattices, they are not optimal for two-dimensional quantum hardware. The power-law decay of Rydberg interaction strengths in real devices leads to poor approximation of unit-disk graphs, requiring extensive post-processing that lacks explainability.
#
# The triangular lattice encoding scheme addresses these limitations by utilizing triangular Rydberg atom arrays which only blocks atoms in the nearest neighbors. This approach reduces independence-constraint violations by approximately two orders of magnitude compared to King's subgraphs, substantially alleviating the need for post-processing in experiments.
#
# The automatic embedding scheme generates graphs on triangular lattices with slightly larger overhead compared to KSG, but remains quadratic. For further improvement like removing dangling vertices, we need to manually optimize the embedding currently.
#


triangular_result = map_graph(TriangularWeighted(), k23_graph; vertex_order=MinhThiTrick())

println("Triangular lattice grid size: ", triangular_result.grid_graph.size)
println("Number of vertices: ", nv(triangular_result.grid_graph))
println("MIS overhead: ", triangular_result.mis_overhead)

# Visualize the mapping
show_graph(triangular_result.grid_graph; show_number=false)
show_grayscale(triangular_result.grid_graph) # show weights in gray scale
show_pins(triangular_result) # show pins in red

# Solve the mapped problem
triangular_mapped_weights = map_weights(triangular_result, source_weights)
triangular_grid_graph, _ = graph_and_weights(triangular_result.grid_graph)
triangular_solution = solve(GenericTensorNetwork(IndependentSet(triangular_grid_graph, triangular_mapped_weights)),
SingleConfigMax())[].c.data
show_config(triangular_result.grid_graph, triangular_solution)

# Map back to source
triangular_source_solution = map_config_back(triangular_result, collect(Int, triangular_solution))
println("Triangular solution: ", triangular_source_solution)

# ## Verification and Comparison

# To verify correctness, we solve the original problem directly and compare with both mapping approaches. All three methods should yield the same optimal solution value.

# Solve original problem directly
direct_solution = solve(GenericTensorNetwork(IndependentSet(k23_graph, source_weights)),
SingleConfigMax())[].c.data

println("Direct solution: ", collect(Int, direct_solution))

# Check all give same optimal value
direct_value = sum(source_weights[i] for i in 1:5 if direct_solution[i] == 1)
square_value = sum(source_weights[i] for i in 1:5 if square_source_solution[i] == 1)
triangular_value = sum(source_weights[i] for i in 1:5 if triangular_source_solution[i] == 1)

println("Optimal values - Direct: $direct_value, Square: $square_value, Triangular: $triangular_value")
println("All correct: ", direct_value ≈ square_value ≈ triangular_value)
91 changes: 80 additions & 11 deletions src/Core.jl
Original file line number Diff line number Diff line change
Expand Up @@ -56,30 +56,99 @@ offset(p::Node, xy) = chxy(p, getxy(p) .+ xy)
const WeightedNode{T<:Real} = Node{T}
const UnWeightedNode = Node{ONE}

############################ Grid Types ############################
"""
Abstract base type for grid geometries.

Grid types define how coordinates are mapped to physical positions for distance calculations.
"""
abstract type AbstractGridType end

"""
SquareGrid <: AbstractGridType

A square lattice grid where coordinates directly correspond to physical positions.
For a node at grid position (i, j), the physical position is (i, j).
"""
struct SquareGrid <: AbstractGridType end

"""
TriangularGrid <: AbstractGridType

A triangular lattice grid where nodes form an equilateral triangular pattern.

# Fields
- `offset_even_cols::Bool`: Whether even-numbered columns are offset vertically.
- `false` (default): Odd columns are offset by 0.5 units vertically
- `true`: Even columns are offset by 0.5 units vertically

# Physical positioning
For a node at grid position (i, j):
- y-coordinate: `j * (√3 / 2)` (maintains equilateral triangle geometry)
- x-coordinate: `i + offset` where offset depends on `offset_even_cols`:
- If `offset_even_cols = false`: offset = `0.5` if j is odd, `0.0` if j is even
- If `offset_even_cols = true`: offset = `0.5` if j is even, `0.0` if j is odd

# Examples
```julia
# Default triangular grid (odd columns offset)
grid1 = TriangularGrid() # equivalent to TriangularGrid(false)

# Even columns offset
grid2 = TriangularGrid(true)
```
"""
struct TriangularGrid <: AbstractGridType
offset_even_cols::Bool

"""
TriangularGrid(offset_even_cols::Bool=false)

Create a triangular grid with specified column offset pattern.

# Arguments
- `offset_even_cols`: If true, even columns are offset by 0.5; if false (default), odd columns are offset.
"""
TriangularGrid(offset_even_cols::Bool=false) = new(offset_even_cols)
end

############################ GridGraph ############################
# GridGraph
struct GridGraph{NT<:Node}
# Main definition
struct GridGraph{NT<:Node, GT<:AbstractGridType}
gridtype::GT
size::Tuple{Int,Int}
nodes::Vector{NT}
radius::Float64
end

# Base constructors (for any node/grid type)
GridGraph(size::Tuple{Int,Int}, nodes::Vector{NT}, radius::Real) where {NT<:Node} =
GridGraph(SquareGrid(), size, nodes, radius)

GridGraph(gridtype::GT, size::Tuple{Int,Int}, nodes::Vector{NT}, radius::Real) where {NT<:Node, GT<:AbstractGridType} =
GridGraph{NT, GT}(gridtype, size, nodes, radius)

function Base.show(io::IO, grid::GridGraph)
println(io, "$(typeof(grid)) (radius = $(grid.radius))")
gridtype_name = grid.gridtype isa SquareGrid ? "Square" : "Triangular"
println(io, "$(gridtype_name)$(typeof(grid)) (radius = $(grid.radius))")
print_grid(io, grid; show_weight=SHOW_WEIGHT[])
end
Base.size(gg::GridGraph) = gg.size
Base.size(gg::GridGraph, i::Int) = gg.size[i]

function graph_and_weights(grid::GridGraph)
return unit_disk_graph(getfield.(grid.nodes, :loc), grid.radius), getfield.(grid.nodes, :weight)
# Use physical positions for both grid types
physical_locs = [physical_position(node, grid.gridtype) for node in grid.nodes]
return unitdisk_graph(physical_locs, grid.radius), getfield.(grid.nodes, :weight)
end
function Graphs.SimpleGraph(grid::GridGraph{Node{ONE}})
return unit_disk_graph(getfield.(grid.nodes, :loc), grid.radius)

function Graphs.SimpleGraph(grid::GridGraph{Node{ONE}, GT}) where GT
physical_locs = [physical_position(node, grid.gridtype) for node in grid.nodes]
return unitdisk_graph(physical_locs, grid.radius)
end
coordinates(grid::GridGraph) = getfield.(grid.nodes, :loc)
function Graphs.neighbors(g::GridGraph, i::Int)
[j for j in 1:nv(g) if i != j && distance(g.nodes[i], g.nodes[j]) <= g.radius]
end
distance(n1::Node, n2::Node) = sqrt(sum(abs2, n1.loc .- n2.loc))
Graphs.neighbors(g::GridGraph, i::Int) = [j for j in 1:nv(g) if i != j && distance(g.nodes[i], g.nodes[j], g.gridtype) <= g.radius]
distance(n1::Node, n2::Node, grid_type::AbstractGridType) = sqrt(sum(abs2, physical_position(n1, grid_type) .- physical_position(n2, grid_type)))
Graphs.nv(g::GridGraph) = length(g.nodes)
Graphs.vertices(g::GridGraph) = 1:nv(g)

Expand Down Expand Up @@ -116,4 +185,4 @@ function GridGraph(m::AbstractMatrix{SimpleCell{WT}}, radius::Real) where WT
end
end
return GridGraph(size(m), nodes, radius)
end
end
6 changes: 4 additions & 2 deletions src/UnitDiskMapping.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ using LuxorGraphPlot
using LuxorGraphPlot.Luxor.Colors

# Basic types
export UnWeighted, Weighted
export UnWeighted, Weighted, TriangularWeighted
export SquareGrid, TriangularGrid
export Cell, AbstractCell, SimpleCell
export Node, WeightedNode, UnWeightedNode
export graph_and_weights, GridGraph, coordinates
Expand Down Expand Up @@ -41,8 +42,8 @@ export pathwidth, PathDecompositionMethod, MinhThiTrick, Greedy

@deprecate Branching MinhThiTrick

include("utils.jl")
include("Core.jl")
include("utils.jl")
include("pathdecomposition/pathdecomposition.jl")
include("copyline.jl")
include("dragondrop.jl")
Expand All @@ -51,6 +52,7 @@ include("logicgates.jl")
include("gadgets.jl")
include("mapping.jl")
include("weighted.jl")
include("triangular.jl")
include("simplifiers.jl")
include("extracting_results.jl")
include("visualize.jl")
Expand Down
Loading