Skip to content

Routine to find zephyr[m,tp] in possibly-faulty zephyr[m,t] with tp < t#261

Draft
VolodyaCO wants to merge 2 commits intodwavesystems:mainfrom
VolodyaCO:zephyr-quotient-search
Draft

Routine to find zephyr[m,tp] in possibly-faulty zephyr[m,t] with tp < t#261
VolodyaCO wants to merge 2 commits intodwavesystems:mainfrom
VolodyaCO:zephyr-quotient-search

Conversation

@VolodyaCO
Copy link
Copy Markdown

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.

@thisac thisac requested a review from kevinchern March 13, 2026 16:51
Copy link
Copy Markdown
Contributor

@kevinchern kevinchern left a comment

Choose a reason for hiding this comment

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

Nice work Jack&Vlad 🤩!! I did a quick first pass with mostly minor suggestions.

@@ -0,0 +1,914 @@
# Copyright 2026 D-Wave Systems Inc.
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
# Copyright 2026 D-Wave Systems Inc.
# Copyright 2026 D-Wave

and other occurrences

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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?

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.

Only change for new files is the policy.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Keeping name short


from dwave_networkx import zephyr_coordinates, zephyr_graph

ZephyrNode = tuple[int, int, int, int, int] # (u, w, k, j, z) coordinate tuple
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.

Does something similar exist in dnx? @mahdiehmalekian @thisac

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Will exist in near future.

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"]
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.

Is there a more descriptive name for "K"? (if not, consider adding a comment explaining what K refers to)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is for @jackraymond

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'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)``.
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.

Can we describe the checklist of requirements? e.g., inputs are required to be networkx graphs.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I just added it.

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):
Copy link
Copy Markdown
Contributor

@kevinchern kevinchern Mar 13, 2026

Choose a reason for hiding this comment

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

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.
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
coordinate nodes back to the original source labelling spcae.
coordinate nodes back to the original source labelling space.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for catching this!

) -> 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
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 would add a standalone description before contrasting with the source version

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ok, I've added this.

ksymmetric: bool = False,
yield_type: YieldType = "edge",
) -> tuple[dict, dict | None, ZephyrSearchMetadata]:
r"""Compute a high-yield Zephyr-to-Zephyr embedding.
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
r"""Compute a high-yield Zephyr-to-Zephyr embedding.
r"""Compute a high-yield Zephyr-to-Zephyr embedding.

😜

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

thanks

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``.
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.

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

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.

We can perhaps add an as_embedding option to match find_subgraph, and default it to True

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Comment on lines +668 to +670
``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
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 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.

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.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

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.

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.

@jackraymond
Copy link
Copy Markdown
Contributor

jackraymond commented Mar 16, 2026

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.
This is an embedding helper function, not a wrapper for networkx, so I think it should be moved to minorminer in the medium term, and perhaps dwave-experimental in the short term (flexibility to change defaults and signatures may be useful).

@jackraymond
Copy link
Copy Markdown
Contributor

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``.
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.

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
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.

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
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.

We don't want to add mionrminer as a dwave_networkx dependency. Maybe move pull request to minorminer/utils (per other 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.

@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?

Copy link
Copy Markdown
Contributor

@kevinchern kevinchern Mar 16, 2026

Choose a reason for hiding this comment

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

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(
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.

Move to examples/

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Image

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 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"]
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.

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)
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
qpu = DWaveSampler(profile="defaults", solver=solver_name)
qpu = DWaveSampler(solver=solver_name)

Copy link
Copy Markdown
Contributor

@jackraymond jackraymond left a comment

Choose a reason for hiding this comment

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

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.
Copy link
Copy Markdown
Author

@VolodyaCO VolodyaCO left a comment

Choose a reason for hiding this comment

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

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)``.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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)):
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Keeping name short

)


def _validate_graph_inputs(source: nx.Graph, target: nx.Graph) -> tuple[int, int, int]:
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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)``.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I just added it.

ksymmetric: bool = False,
yield_type: YieldType = "edge",
) -> tuple[dict, dict | None, ZephyrSearchMetadata]:
r"""Compute a high-yield Zephyr-to-Zephyr embedding.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

thanks

Comment on lines +648 to +649
ksearch (KSearchType): Search strategy. One of ``'by_quotient_rail'``,
``'by_quotient_node'``, or ``'by_rail_then_node'``. Defaults to ``'by_quotient_rail'``.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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``.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Comment on lines +668 to +670
``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
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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(
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Image

@VolodyaCO
Copy link
Copy Markdown
Author

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)
Copy link
Copy Markdown
Contributor

@jackraymond jackraymond Mar 20, 2026

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

@jackraymond jackraymond Mar 20, 2026

Choose a reason for hiding this comment

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

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).

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.

5 participants