Skip to content

Add example and tests for fully yielded Zephyr subgraph search#45

Open
VolodyaCO wants to merge 5 commits intodwavesystems:mainfrom
VolodyaCO:zephyr_quotient_search
Open

Add example and tests for fully yielded Zephyr subgraph search#45
VolodyaCO wants to merge 5 commits intodwavesystems:mainfrom
VolodyaCO:zephyr_quotient_search

Conversation

@VolodyaCO
Copy link
Copy Markdown

Continuation of dwavesystems/dwave-graphs#261

Adds a routine to find embeddings of Zephyr[m, tp] into Zephyr[m, t] with tp < t.

Copy link
Copy Markdown

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

Added minor questions and comments



def _extract_graph_properties(source: nx.Graph, target: nx.Graph) -> tuple[int, int, int]:
"""Extract and validate Zephyr graph properties, returning ``(m, tp, t)``.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I suggest something more descriptive rather than variable names, e.g.,

Suggested change
"""Extract and validate Zephyr graph properties, returning ``(m, tp, t)``.
"""Extract and validate Zephyr graph properties (rows, tile count, and target tile count).

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.

Done

Comment on lines +79 to +80
tuple[int, int, int]: ``(m, tp, t)`` where ``m`` is rows,
``tp`` is source tile count, and ``t`` is target tile count.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
tuple[int, int, int]: ``(m, tp, t)`` where ``m`` is rows,
``tp`` is source tile count, and ``t`` is target tile count.
tuple[int, int, int]: source Zephyr rows, source tile count, and target tile count.

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.

Done


Raises:
TypeError: If metadata values are not integers.
ValueError: If graph metadata is missing or incompatible.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I this should detail what it means to be incompatible; incompatibility is implied by ValueError, e.g., positive int or ...

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.

Done

for n in _source.nodes()
if n in working_embedding
)
else:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
else:
elif yield_type == "edges":
# do stuff
else:
raise ValueError("blah blah")

I recommend being explicit about checking yield type.

Edit: maybe this is fine since validity of yield type has been validated previously...

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 two options are edge and rail-edge. This is fine, as you say, because the type has been validated.

Comment on lines +29 to +30
YieldType = Literal["node", "edge", "rail-edge"]
QuotientSearchType = Literal["by_quotient_rail", "by_quotient_node", "by_rail_then_node"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Question!!
I think most of these are only ever used in type-hinting. Should we still define these constant type hints here? Is there a best-practice or principle that recommends this pattern?
Subjectively, I find it less readable than writing the types out explicitly in the type hints.

cc @thisac

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.

Yep. in vscode all it takes is to hover over the constant type hint and it will show more details, though.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Type-aliases are good when not overused. I agree with @kevinchern that it can make code less readable. With that said, I'm fine with these, except perhaps Embedding, EmbeddingChain since they're not very clearly named.

__all__ = ["zephyr_quotient_search"]

ZephyrNode = tuple[int, int, int, int, int] # (u, w, k, j, z) coordinate tuple
Embedding = dict[ZephyrNode, ZephyrNode] # Internal single-node format
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note for considering a different name: dwave.embedding has an EmbeddedStructure class. Might be worth following that pattern but tailored for Zephyr. e.g., ZephyrEmbeddedStructure (ok maybe this is too long lol)

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 Embedding is just a short-hand typing tool. The EmbeddedStructure is a bit more convoluted.


pruned_embedding = {
to_source(k): to_target(v) for k, v in working_embedding.items() if v in target_nodeset
} # TODO:?: why would a target node in the working_embedding not be in the target_nodeset?
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@VolodyaCO has this question been addressed with @jackraymond ?

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.

It hasn't AFAIR.

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.

to_target is an identity, so we can remove it right?

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.

Perhaps its required because you want to cast it to a ZephyrNode, and we can't be sure that's the format provided - the function name could be changed and type hinting improved?

Copy link
Copy Markdown
Contributor

@SebastianGitt SebastianGitt left a comment

Choose a reason for hiding this comment

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

Looks good! Just have some minor comments.

tiles. It is designed for defective targets where a direct identity map may lose
nodes or edges.

The search is organised around the **quotient graph** of the Zephyr topology, formed by
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
The search is organised around the **quotient graph** of the Zephyr topology, formed by
The search is organized around the **quotient graph** of the Zephyr topology, formed by

minor issue but US English should be used

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.

Oh I will have a bunch of these. thanks

Comment thread tests/test_zephyr_quotient_search.py Outdated
# Phase 1: uniform random deletion
n_uniform = round(proportion * uniform_proportion * N)
uniform_indices = rng.choice(N, size=n_uniform, replace=False)
deleted = {all_nodes[i] for i in uniform_indices}
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.

consider renaming this to something more clear

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.

Changed to deleted_nodes.

"""Validate that source and target are Zephyr NetworkX graphs.

Both source and target graphs must be networkx graph instances with a 'family' metadata key
set to 'zephyr'. Each graph must also contain 'rows', 'tile' and 'labels metadata keys.
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
set to 'zephyr'. Each graph must also contain 'rows', 'tile' and 'labels metadata keys.
set to 'zephyr'. Each graph must also contain 'rows', 'tile' and 'labels' metadata keys.

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.

Done.

if expand_boundary_search:
if w == 0:
ksymmetric = False
# brrow candidates from adjacent internal column
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
# brrow candidates from adjacent internal column
# borrow candidates from adjacent internal column

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.

Done

\{(u, w_t, k_t, j, z) : j \in \{0,1\},\ z \in \{0,\dots,m-1\}\}.

We can define its objective for ``yield_type='edge'`` as the number of edges preserved within
that rail, i.e., the numbe of edges in the target subgraph induced by the proposed rail, or
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
that rail, i.e., the numbe of edges in the target subgraph induced by the proposed rail, or
that rail, i.e., the number of edges in the target subgraph induced by the proposed rail, or

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.

Done.


F_q = \{m \in V(S) \setminus B_q : m \in \operatorname{dom}(\phi)\},

where :math:`\phi` is the current embedding`. 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.

Suggested change
where :math:`\phi` is the current embedding`. Then
where :math:`\phi` is the current embedding. 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.

Done


Args:
quotient_search (str): Search mode.
yield_type (str): Optimization objective.
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
yield_type (str): Optimization objective.
yield_type (YieldType): Optimization objective.

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 should be a str, as it validates the search parameters. If it is not a YieldType, then an error is raised.

:math:`w` is at a boundary. Defaults to ``True``.
ksymmetric (bool): If ``True``, treat source :math:`k` order as interchangeable when scoring
rails. Defaults to ``False``.
yield_type (str): ``"node"``, ``"edge"``, or ``"rail-edge"``. Defaults to ``"edge"``.
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
yield_type (str): ``"node"``, ``"edge"``, or ``"rail-edge"``. Defaults to ``"edge"``.
yield_type (YieldType): ``"node"``, ``"edge"``, or ``"rail-edge"``. Defaults to ``"edge"``.

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.

done, thanks

)


def _validate_graph_inputs(source: nx.Graph, target: nx.Graph):
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
def _validate_graph_inputs(source: nx.Graph, target: nx.Graph):
def _validate_graph_inputs(source: nx.Graph, target: nx.Graph) -> None:

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.

Done.

Comment thread tests/test_zephyr_quotient_search.py Outdated
ZephyrSearchMetadata, zephyr_quotient_search)


def generate_faulty_zephyr_graph(m, t, proportion, uniform_proportion, seed=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.

consider adding type hinting here since this function is non-trivial and used in multiple places

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.

Added type hinting

Copy link
Copy Markdown

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

Did a few passes with focus on usability and some design patterns 🚀

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.

On top of reviews by Sebastian/Kevin:

Check imports, to_target().

A couple of other comments.

Great job


pruned_embedding = {
to_source(k): to_target(v) for k, v in working_embedding.items() if v in target_nodeset
} # TODO:?: why would a target node in the working_embedding not be in the target_nodeset?
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.

to_target is an identity, so we can remove it right?


pruned_embedding = {
to_source(k): to_target(v) for k, v in working_embedding.items() if v in target_nodeset
} # TODO:?: why would a target node in the working_embedding not be in the target_nodeset?
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.

Perhaps its required because you want to cast it to a ZephyrNode, and we can't be sure that's the format provided - the function name could be changed and type hinting improved?

from minorminer.utils.parallel_embeddings import find_sublattice_embeddings

import dwave_networkx as dnx
from dwave_networkx.generators._zephyr_playground import zephyr_quotient_search
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.

Requires correction, this won't run

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.

Corrected. Thanks for noticing!

if target.graph["labels"] != "coordinate":
raise ValueError("target graph has unknown labelling scheme")

def to_target(n: ZephyrNode) -> ZephyrNode:
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.

to_target is an identity. Is it really necessary? Is it necessary to caste non-ZephyrNode to ZephyrNode for example? If so the type hint is incorrect.

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 to_target is an identity because the case where the graph is not in coordinate labels has already been handled by previous logic. The case where the graph is already in coordinate labels, this function needs to be an identity.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is this ever something other than just an identity? Otherwise I still don't see this as doing anything.

working_embedding: Embedding = {n: n for n in source_nodes}
else:
# Convert chain format to internal single-node format
working_embedding = {k: v[0] for k, v in embedding.items()}
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 we need to check a few things here?

  • Should we check there is a key for every source node? If it is absent throw an error that a key is missing in the candidate (and document requirement)
  • Check the length of all values is 1.

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.

these are both checked in _validate_search_parameters and _ensure_coordinate_source.

This routine starts from a source Zephyr graph with ``m`` rows and ``tp`` tiles,
and maps it into a target Zephyr graph with the same ``m`` rows and ``t >= tp``
tiles. It is designed for defective targets where a direct identity map may lose
nodes or 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.

I think we should highlight limitations at the end of this first paragraph:

"Since a greedy method is used for embedding search, it is possible it fails to find a 1:1 embedding where one is viable. A complete method such as :code:minorminer.subgraph.find_subgraph may be more appropriate in a scenario such as this, especially with customization of parameters to the target families. Similarly, when defect rates are high direct use of :code: minorminer.find_embedding may be a more efficient strategy."

I'm teeing up the other module I'd like to add here which is find_subgraph enhanced by S,T pair specific node_labels.

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.

Added.

@jackraymond
Copy link
Copy Markdown
Contributor

I'd like to pair this module with a tool that uses source and target graph specific knowledge to accelerate find_subgraph(). This tool is not zephyr specific, but it would be good if it could be placed alongside this tool. For this reason, could we consider relabeling the subdirectory as 'embedding_methods' rather than 'zephyr_embedding_methods' (less specific), or perhaps 'processor_graph_embedding_methods' (if we wanted to be more specific)

…en updated when an incomplete source was provided. Clarify that the zephyr_quotient_embedding_search is a greedy method and has limitations in the docstrings.

Co-authored-by: Copilot <copilot@github.com>
@VolodyaCO
Copy link
Copy Markdown
Author

@jackraymond the example has been updated and runs correctly as expected.

Vladimir Vargas Calderón and others added 2 commits April 27, 2026 17:38
Co-authored-by: Copilot <copilot@github.com>
…on during the example.

Co-authored-by: Copilot <copilot@github.com>
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.

LGTM

Comment thread tests/test_zephyr_quotient_search.py Outdated
import networkx as nx
import numpy as np
from dwave_networkx import zephyr_graph
from dwave_networkx.generators._zephyr_playground import (
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 is a bad import

Copy link
Copy Markdown

@thisac thisac left a comment

Choose a reason for hiding this comment

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

Might be good to check out dwavesystems/dwave-graphs#266 and coordinate with @mahdiehmalekian, specifically with regards to some of the zephyr utility functions and checks.

I'll have a closer look at the main zephyr_quotient_search function and tests.

# See the License for the specific language governing permissions and
# limitations under the License.

from .zephyr_quotient_embedding_search import *
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Recommended to use absolute imports instead of relative.


__all__ = ["zephyr_quotient_search"]

ZephyrNode = tuple[int, int, int, int, int] # (u, w, k, j, z) coordinate tuple
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Since this will likely be moved over to dwave-graphs at some point, and there's already a ZephyrCoord tuple-like being added there, it's probably good to coordinate a bit and make sure that these two are compatible.

__all__ = ["zephyr_quotient_search"]

ZephyrNode = tuple[int, int, int, int, int] # (u, w, k, j, z) coordinate tuple
Embedding = dict[ZephyrNode, ZephyrNode] # Internal single-node format
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems simple enough to just keep dict[ZephyrNode, ZephyrNode] as the type.

Comment on lines +29 to +30
YieldType = Literal["node", "edge", "rail-edge"]
QuotientSearchType = Literal["by_quotient_rail", "by_quotient_node", "by_rail_then_node"]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Type-aliases are good when not overused. I agree with @kevinchern that it can make code less readable. With that said, I'm fine with these, except perhaps Embedding, EmbeddingChain since they're not very clearly named.

Comment on lines +140 to +142
if embedding is not None and not isinstance(embedding, dict):
raise TypeError(f"embedding must be a dictionary when provided. Got {type(embedding)}")
if embedding is not None:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

One less check.

Suggested change
if embedding is not None and not isinstance(embedding, dict):
raise TypeError(f"embedding must be a dictionary when provided. Got {type(embedding)}")
if embedding is not None:
if embedding is not None:
if not isinstance(embedding, dict):
raise TypeError(f"embedding must be a dictionary when provided. Got {type(embedding)}")

Comment on lines +204 to +209
Returns:
tuple[nx.Graph, set[ZephyrNode], Callable[[ZephyrNode], int | ZephyrNode]]:
``(_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 space.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Shouldn't be indented on returns. Also, could remove all types in docstrings since type-hints are used.

"graph, along with any other missing nodes.", UserWarning
)
_source.add_nodes_from(source_nodes)
breakpoint()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Leftover from debugging?

Suggested change
breakpoint()

Comment on lines +255 to +259
# If the labels are coordinate. Then we just return the graph as is and the identity function
# for to_source:

def to_source(n: ZephyrNode) -> ZephyrNode:
return n
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I don't really see the point of this?

Comment on lines +284 to +285
tuple[nx.Graph, Callable[[ZephyrNode], int | ZephyrNode]]:
``(_target, to_target)`` where ``_target`` is coordinate-labelled.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remove indentation.

if target.graph["labels"] != "coordinate":
raise ValueError("target graph has unknown labelling scheme")

def to_target(n: ZephyrNode) -> ZephyrNode:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is this ever something other than just an identity? Otherwise I still don't see this as doing anything.

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