Skip to content

Add Grid for surveying Zephyr topologies - #291

Open
mahdiehmalekian wants to merge 3 commits into
dwavesystems:mainfrom
mahdiehmalekian:grid
Open

mahdiehmalekian wants to merge 3 commits into
dwavesystems:mainfrom
mahdiehmalekian:grid

Conversation

@mahdiehmalekian

Copy link
Copy Markdown
Contributor

Adds minorminer.utils._clique.grid, another internal piece of the Zephyr clique embedder, after el_geometry (#289) and maximum_bipartite_matching (#290).

Grid surveys a possibly-faulty Zephyr topology once, at construction, into tables the clique embedder then queries directly. Tests are in tests/utils/clique/test_grid.py.

AI use: I used Claude to clean up and edit documentation of the grid.py and to generate the test_grid.py. Checked and edited myself.

Note to reviewers: This is part of a larger Zephyr clique embedder effort and subsequent clique-embedder PRs depend on this PR and can only follow once this merges. Would appreciate a speedy review.

@SebastianGitt SebastianGitt left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, but I think many of the methods would be more readable if they had more descriptive variable names.

Comment thread minorminer/utils/_clique/grid.py Outdated
Comment thread minorminer/utils/_clique/grid.py
Comment thread minorminer/utils/_clique/grid.py Outdated
Comment thread minorminer/utils/_clique/grid.py Outdated
Comment thread minorminer/utils/_clique/grid.py Outdated
Comment thread tests/utils/clique/test_grid.py Outdated
Comment thread tests/utils/clique/test_grid.py Outdated
Comment thread minorminer/utils/_clique/grid.py Outdated
Comment thread minorminer/utils/_clique/grid.py Outdated
Comment thread minorminer/utils/_clique/grid.py Outdated

@SebastianGitt SebastianGitt left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just took another pass with some higher-level observations and suggestions.

Comment thread minorminer/utils/_clique/grid.py Outdated
Comment thread minorminer/utils/_clique/grid.py
Comment thread minorminer/utils/_clique/grid.py Outdated
Comment thread minorminer/utils/_clique/grid.py
Comment thread minorminer/utils/_clique/grid.py
Comment thread minorminer/utils/_clique/grid.py
Comment thread minorminer/utils/_clique/grid.py Outdated

@thisac thisac left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @mahdiehmalekian. I'd like to do a more thorough review.

runs, ...) that the rest of the algorithm queries, rather than re-walking the graph on every
lookup.

Build with Grid.from_graph(graph). See the module docstring for the full list of attributes and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Build with Grid.from_graph(graph). See the module docstring for the full list of attributes and
Build with ``Grid.from_graph(graph)``. See the module docstring for the full list of attributes and

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would make more sense to add the attributes here instead of at the module level. Also the __init__() args are missing.


Coordinates range over ``[0, 4m]``. This only allocates the empty containers; from_graph
calls _classify and _run_survey to populate them. Prefer the from_graph factory.
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be detailed in the class docstring, along with the arguments. Not here.

"pos",
)

# ------------------------------------------------------------------ build

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# ------------------------------------------------------------------ build

Comment on lines +171 to +183
# attribute types (bare annotations; the storage is __slots__ below)
m: int
t: int
abs_min: int
abs_max: int
labels: str # output mode: "int"/"coordinates"/"cartesian"
present_qubits: dict[str, dict[Node, int]] # present_qubits[kind][node] -> linear index r
missing_qubits: dict[str, frozenset[Node] | set[Node]]
edges: set[tuple[int, int]] | None # present couplers as (lo, hi) r-pairs
missing_internal_couplers: dict | None # see _missing_internal_couplers
runs: Runs | None
_el_cache: dict # el_reachable memo
pos: Pos | None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need these. Better to add types in __init__() instead.

Comment on lines +184 to +197
__slots__ = (
"m",
"t",
"abs_min",
"abs_max",
"labels",
"present_qubits",
"missing_qubits",
"edges",
"missing_internal_couplers",
"runs",
"_el_cache",
"pos",
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you really need __slots__ here? Is it for some gain in attribute access (have you benchmarked this) or something else?

Comment on lines +279 to +300
elif labels == "cartesian": # nodes are cartesian (x, y, k) already
grid._classify_from_present_nodes(set(graph.nodes()))
# cartesian edges -> linear r pairs (internal edge set stays linear)
edges = set()
for ccoord_a, ccoord_b in graph.edges():
lcoord_a = grid.cartesian_to_linear(ccoord_a)
lcoord_b = grid.cartesian_to_linear(ccoord_b)
if lcoord_a is None or lcoord_b is None:
continue
edges.add((lcoord_a, lcoord_b) if lcoord_a < lcoord_b else (lcoord_b, lcoord_a))
grid.edges = edges
else: # coordinates: nodes are Zephyr 5-tuples
grid._classify_from_present_nodes({zephyr_to_cartesian(z) for z in graph.nodes()})
# convert coordinate edges -> cartesian -> linear r pairs
edges = set()
for zcoord_a, zcoord_b in graph.edges():
lcoord_a = grid.cartesian_to_linear(zephyr_to_cartesian(zcoord_a))
lcoord_b = grid.cartesian_to_linear(zephyr_to_cartesian(zcoord_b))
if lcoord_a is None or lcoord_b is None:
continue
edges.add((lcoord_a, lcoord_b) if lcoord_a < lcoord_b else (lcoord_b, lcoord_a))
grid.edges = edges

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can likely be deduplicated (and a bit cleaned up). E.g., something like

Suggested change
elif labels == "cartesian": # nodes are cartesian (x, y, k) already
grid._classify_from_present_nodes(set(graph.nodes()))
# cartesian edges -> linear r pairs (internal edge set stays linear)
edges = set()
for ccoord_a, ccoord_b in graph.edges():
lcoord_a = grid.cartesian_to_linear(ccoord_a)
lcoord_b = grid.cartesian_to_linear(ccoord_b)
if lcoord_a is None or lcoord_b is None:
continue
edges.add((lcoord_a, lcoord_b) if lcoord_a < lcoord_b else (lcoord_b, lcoord_a))
grid.edges = edges
else: # coordinates: nodes are Zephyr 5-tuples
grid._classify_from_present_nodes({zephyr_to_cartesian(z) for z in graph.nodes()})
# convert coordinate edges -> cartesian -> linear r pairs
edges = set()
for zcoord_a, zcoord_b in graph.edges():
lcoord_a = grid.cartesian_to_linear(zephyr_to_cartesian(zcoord_a))
lcoord_b = grid.cartesian_to_linear(zephyr_to_cartesian(zcoord_b))
if lcoord_a is None or lcoord_b is None:
continue
edges.add((lcoord_a, lcoord_b) if lcoord_a < lcoord_b else (lcoord_b, lcoord_a))
grid.edges = edges
else: # labels either cartesian or 5-tuple
if labels == "coordinates":
convert = zephyr_to_cartesian
else:
convert = lambda n: n
grid._classify_from_present_nodes({convert(n) for n in graph.nodes()})
to_linear = grid.cartesian_to_linear
edges = set()
for node_a, node_b in graph.edges():
lcoord_a = to_linear(convert(node_a))
lcoord_b = to_linear(convert(node_b))
if lcoord_a is None or lcoord_b is None:
continue
if lcoord_a < lcoord_b:
edges.add((lcoord_a, lcoord_b))
else:
edges.add((lcoord_a, lcoord_b))
grid.edges = edges

Note, test before accepting.

Comment on lines +208 to +209
self.abs_min = 0
self.abs_max = 4 * m

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these just be constants instead?

Comment on lines +433 to +435
H_KIND = {1: "h1", 3: "h3"}
krange = range(t)
OFFS = ((-1, -1), (-1, 1), (1, -1), (1, 1))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since H_KIND and OFFS are named as (and are) constants, they should be declared outside of the function scope (at the top of the file alongside other constants, and probably renamed to something clearer).

f"cannot infer zephyr label mode from tuple node {sample!r}; "
f"expected a 5-tuple (Zephyr) or 3-tuple (cartesian)"
)
elif isinstance(sample, (int,)) and not isinstance(sample, bool):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this also accept NumPy integers? If so, use number.Integral instead of int.

Comment on lines +206 to +218
self.m = m
self.t = t
self.abs_min = 0
self.abs_max = 4 * m
self.labels = "int" # output mode: "int", "coordinates", or "cartesian"
self.present_qubits = {k: {} for k in _KINDS}
self.missing_qubits = {k: set() for k in _KINDS}
self.edges = None
# survey outputs (filled by _run_survey)
self.missing_internal_couplers = None
self.runs = None
self._el_cache = {}
self.pos = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of these (might) cause errors if from_graph() hasn't been run. E.g., self.edges = None if has_edge() is called before. Probably better to have reasonable values here instead of None or enforce that from_graph() has been called.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants