Routine to find zephyr[m,tp] in possibly-faulty zephyr[m,t] with tp < t#261
Routine to find zephyr[m,tp] in possibly-faulty zephyr[m,t] with tp < t#261VolodyaCO wants to merge 2 commits intodwavesystems:mainfrom
Conversation
kevinchern
left a comment
There was a problem hiding this comment.
Nice work Jack&Vlad 🤩!! I did a quick first pass with mostly minor suggestions.
| @@ -0,0 +1,914 @@ | |||
| # Copyright 2026 D-Wave Systems Inc. | |||
There was a problem hiding this comment.
| # Copyright 2026 D-Wave Systems Inc. | |
| # Copyright 2026 D-Wave |
and other occurrences
There was a problem hiding this comment.
Other headers in the files have the full name:
https://github.com/dwavesystems/dwave-networkx/blob/main/dwave_networkx/generators/zephyr.py#L1
Should I keep the full name?
There was a problem hiding this comment.
Only change for new files is the policy.
|
|
||
| from dwave_networkx import zephyr_coordinates, zephyr_graph | ||
|
|
||
| ZephyrNode = tuple[int, int, int, int, int] # (u, w, k, j, z) coordinate tuple |
There was a problem hiding this comment.
Does something similar exist in dnx? @mahdiehmalekian @thisac
| ZephyrNode = tuple[int, int, int, int, int] # (u, w, k, j, z) coordinate tuple | ||
| Embedding = dict[ZephyrNode, ZephyrNode] | ||
| YieldType = Literal["node", "edge", "rail-edge"] | ||
| KSearchType = Literal["by_quotient_rail", "by_quotient_node", "by_rail_then_node"] |
There was a problem hiding this comment.
Is there a more descriptive name for "K"? (if not, consider adding a comment explaining what K refers to)
There was a problem hiding this comment.
It's not currently an informative name.
Per literal options, QuotientSearchType might be good, and we have to be careful to describe the 'quotient graph' in docs.
|
|
||
|
|
||
| def _validate_graph_inputs(source: nx.Graph, target: nx.Graph) -> tuple[int, int, int]: | ||
| """Validate Zephyr graph compatibility and return ``(m, tp, t)``. |
There was a problem hiding this comment.
Can we describe the checklist of requirements? e.g., inputs are required to be networkx graphs.
| TypeError: If inputs are not NetworkX graphs. | ||
| ValueError: If graph metadata is missing or incompatible. | ||
| """ | ||
| if not isinstance(source, nx.Graph) or not isinstance(target, nx.Graph): |
There was a problem hiding this comment.
I think we don't require them to be networkx graphs and we'll be moving away from networkx.
That said, this is fine; just a heads-up
edit: ^^^ this is partially incorrect 😅. We're not moving away from networkx, but probably don't require this input to be a networkx graph
| ``(_source, source_nodes, to_source)`` where ``_source`` is | ||
| coordinate-labelled, ``source_nodes`` is the full canonical coordinate | ||
| node set implied by ``m`` and ``tp``, and ``to_source`` maps | ||
| coordinate nodes back to the original source labelling spcae. |
There was a problem hiding this comment.
| coordinate nodes back to the original source labelling spcae. | |
| coordinate nodes back to the original source labelling space. |
| ) -> tuple[nx.Graph, Callable[[ZephyrNode], int | ZephyrNode]]: | ||
| """Return a coordinate-labelled target graph and conversion callable. | ||
|
|
||
| This is similar to the source version but does not need to return the full canonical node set |
There was a problem hiding this comment.
I would add a standalone description before contrasting with the source version
| ksymmetric: bool = False, | ||
| yield_type: YieldType = "edge", | ||
| ) -> tuple[dict, dict | None, ZephyrSearchMetadata]: | ||
| r"""Compute a high-yield Zephyr-to-Zephyr embedding. |
There was a problem hiding this comment.
| r"""Compute a high-yield Zephyr-to-Zephyr embedding. | |
| r"""Compute a high-yield Zephyr-to-Zephyr embedding. |
😜
| Returns: | ||
| tuple[dict, dict | None, ZephyrSearchMetadata]: | ||
| ``(subgraph_embedding, minor_embedding, metadata)`` | ||
| ``subgraph_embedding`` is a pruned one-to-one node map ``source_node -> target_node``. |
There was a problem hiding this comment.
Can we make the returned embeddings consistent in the sense that the one-to-one embeddings are length-1 chains, e.g., emb[1] == (53,) instead of emb[1] == 53?
cc @jackraymond
There was a problem hiding this comment.
We can perhaps add an as_embedding option to match find_subgraph, and default it to True
There was a problem hiding this comment.
If it is always assumed that one-to-one embeddings always map nodes to chains (of one node), then I would ensure that the input embedding is also given in this format, and I wouldn't add the as_embedding flag; by default all embeddings would embed to chains.
| ``source_node -> tuple[target_nodes, ...]`` or ``None``. If the greedy search did not | ||
| achieve full yield and minorminer is disabled, or minorminer was not able to find an | ||
| embedding, then ``minor_embedding`` is ``None``. If full yield is achieved, then |
There was a problem hiding this comment.
I think this interface can be naturally split in two functions. Currently, it attempts to A) embed with one method, and tries B) minorminer with a seeded solution when the first fails. Would having the two functions A and B work? This current function can still exist, just as a wrapper that calls A and B in succession.
There was a problem hiding this comment.
one minor problem or consideration with my suggestion: validation would step would be required in both procedures A and B. Chaining them would result in redundant validation steps.
There was a problem hiding this comment.
Why would we separate the minorminer part? It only uses find_embedding straight-forwardly. What I mean is that if a user wants to use find_embedding to improve a current embedding, all that needs to be done is to use find_embedding directly using the initial_chains argument.
There was a problem hiding this comment.
Ah wait that's a good point. In that case, I think it's better to remove it altogether for modularity's sake. This would simplify the dependencies and nested arguments.
|
Embedding helpers are more often added to minorminer these days, would that not be the correct location? dwave-experimental is another option. This pull-request fails (testing) because dwave-networkx does not (and should not) depend on minorminer. |
|
With a view to related functionality, I'd prefer this module were called something like processor_sublattice_embeddings.py and have the module located at ~/GIT/minorminer/minorminer/utils |
| Returns: | ||
| tuple[dict, dict | None, ZephyrSearchMetadata]: | ||
| ``(subgraph_embedding, minor_embedding, metadata)`` | ||
| ``subgraph_embedding`` is a pruned one-to-one node map ``source_node -> target_node``. |
There was a problem hiding this comment.
We can perhaps add an as_embedding option to match find_subgraph, and default it to True
| import minorminer | ||
| import networkx as nx | ||
| import numpy as np | ||
| from dwave.embedding import is_valid_embedding, verify_embedding |
There was a problem hiding this comment.
We don't want to add dwave as a dwave_networkx dependency. Maybe move pull request to minorminer/utils (per other comment).
| from collections import namedtuple | ||
| from typing import Callable, Literal, get_args | ||
|
|
||
| import minorminer |
There was a problem hiding this comment.
We don't want to add mionrminer as a dwave_networkx dependency. Maybe move pull request to minorminer/utils (per other comment).
There was a problem hiding this comment.
@jackraymond I just realized the only dependence on minorminer is find_embedding. Is the method feasible if we passed in a embedding_function and embedding_function_kwargs as arguments for the more general approach?
There was a problem hiding this comment.
nvm; this is utility is closer to embeddings than it is to graphs, and it still has dependency on dwave for other functions like validating/verifying embeddings. more reasons to be in minorminer than dnx.
| return pruned_embedding, embedding2, metadata | ||
|
|
||
|
|
||
| def main( |
There was a problem hiding this comment.
I would actually get rid of this. Most of what is in the main function is already in the tests. I just didn't delete it the first time. An useful example for me would be to take an existing qpu graph and use the zephyr_quotient_search function to get a fully-yielded Zephyr subgraph. I believe this is what we are building this tool for. However, from the report cards, the unyielded qubits (either nodes or couplers) seem to agglutinate, which means that for a real qpu graph there are no valid zephyr subgraphs with the same number of columns m.
There was a problem hiding this comment.
I agree, better to delete than to move.
At yield rates on current Adv2 processors subgraph isomorphisms (for any tp) can be verified as non-existent. If we fully randomize the location of node failures this remains true (with high probability), so agglutination is not the only challenge. But that would be the correct direction for an example.
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| solver_names = ["Advantage2_system1.11", "Advantage2_system1.12"] |
There was a problem hiding this comment.
Move to examples, and perhaps default to any zephyr solver through the client. Can also allow non-client option that uses MockDwaveSampler.
| else: | ||
| fn = f"{solver_name}.pkl" | ||
| if not os.path.isfile(fn): | ||
| qpu = DWaveSampler(profile="defaults", solver=solver_name) |
There was a problem hiding this comment.
| qpu = DWaveSampler(profile="defaults", solver=solver_name) | |
| qpu = DWaveSampler(solver=solver_name) |
jackraymond
left a comment
There was a problem hiding this comment.
We will need to move this to minorminer or dwave-experimental
Mostly looks good. But will require second round review.
Ran tests and examples. I will try to test performance of this (greedy) method relative to our slower complete methods (per internal research).
Validate that an input embedding is indeed a one-to-one mapping. Explain thoroughly yield types in the docstrings of the relevant functions. Fix typos in the docstrings.
VolodyaCO
left a comment
There was a problem hiding this comment.
Went through the code comments and pushed fixes. Left questions in some of the comments too.
|
|
||
|
|
||
| def _validate_graph_inputs(source: nx.Graph, target: nx.Graph) -> tuple[int, int, int]: | ||
| """Validate Zephyr graph compatibility and return ``(m, tp, t)``. |
There was a problem hiding this comment.
Even though the user can extract m, tp and t, they can always be obtained from the source and target graphs, so returning these numbers avoids a potential error of the user entering the wrong numbers.
| if target.graph.get("family") != "zephyr": | ||
| raise ValueError("target graph should be a zephyr family graph") | ||
|
|
||
| for graph_name, graph in zip(("source", "target"), (source, target)): |
There was a problem hiding this comment.
I've always thought of zip as a natural function for these sort of things... any reason to avoid it?
| @@ -0,0 +1,914 @@ | |||
| # Copyright 2026 D-Wave Systems Inc. | |||
| ) | ||
|
|
||
|
|
||
| def _validate_graph_inputs(source: nx.Graph, target: nx.Graph) -> tuple[int, int, int]: |
There was a problem hiding this comment.
I can first validate that the graph's family exist and are valid, and that the graph's row, tile and labels fields exist to be able to extract these properties. Then, another function would validate these properties. Does that sound good?
|
|
||
|
|
||
| def _validate_graph_inputs(source: nx.Graph, target: nx.Graph) -> tuple[int, int, int]: | ||
| """Validate Zephyr graph compatibility and return ``(m, tp, t)``. |
| ksymmetric: bool = False, | ||
| yield_type: YieldType = "edge", | ||
| ) -> tuple[dict, dict | None, ZephyrSearchMetadata]: | ||
| r"""Compute a high-yield Zephyr-to-Zephyr embedding. |
| ksearch (KSearchType): Search strategy. One of ``'by_quotient_rail'``, | ||
| ``'by_quotient_node'``, or ``'by_rail_then_node'``. Defaults to ``'by_quotient_rail'``. |
There was a problem hiding this comment.
The ksearch strategies are already explained in the docstrings. We can go for shorter names, but I think the longer name may be more informative (as, e.g., the epxression ksearch='by_quotient_node' looks close to what the expression would mean in natural language).
| Returns: | ||
| tuple[dict, dict | None, ZephyrSearchMetadata]: | ||
| ``(subgraph_embedding, minor_embedding, metadata)`` | ||
| ``subgraph_embedding`` is a pruned one-to-one node map ``source_node -> target_node``. |
There was a problem hiding this comment.
If it is always assumed that one-to-one embeddings always map nodes to chains (of one node), then I would ensure that the input embedding is also given in this format, and I wouldn't add the as_embedding flag; by default all embeddings would embed to chains.
| ``source_node -> tuple[target_nodes, ...]`` or ``None``. If the greedy search did not | ||
| achieve full yield and minorminer is disabled, or minorminer was not able to find an | ||
| embedding, then ``minor_embedding`` is ``None``. If full yield is achieved, then |
There was a problem hiding this comment.
Why would we separate the minorminer part? It only uses find_embedding straight-forwardly. What I mean is that if a user wants to use find_embedding to improve a current embedding, all that needs to be done is to use find_embedding directly using the initial_chains argument.
| return pruned_embedding, embedding2, metadata | ||
|
|
||
|
|
||
| def main( |
There was a problem hiding this comment.
I would actually get rid of this. Most of what is in the main function is already in the tests. I just didn't delete it the first time. An useful example for me would be to take an existing qpu graph and use the zephyr_quotient_search function to get a fully-yielded Zephyr subgraph. I believe this is what we are building this tool for. However, from the report cards, the unyielded qubits (either nodes or couplers) seem to agglutinate, which means that for a real qpu graph there are no valid zephyr subgraphs with the same number of columns m.
|
As per moving this PR to minorminer, I do agree. Should we finish this first revision and then close the PR here and open it in minorminer? |
| ``max_num_yielded``, ``starting_num_yielded``, and ``final_num_yielded``. | ||
| """ | ||
|
|
||
| m, tp, t = _validate_graph_inputs(source, target) |
There was a problem hiding this comment.
We should probably allow for source graphs smaller than target graphs, i.e. any combination mp<=m, tp<=t
The simplest (sufficient) way to to do this is to check every sublattice:
best_tuple = None
for gen in zephyr_sublattice_mappings(S, T):
# This method could be made more efficient by reuse of information on overlapping mappings
proposal = zephyr_quotient_search(T=mask(T, gen), *other_kwargs )
if is_better(best_tuple, proposal):
best_tuple = proposal
if is_optimal:
break
return best_tuple
There was a problem hiding this comment.
Maybe better: when mp <m we advise people to use this embedding method in combination with minorminer.find_sublattice_embeddings (rather than rewriting the same method in special case).
This PR adds a heuristic routine written by @jackraymond to find an embedding from a source graph zephyr[m, tp] to a target graph which is a subgraph of zephyr[m,t]. The motivation of this routine is to have a tool to generate zephyr subgraphs that are resilient to possible future qubit/coupler defects in actual QPUs.
Tests are added to make sure that the routine indeed improves the yield of the embedded graph.