Add Grid for surveying Zephyr topologies - #291
mahdiehmalekian wants to merge 3 commits into
Conversation
SebastianGitt
left a comment
There was a problem hiding this comment.
Looks good, but I think many of the methods would be more readable if they had more descriptive variable names.
SebastianGitt
left a comment
There was a problem hiding this comment.
Just took another pass with some higher-level observations and suggestions.
thisac
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
| 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 |
There was a problem hiding this comment.
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. | ||
| """ |
There was a problem hiding this comment.
Should be detailed in the class docstring, along with the arguments. Not here.
| "pos", | ||
| ) | ||
|
|
||
| # ------------------------------------------------------------------ build |
There was a problem hiding this comment.
| # ------------------------------------------------------------------ build |
| # 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 |
There was a problem hiding this comment.
I don't think you need these. Better to add types in __init__() instead.
| __slots__ = ( | ||
| "m", | ||
| "t", | ||
| "abs_min", | ||
| "abs_max", | ||
| "labels", | ||
| "present_qubits", | ||
| "missing_qubits", | ||
| "edges", | ||
| "missing_internal_couplers", | ||
| "runs", | ||
| "_el_cache", | ||
| "pos", | ||
| ) |
There was a problem hiding this comment.
Do you really need __slots__ here? Is it for some gain in attribute access (have you benchmarked this) or something else?
| 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 |
There was a problem hiding this comment.
This can likely be deduplicated (and a bit cleaned up). E.g., something like
| 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.
| self.abs_min = 0 | ||
| self.abs_max = 4 * m |
There was a problem hiding this comment.
Should these just be constants instead?
| H_KIND = {1: "h1", 3: "h3"} | ||
| krange = range(t) | ||
| OFFS = ((-1, -1), (-1, 1), (1, -1), (1, 1)) |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
Should this also accept NumPy integers? If so, use number.Integral instead of int.
| 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 |
There was a problem hiding this comment.
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.
Adds
minorminer.utils._clique.grid, another internal piece of the Zephyr clique embedder, afterel_geometry(#289) andmaximum_bipartite_matching(#290).Gridsurveys a possibly-faulty Zephyr topology once, at construction, into tables the clique embedder then queries directly. Tests are intests/utils/clique/test_grid.py.AI use: I used Claude to clean up and edit documentation of the
grid.pyand to generate thetest_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.