Skip to content

Commit cb775b8

Browse files
authored
Merge pull request #62 from JuliaQUBO/feature/issue-61-topology-plots
Add native Pegasus and Zephyr plot helpers
2 parents a35fa52 + e1f6a66 commit cb775b8

4 files changed

Lines changed: 290 additions & 0 deletions

File tree

CondaPkg.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ channels = ["anaconda", "conda-forge"]
22

33
[deps]
44
python = ">=3.11,<=3.13"
5+
matplotlib = ">=3"
56

67
[pip.deps]
78
dwave-ocean-sdk = "==9.3.0"

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,27 @@ current `QUBODrivers` interface cleanly.
123123
## API Token
124124
To use D-Wave's QPU it is necessary to obtain an API Token from [Leap](https://cloud.dwavesys.com/leap/).
125125

126+
## Topology and Embedding Plots
127+
`DWave.draw_topology` and `DWave.draw_embedding` wrap D-Wave NetworkX's
128+
Pegasus and Zephyr plotting helpers and return a Matplotlib figure by default.
129+
Both helpers accept `DWave.WorkingGraph` values built from sampler metadata, so
130+
plots use the full calibrated working graph rather than only the embedded
131+
qubits.
132+
133+
```julia
134+
using DWave
135+
using QUBOTools
136+
137+
metadata = QUBOTools.metadata(sampleset)
138+
139+
DWave.draw_topology(DWave.WorkingGraph(metadata))
140+
DWave.draw_embedding(metadata)
141+
```
142+
143+
Pass `ax = existing_axis` to draw into an existing Matplotlib axis; all other
144+
keyword arguments are forwarded to the corresponding D-Wave NetworkX draw
145+
function.
146+
126147
## Development Checks
127148
To verify that DWave can share a CondaPkg environment with the other
128149
Python-backed JuliaQUBO benchmark drivers, run:

src/wrapper/topology.jl

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,169 @@ function _dnx_working_graph(arch::WorkingGraph)
333333
return nothing
334334
end
335335

336+
function _dnx_hardware_graph(arch::Pegasus)
337+
return _dnx().pegasus_graph(
338+
arch.size;
339+
node_list = _py(arch.nodes),
340+
edge_list = _py(arch.edges),
341+
)
342+
end
343+
344+
function _dnx_hardware_graph(arch::Zephyr)
345+
return _dnx().zephyr_graph(
346+
arch.size,
347+
arch.shore_size;
348+
node_list = _py(arch.nodes),
349+
edge_list = _py(arch.edges),
350+
)
351+
end
352+
353+
function _dnx_hardware_graph(arch::WorkingGraph)
354+
graph = _dnx_working_graph(arch)
355+
356+
graph === nothing && throw(ArgumentError(
357+
"D-Wave drawing helpers require Pegasus or Zephyr topology metadata with a nonempty shape",
358+
))
359+
360+
return graph
361+
end
362+
363+
function _hardware_topology(source)
364+
source isa DWaveHardwareTopology && return source
365+
source isa AbstractDict && return WorkingGraph(source)
366+
source isa QUBOTools.SampleSet && return WorkingGraph(QUBOTools.metadata(source))
367+
source isa PythonCall.Py && return WorkingGraph(source)
368+
369+
throw(ArgumentError(
370+
"expected a D-Wave hardware topology, metadata dictionary, sample set, or sampler",
371+
))
372+
end
373+
374+
_draw_topology_function(::Pegasus) = _dnx().draw_pegasus
375+
_draw_topology_function(::Zephyr) = _dnx().draw_zephyr
376+
377+
function _draw_topology_function(arch::WorkingGraph)
378+
arch.topology_type == "pegasus" && return _dnx().draw_pegasus
379+
arch.topology_type == "zephyr" && return _dnx().draw_zephyr
380+
381+
throw(ArgumentError("D-Wave drawing helpers only support Pegasus and Zephyr topologies"))
382+
end
383+
384+
_draw_embedding_function(::Pegasus) = _dnx().draw_pegasus_embedding
385+
_draw_embedding_function(::Zephyr) = _dnx().draw_zephyr_embedding
386+
387+
function _draw_embedding_function(arch::WorkingGraph)
388+
arch.topology_type == "pegasus" && return _dnx().draw_pegasus_embedding
389+
arch.topology_type == "zephyr" && return _dnx().draw_zephyr_embedding
390+
391+
throw(ArgumentError("D-Wave embedding drawing helpers only support Pegasus and Zephyr topologies"))
392+
end
393+
394+
function _matplotlib_figure_axes()
395+
figure_axes = PythonCall.pyimport("matplotlib.pyplot").subplots()
396+
397+
return figure_axes[0], figure_axes[1]
398+
end
399+
400+
function _draw_result(draw_function, graph, args...; ax = nothing, kwargs...)
401+
if ax === nothing
402+
figure, draw_axis = _matplotlib_figure_axes()
403+
draw_function(graph, args...; ax = draw_axis, kwargs...)
404+
405+
return figure
406+
else
407+
draw_function(graph, args...; ax = ax, kwargs...)
408+
409+
return ax
410+
end
411+
end
412+
413+
function _normalise_embedding_for_drawing(embedding_data)
414+
normal_embedding = _normalise_embedding(embedding_data)
415+
416+
normal_embedding === nothing && throw(ArgumentError(
417+
"embedding must be a dictionary mapping variables to integer qubit chains",
418+
))
419+
420+
return normal_embedding
421+
end
422+
423+
function _embedding_from_metadata(metadata::AbstractDict)
424+
normal_embedding = embedding(metadata)
425+
426+
normal_embedding === nothing && throw(ArgumentError(
427+
"metadata does not contain a D-Wave embedding; sample with return_embedding=true",
428+
))
429+
430+
return normal_embedding
431+
end
432+
433+
@doc raw"""
434+
DWave.draw_topology(source; kwargs...)
435+
436+
Draw a Pegasus or Zephyr hardware topology with D-Wave NetworkX.
437+
438+
`source` may be a `Pegasus`, `Zephyr`, `WorkingGraph`, D-Wave sampler,
439+
`QUBOTools.SampleSet`, or metadata dictionary accepted by `WorkingGraph`.
440+
Keyword arguments are forwarded to `dwave_networkx.draw_pegasus` or
441+
`dwave_networkx.draw_zephyr`.
442+
443+
By default this creates and returns a Matplotlib figure, which notebooks can
444+
display directly. If `ax` is supplied, drawing is performed on that axis and
445+
the same axis is returned.
446+
"""
447+
function draw_topology(arch::DWaveHardwareTopology; kwargs...)
448+
return _draw_result(
449+
_draw_topology_function(arch),
450+
_dnx_hardware_graph(arch);
451+
kwargs...,
452+
)
453+
end
454+
455+
function draw_topology(source; kwargs...)
456+
return draw_topology(_hardware_topology(source); kwargs...)
457+
end
458+
459+
@doc raw"""
460+
DWave.draw_embedding(source, embedding; kwargs...)
461+
DWave.draw_embedding(sampleset_or_metadata; kwargs...)
462+
463+
Draw a returned minor embedding over the full Pegasus or Zephyr working graph
464+
with D-Wave NetworkX.
465+
466+
The two-argument form accepts any topology source supported by
467+
`draw_topology` plus an embedding dictionary of the form
468+
`Dict{Int,Vector{Int}}`. The one-argument form extracts both the working graph
469+
and embedding from a `QUBOTools.SampleSet` or metadata dictionary, preserving
470+
compatibility with `DWave.WorkingGraph(QUBOTools.metadata(sampleset))`.
471+
472+
Keyword arguments are forwarded to `dwave_networkx.draw_pegasus_embedding` or
473+
`dwave_networkx.draw_zephyr_embedding`. By default this creates and returns a
474+
Matplotlib figure; if `ax` is supplied, that axis is returned.
475+
"""
476+
function draw_embedding(arch::DWaveHardwareTopology, embedding_data::AbstractDict; kwargs...)
477+
return _draw_result(
478+
_draw_embedding_function(arch),
479+
_dnx_hardware_graph(arch),
480+
_py(_normalise_embedding_for_drawing(embedding_data));
481+
kwargs...,
482+
)
483+
end
484+
485+
function draw_embedding(source, embedding_data::AbstractDict; kwargs...)
486+
return draw_embedding(_hardware_topology(source), embedding_data; kwargs...)
487+
end
488+
489+
function draw_embedding(metadata::AbstractDict; kwargs...)
490+
return draw_embedding(WorkingGraph(metadata), _embedding_from_metadata(metadata); kwargs...)
491+
end
492+
493+
function draw_embedding(sampleset::QUBOTools.SampleSet; kwargs...)
494+
metadata = QUBOTools.metadata(sampleset)
495+
496+
return draw_embedding(WorkingGraph(metadata), _embedding_from_metadata(metadata); kwargs...)
497+
end
498+
336499
function _dnx_layout_points(layout, nodes::Vector{Int})
337500
points = Vector{QUBOTools.Point{2,Float64}}(undef, length(nodes))
338501

test/topology.jl

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import QUBOTools
55
import Test
66

77
const Graphs = DWave.Graphs
8+
const PythonCall = DWave.PythonCall
89

910
function _node_edge_pairs(arch)
1011
graph = QUBOTools.topology(QUBOTools.layout(arch))
@@ -23,6 +24,30 @@ function _point_count(points)
2324
return length(unique(points))
2425
end
2526

27+
function _py_len(value)
28+
return PythonCall.pyconvert(Int, PythonCall.pybuiltins.len(value))
29+
end
30+
31+
function _py_class_name(value)
32+
return PythonCall.pyconvert(String, value.__class__.__name__)
33+
end
34+
35+
function _close_figure(figure)
36+
PythonCall.pyimport("matplotlib.pyplot").close(figure)
37+
38+
return nothing
39+
end
40+
41+
function _use_matplotlib_test_backend()
42+
config_dir = get!(ENV, "MPLCONFIGDIR") do
43+
mktempdir()
44+
end
45+
PythonCall.pyimport("os").environ["MPLCONFIGDIR"] = config_dir
46+
PythonCall.pyimport("matplotlib").use("Agg"; force = true)
47+
48+
return nothing
49+
end
50+
2651
Test.@testset "Pegasus layout uses Ocean topology data" begin
2752
arch = DWave.Pegasus(2)
2853
arch_layout = QUBOTools.layout(arch)
@@ -113,6 +138,86 @@ Test.@testset "WorkingGraph unknown topology uses graph geometry fallback" begin
113138
Test.@test _point_count(points) == 3
114139
end
115140

141+
Test.@testset "D-Wave NetworkX drawing helpers use full working graphs" begin
142+
_use_matplotlib_test_backend()
143+
144+
pegasus_metadata = Dict{String,Any}(
145+
"dwave_info" => Dict{String,Any}(
146+
"chip_info" => Dict{String,Any}(
147+
"topology" => Dict{String,Any}("type" => "pegasus", "shape" => Any[2]),
148+
"qubits" => Any[2, 3, 28],
149+
"couplers" => Any[Any[2, 3], Any[2, 28]],
150+
),
151+
"embedding_context" => Dict{String,Any}(
152+
"embedding" => Dict{Any,Any}(1 => Any[2], 2 => Any[28]),
153+
),
154+
),
155+
)
156+
pegasus = DWave.WorkingGraph(pegasus_metadata)
157+
pegasus_graph = DWave._dnx_hardware_graph(pegasus)
158+
159+
Test.@test isdefined(DWave, :draw_topology)
160+
Test.@test isdefined(DWave, :draw_embedding)
161+
Test.@test _py_len(pegasus_graph.nodes()) == length(pegasus.nodes)
162+
Test.@test _py_len(pegasus_graph.edges()) == length(pegasus.edges)
163+
164+
figure = DWave.draw_topology(pegasus; node_size = 8, with_labels = false)
165+
166+
try
167+
Test.@test _py_class_name(figure) == "Figure"
168+
finally
169+
_close_figure(figure)
170+
end
171+
172+
figure = DWave.draw_embedding(pegasus_metadata; node_size = 8, with_labels = false)
173+
174+
try
175+
Test.@test _py_class_name(figure) == "Figure"
176+
finally
177+
_close_figure(figure)
178+
end
179+
180+
zephyr_metadata = Dict{String,Any}(
181+
"dwave_info" => Dict{String,Any}(
182+
"chip_info" => Dict{String,Any}(
183+
"topology" => Dict{String,Any}("type" => "zephyr", "shape" => Any[2, 4]),
184+
"qubits" => Any[0, 1, 4],
185+
"couplers" => Any[Any[0, 1], Any[1, 4]],
186+
),
187+
"embedding_context" => Dict{String,Any}(
188+
"embedding" => Dict{Any,Any}(1 => Any[0], 2 => Any[4]),
189+
),
190+
),
191+
)
192+
zephyr = DWave.WorkingGraph(zephyr_metadata)
193+
zephyr_graph = DWave._dnx_hardware_graph(zephyr)
194+
195+
Test.@test _py_len(zephyr_graph.nodes()) == length(zephyr.nodes)
196+
Test.@test _py_len(zephyr_graph.edges()) == length(zephyr.edges)
197+
198+
figure = DWave.draw_topology(zephyr; node_size = 8, with_labels = false)
199+
200+
try
201+
Test.@test _py_class_name(figure) == "Figure"
202+
finally
203+
_close_figure(figure)
204+
end
205+
206+
figure = DWave.draw_embedding(zephyr, Dict(1 => [0], 2 => [4]); node_size = 8, with_labels = false)
207+
208+
try
209+
Test.@test _py_class_name(figure) == "Figure"
210+
finally
211+
_close_figure(figure)
212+
end
213+
214+
Test.@test_throws ArgumentError DWave.draw_embedding(Dict{String,Any}(
215+
"dwave_info" => Dict{String,Any}(
216+
"chip_info" => pegasus_metadata["dwave_info"]["chip_info"],
217+
),
218+
))
219+
end
220+
116221
if DWave.__auth__(; verbose = false)
117222
Test.@testset "WorkingGraph builds from live D-Wave sampler metadata" begin
118223
sampler = DWave.dwave_system.DWaveSampler(; token = ENV["DWAVE_API_TOKEN"])

0 commit comments

Comments
 (0)