From 6a1cc7df3ce03d3bb364e99ad2c2158c5af62f1d Mon Sep 17 00:00:00 2001 From: Sefunmi Ashiru Date: Tue, 6 Jan 2026 17:31:06 +0000 Subject: [PATCH 1/7] Add GraphMonomorphismMapper initial placement strategy --- cirq-core/cirq/__init__.py | 1 + .../cirq/protocols/json_test_data/spec.py | 1 + cirq-core/cirq/transformers/__init__.py | 1 + .../cirq/transformers/routing/__init__.py | 4 + .../routing/graph_monomorphism_mapper.py | 190 ++++++++++++++++++ .../routing/graph_monomorphism_mapper_test.py | 135 +++++++++++++ 6 files changed, 332 insertions(+) create mode 100644 cirq-core/cirq/transformers/routing/graph_monomorphism_mapper.py create mode 100644 cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py diff --git a/cirq-core/cirq/__init__.py b/cirq-core/cirq/__init__.py index 95d711e4ef8..6c3a0c46ee6 100644 --- a/cirq-core/cirq/__init__.py +++ b/cirq-core/cirq/__init__.py @@ -368,6 +368,7 @@ index_tags as index_tags, is_negligible_turn as is_negligible_turn, LineInitialMapper as LineInitialMapper, + GraphMonomorphismMapper as GraphMonomorphismMapper, MappingManager as MappingManager, map_clean_and_borrowable_qubits as map_clean_and_borrowable_qubits, map_moments as map_moments, diff --git a/cirq-core/cirq/protocols/json_test_data/spec.py b/cirq-core/cirq/protocols/json_test_data/spec.py index 4e4dbb4bafd..08a91d29f2f 100644 --- a/cirq-core/cirq/protocols/json_test_data/spec.py +++ b/cirq-core/cirq/protocols/json_test_data/spec.py @@ -91,6 +91,7 @@ # Routing utilities 'HardCodedInitialMapper', 'LineInitialMapper', + 'GraphMonomorphismMapper', 'MappingManager', 'RouteCQC', # Qubit Managers, diff --git a/cirq-core/cirq/transformers/__init__.py b/cirq-core/cirq/transformers/__init__.py index a6e37eb0882..b2172f249d0 100644 --- a/cirq-core/cirq/transformers/__init__.py +++ b/cirq-core/cirq/transformers/__init__.py @@ -51,6 +51,7 @@ AbstractInitialMapper as AbstractInitialMapper, HardCodedInitialMapper as HardCodedInitialMapper, LineInitialMapper as LineInitialMapper, + GraphMonomorphismMapper as GraphMonomorphismMapper, MappingManager as MappingManager, RouteCQC as RouteCQC, routed_circuit_with_mapping as routed_circuit_with_mapping, diff --git a/cirq-core/cirq/transformers/routing/__init__.py b/cirq-core/cirq/transformers/routing/__init__.py index 33d836d881c..baa3c9f2153 100644 --- a/cirq-core/cirq/transformers/routing/__init__.py +++ b/cirq-core/cirq/transformers/routing/__init__.py @@ -23,6 +23,10 @@ from cirq.transformers.routing.line_initial_mapper import LineInitialMapper as LineInitialMapper +from cirq.transformers.routing.graph_monomorphism_mapper import ( + GraphMonomorphismMapper as GraphMonomorphismMapper, +) + from cirq.transformers.routing.route_circuit_cqc import RouteCQC as RouteCQC from cirq.transformers.routing.visualize_routed_circuit import ( diff --git a/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper.py b/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper.py new file mode 100644 index 00000000000..25654aa50fe --- /dev/null +++ b/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper.py @@ -0,0 +1,190 @@ +# Copyright 2026 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Maps logical to physical qubits by finding a graph monomorphism into the device graph. + +This mapper builds an *interaction graph* from the circuit (logical qubits as nodes, and an edge +between two logical qubits if they participate in any 2-qubit operation). It then attempts to find +an injective mapping of logical nodes into physical nodes such that every logical edge maps to a +physical edge (i.e. a subgraph/monomorphism embedding). + +If multiple embeddings exist, it chooses the one that (heuristically) is most "central" on the +device by minimizing total distance-to-center and then (tie-break) maximizing total degree. + +If no monomorphism exists, it raises ValueError (so a router can fall back to a different strategy). +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Dict, Iterable, Optional, Tuple + +import networkx as nx + +from cirq import protocols, value +from cirq.transformers.routing import initial_mapper + +if TYPE_CHECKING: + import cirq + + +@value.value_equality +class GraphMonomorphismMapper(initial_mapper.AbstractInitialMapper): + """Places logical qubits onto physical qubits via graph monomorphism (subgraph embedding).""" + + def __init__( + self, + device_graph: nx.Graph, + *, + max_matches: int = 5_000, + timeout_steps: Optional[int] = None, + ) -> None: + """ + Args: + device_graph: Device connectivity graph (physical qubits are nodes). + If directed, we treat it as undirected for the purposes of placement. + max_matches: Max number of candidate embeddings to consider before picking best-so-far. + timeout_steps: Optional hard cap on internal iteration steps (additional guardrail). + """ + # For placement, treat connectivity as undirected adjacency. + # (If you need strict directionality, you'd do a DiGraph monomorphism with edge constraints.) + if nx.is_directed(device_graph): + ug = nx.Graph() + ug.add_nodes_from(sorted(list(device_graph.nodes(data=True)))) + ug.add_edges_from(sorted(tuple(sorted(e)) for e in device_graph.edges)) + self.device_graph = ug + else: + ug = nx.Graph() + ug.add_nodes_from(sorted(list(device_graph.nodes(data=True)))) + ug.add_edges_from(sorted(tuple(sorted(e)) for e in device_graph.edges)) + self.device_graph = ug + + # Center is used only as a heuristic scoring anchor. + # (nx.center returns nodes with minimum eccentricity.) + self.center = nx.center(self.device_graph)[0] + self.max_matches = int(max_matches) + self.timeout_steps = None if timeout_steps is None else int(timeout_steps) + + def _make_circuit_interaction_graph(self, circuit: cirq.AbstractCircuit) -> nx.Graph: + """Builds the circuit interaction graph from 2-qubit operations.""" + g = nx.Graph() + logical_qubits = sorted(circuit.all_qubits()) + g.add_nodes_from(logical_qubits) + + for op in circuit.all_operations(): + if protocols.num_qubits(op) != 2: + continue + q0, q1 = op.qubits + if q0 == q1: + continue + # Coalesce repeated interactions into a single simple edge. + g.add_edge(q0, q1) + + return g + + def _score_embedding( + self, + logical_to_physical: Dict["cirq.Qid", "cirq.Qid"], + dist_to_center: Dict["cirq.Qid", int], + ) -> Tuple[int, int]: + """ + Lower score is better. + + Primary: sum of distances to center (more compact/central). + Tie-break: -sum degrees (prefer higher-degree placements). + """ + total_dist = 0 + total_degree = 0 + for _, pq in logical_to_physical.items(): + total_dist += dist_to_center.get(pq, 10**9) + total_degree += self.device_graph.degree(pq) + return (total_dist, -total_degree) + + def initial_mapping(self, circuit: cirq.AbstractCircuit) -> Dict["cirq.Qid", "cirq.Qid"]: + """ + Returns: + dict mapping logical qubits -> physical qubits. + + Raises: + ValueError if no graph monomorphism embedding exists. + """ + circuit_g = self._make_circuit_interaction_graph(circuit) + + # Trivial fast path: no qubits. + if circuit_g.number_of_nodes() == 0: + return {} + + # If the circuit has more logical qubits than device has physical qubits, impossible. + if circuit_g.number_of_nodes() > self.device_graph.number_of_nodes(): + raise ValueError("Circuit has more qubits than the device graph can host.") + + # Precompute distances to the device center for scoring. + dist_to_center = dict(nx.single_source_shortest_path_length(self.device_graph, self.center)) + + # NetworkX subgraph isomorphism: + # GraphMatcher(G_big, G_small).subgraph_isomorphisms_iter() + # yields mappings: big_node -> small_node. + matcher = nx.algorithms.isomorphism.GraphMatcher(self.device_graph, circuit_g) + + best_map: Optional[Dict["cirq.Qid", "cirq.Qid"]] = None + best_score: Optional[Tuple[int, int]] = None + + steps = 0 + matches_seen = 0 + + for big_to_small in matcher.subgraph_isomorphisms_iter(): + # Optional guardrails. + steps += 1 + if self.timeout_steps is not None and steps > self.timeout_steps: + break + + # Invert to get logical -> physical. + # big_to_small: physical -> logical + logical_to_physical = {lq: pq for pq, lq in big_to_small.items()} + + # Ensure all logical nodes are mapped (they should be, but be defensive). + if len(logical_to_physical) != circuit_g.number_of_nodes(): + continue + + score = self._score_embedding(logical_to_physical, dist_to_center) + if best_score is None or score < best_score: + best_score = score + best_map = logical_to_physical + + matches_seen += 1 + if matches_seen >= self.max_matches: + break + + if best_map is None: + raise ValueError( + "No graph monomorphism embedding found for circuit interaction graph " + "into device graph." + ) + + return best_map + + def _value_equality_values_(self): + return ( + tuple(self.device_graph.nodes), + tuple(self.device_graph.edges), + self.max_matches, + self.timeout_steps, + ) + + def __repr__(self): + graph_type = type(self.device_graph).__name__ + return ( + "cirq.GraphMonomorphismMapper(" + f"nx.{graph_type}({dict(self.device_graph.adjacency())}), " + f"max_matches={self.max_matches}, timeout_steps={self.timeout_steps})" + ) \ No newline at end of file diff --git a/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py b/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py new file mode 100644 index 00000000000..8b649b5bb99 --- /dev/null +++ b/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py @@ -0,0 +1,135 @@ +# Copyright 2026 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import networkx as nx +import pytest + +import cirq + +# Unit tests: keep N small and VF2 bounded. +_MAX_MATCHES = 1 # stop at first embedding +_TIMEOUT_STEPS = 2_000 # hard cap search work + + +def construct_star_circuit_5q(): + # Interaction graph edges: (1-3), (2-3), (4-3). Center has degree 3. + return cirq.Circuit( + [ + cirq.Moment(cirq.CNOT(cirq.NamedQubit("1"), cirq.NamedQubit("3"))), + cirq.Moment(cirq.CNOT(cirq.NamedQubit("2"), cirq.NamedQubit("3"))), + cirq.Moment(cirq.CNOT(cirq.NamedQubit("4"), cirq.NamedQubit("3"))), + cirq.Moment(cirq.X(cirq.NamedQubit("5"))), + ] + ) + + +def construct_path_circuit(k: int): + q = cirq.LineQubit.range(k) + return cirq.Circuit(cirq.CNOT(q[i], q[i + 1]) for i in range(k - 1)) + + +def _interaction_graph(circuit: cirq.AbstractCircuit) -> nx.Graph: + g = nx.Graph() + g.add_nodes_from(sorted(circuit.all_qubits())) + for op in circuit.all_operations(): + if cirq.num_qubits(op) != 2: + continue + a, b = op.qubits + if a != b: + g.add_edge(a, b) + return g + + +def _assert_is_monomorphism( + circuit: cirq.AbstractCircuit, device_graph: nx.Graph, mapping: dict[cirq.Qid, cirq.Qid] +) -> None: + # Injective + total. + assert len(set(mapping.values())) == len(mapping.values()) + assert set(mapping.keys()) == set(circuit.all_qubits()) + + # Edge-preserving. + cg = _interaction_graph(circuit) + dg = device_graph.to_undirected() if nx.is_directed(device_graph) else device_graph + for u, v in cg.edges: + pu, pv = mapping[u], mapping[v] + assert dg.has_edge(pu, pv) + + +def test_path_embeds_on_small_grid() -> None: + # Small (<= 10) always embeddable on 4x4 grid, should be very fast. + circuit = construct_path_circuit(10) + device = cirq.testing.construct_grid_device(4, 4) # 16 physical + g = device.metadata.nx_graph + + mapper = cirq.GraphMonomorphismMapper(g, max_matches=_MAX_MATCHES, timeout_steps=_TIMEOUT_STEPS) + mapping = mapper.initial_mapping(circuit) + + _assert_is_monomorphism(circuit, g, mapping) + device.validate_circuit(circuit.transform_qubits(mapping)) + + +def test_star_embeds_on_small_grid() -> None: + # Degree-3 center requires a physical node with degree >= 3 (grid has it). + circuit = construct_star_circuit_5q() + device = cirq.testing.construct_grid_device(4, 4) + g = device.metadata.nx_graph + + mapper = cirq.GraphMonomorphismMapper(g, max_matches=_MAX_MATCHES, timeout_steps=_TIMEOUT_STEPS) + mapping = mapper.initial_mapping(circuit) + + _assert_is_monomorphism(circuit, g, mapping) + device.validate_circuit(circuit.transform_qubits(mapping)) + + +def test_star_fails_on_ring() -> None: + # Ring max degree is 2, but star requires degree 3 -> impossible. + circuit = construct_star_circuit_5q() + device = cirq.testing.construct_ring_device(10, directed=True) + g = device.metadata.nx_graph + + mapper = cirq.GraphMonomorphismMapper(g, max_matches=_MAX_MATCHES, timeout_steps=500) + with pytest.raises(ValueError, match="No graph monomorphism embedding found"): + mapper.initial_mapping(circuit) + + +def test_path_embeds_on_ring() -> None: + # A path (max degree 2) should embed on a ring. + circuit = construct_path_circuit(6) + device = cirq.testing.construct_ring_device(10, directed=True) + g = device.metadata.nx_graph + + mapper = cirq.GraphMonomorphismMapper(g, max_matches=_MAX_MATCHES, timeout_steps=_TIMEOUT_STEPS) + mapping = mapper.initial_mapping(circuit) + + _assert_is_monomorphism(circuit, g, mapping) + device.validate_circuit(circuit.transform_qubits(mapping)) + + +def test_more_logical_than_physical_fails_fast() -> None: + # Keep small, but verify the early size check. + circuit = construct_path_circuit(17) # 17 logical + device = cirq.testing.construct_grid_device(4, 4) # 16 physical + g = device.metadata.nx_graph + + mapper = cirq.GraphMonomorphismMapper(g, max_matches=_MAX_MATCHES, timeout_steps=_TIMEOUT_STEPS) + with pytest.raises(ValueError, match="more qubits than the device graph can host"): + mapper.initial_mapping(circuit) + + +def test_repr() -> None: + g = cirq.testing.construct_grid_device(4, 4).metadata.nx_graph + mapper = cirq.GraphMonomorphismMapper(g, max_matches=123, timeout_steps=456) + cirq.testing.assert_equivalent_repr(mapper, setup_code="import cirq\nimport networkx as nx") \ No newline at end of file From 2466b601f5d1e40d1f892de1c8369b389a06f438 Mon Sep 17 00:00:00 2001 From: Sefunmi Ashiru Date: Tue, 6 Jan 2026 18:04:34 +0000 Subject: [PATCH 2/7] Add formatting GraphMonomorphismMapper initial placement strategy --- .../cirq/transformers/routing/graph_monomorphism_mapper.py | 4 ++-- .../transformers/routing/graph_monomorphism_mapper_test.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper.py b/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper.py index 25654aa50fe..6348d1f2a83 100644 --- a/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper.py +++ b/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper.py @@ -27,7 +27,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict, Iterable, Optional, Tuple +from typing import Dict, Iterable, Optional, Tuple, TYPE_CHECKING import networkx as nx @@ -187,4 +187,4 @@ def __repr__(self): "cirq.GraphMonomorphismMapper(" f"nx.{graph_type}({dict(self.device_graph.adjacency())}), " f"max_matches={self.max_matches}, timeout_steps={self.timeout_steps})" - ) \ No newline at end of file + ) diff --git a/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py b/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py index 8b649b5bb99..600486c4c2d 100644 --- a/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py +++ b/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py @@ -20,8 +20,8 @@ import cirq # Unit tests: keep N small and VF2 bounded. -_MAX_MATCHES = 1 # stop at first embedding -_TIMEOUT_STEPS = 2_000 # hard cap search work +_MAX_MATCHES = 1 # stop at first embedding +_TIMEOUT_STEPS = 2_000 # hard cap search work def construct_star_circuit_5q(): @@ -132,4 +132,4 @@ def test_more_logical_than_physical_fails_fast() -> None: def test_repr() -> None: g = cirq.testing.construct_grid_device(4, 4).metadata.nx_graph mapper = cirq.GraphMonomorphismMapper(g, max_matches=123, timeout_steps=456) - cirq.testing.assert_equivalent_repr(mapper, setup_code="import cirq\nimport networkx as nx") \ No newline at end of file + cirq.testing.assert_equivalent_repr(mapper, setup_code="import cirq\nimport networkx as nx") From 1d8e0d78efe49d24a31b674e0a3efe1d41acd9c5 Mon Sep 17 00:00:00 2001 From: Sefunmi Ashiru Date: Tue, 6 Jan 2026 18:05:06 +0000 Subject: [PATCH 3/7] Trigger CLA recheck From fd3e42b5fff8bd76808b9c01f80e10d01c9ce8d4 Mon Sep 17 00:00:00 2001 From: Sefunmi Ashiru Date: Wed, 7 Jan 2026 18:10:01 +0000 Subject: [PATCH 4/7] fix lint errors --- .../routing/graph_monomorphism_mapper.py | 71 +++++++++++-------- .../routing/graph_monomorphism_mapper_test.py | 2 +- 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper.py b/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper.py index 6348d1f2a83..facc5c210dd 100644 --- a/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper.py +++ b/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper.py @@ -1,4 +1,4 @@ -# Copyright 2026 +# Copyright 2022 The Cirq Developers # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ from __future__ import annotations -from typing import Dict, Iterable, Optional, Tuple, TYPE_CHECKING +from typing import TYPE_CHECKING, Optional import networkx as nx @@ -49,25 +49,21 @@ def __init__( max_matches: int = 5_000, timeout_steps: Optional[int] = None, ) -> None: - """ + """Initializes a GraphMonomorphismMapper. + Args: - device_graph: Device connectivity graph (physical qubits are nodes). - If directed, we treat it as undirected for the purposes of placement. - max_matches: Max number of candidate embeddings to consider before picking best-so-far. + device_graph: Device connectivity graph (physical qubits are nodes). If directed, it is + treated as undirected for the purposes of placement. + max_matches: Maximum number of candidate embeddings to consider before choosing the best + mapping found so far. timeout_steps: Optional hard cap on internal iteration steps (additional guardrail). """ # For placement, treat connectivity as undirected adjacency. - # (If you need strict directionality, you'd do a DiGraph monomorphism with edge constraints.) - if nx.is_directed(device_graph): - ug = nx.Graph() - ug.add_nodes_from(sorted(list(device_graph.nodes(data=True)))) - ug.add_edges_from(sorted(tuple(sorted(e)) for e in device_graph.edges)) - self.device_graph = ug - else: - ug = nx.Graph() - ug.add_nodes_from(sorted(list(device_graph.nodes(data=True)))) - ug.add_edges_from(sorted(tuple(sorted(e)) for e in device_graph.edges)) - self.device_graph = ug + # If you need strict directionality, you'd do a DiGraph monomorphism with edge constraints. + ug = nx.Graph() + ug.add_nodes_from(sorted(list(device_graph.nodes(data=True)))) + ug.add_edges_from(sorted(tuple(sorted(e)) for e in device_graph.edges)) + self.device_graph = ug # Center is used only as a heuristic scoring anchor. # (nx.center returns nodes with minimum eccentricity.) @@ -94,14 +90,21 @@ def _make_circuit_interaction_graph(self, circuit: cirq.AbstractCircuit) -> nx.G def _score_embedding( self, - logical_to_physical: Dict["cirq.Qid", "cirq.Qid"], - dist_to_center: Dict["cirq.Qid", int], - ) -> Tuple[int, int]: - """ - Lower score is better. + logical_to_physical: dict[cirq.Qid, cirq.Qid], + dist_to_center: dict[cirq.Qid, int], + ) -> tuple[int, int]: + """Scores an embedding; lower score is better. + + The score is a tuple used for lexicographic comparison: + (sum of distances to the device center, -sum of device degrees). + + Args: + logical_to_physical: Mapping from logical qubits to physical qubits. + dist_to_center: Precomputed shortest-path distance from each physical qubit to the + device center. - Primary: sum of distances to center (more compact/central). - Tie-break: -sum degrees (prefer higher-degree placements). + Returns: + A score tuple. Lower is preferred; ties are broken by favoring higher-degree placements. """ total_dist = 0 total_degree = 0 @@ -110,13 +113,19 @@ def _score_embedding( total_degree += self.device_graph.degree(pq) return (total_dist, -total_degree) - def initial_mapping(self, circuit: cirq.AbstractCircuit) -> Dict["cirq.Qid", "cirq.Qid"]: - """ + def initial_mapping(self, circuit: cirq.AbstractCircuit) -> dict[cirq.Qid, cirq.Qid]: + """Finds an initial mapping by embedding the circuit interaction graph into the device graph. + + Args: + circuit: The input circuit with logical qubits. + Returns: - dict mapping logical qubits -> physical qubits. + A dictionary mapping logical qubits in the circuit (keys) to physical qubits on the + device (values). Raises: - ValueError if no graph monomorphism embedding exists. + ValueError: If no graph monomorphism embedding exists, or if the circuit has more qubits + than the device graph can host. """ circuit_g = self._make_circuit_interaction_graph(circuit) @@ -136,8 +145,8 @@ def initial_mapping(self, circuit: cirq.AbstractCircuit) -> Dict["cirq.Qid", "ci # yields mappings: big_node -> small_node. matcher = nx.algorithms.isomorphism.GraphMatcher(self.device_graph, circuit_g) - best_map: Optional[Dict["cirq.Qid", "cirq.Qid"]] = None - best_score: Optional[Tuple[int, int]] = None + best_map: Optional[dict[cirq.Qid, cirq.Qid]] = None + best_score: Optional[tuple[int, int]] = None steps = 0 matches_seen = 0 @@ -187,4 +196,4 @@ def __repr__(self): "cirq.GraphMonomorphismMapper(" f"nx.{graph_type}({dict(self.device_graph.adjacency())}), " f"max_matches={self.max_matches}, timeout_steps={self.timeout_steps})" - ) + ) \ No newline at end of file diff --git a/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py b/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py index 600486c4c2d..8fd494622a0 100644 --- a/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py +++ b/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py @@ -1,4 +1,4 @@ -# Copyright 2026 +# Copyright 2022 The Cirq Developers # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From ba1d938d75c3762acd1a04f2eea6140e3c499d96 Mon Sep 17 00:00:00 2001 From: Sefunmi Ashiru Date: Wed, 7 Jan 2026 18:20:07 +0000 Subject: [PATCH 5/7] added test for coverage --- .../routing/graph_monomorphism_mapper_test.py | 97 +++++++++++++++++-- 1 file changed, 89 insertions(+), 8 deletions(-) diff --git a/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py b/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py index 8fd494622a0..ee8f1efdf59 100644 --- a/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py +++ b/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py @@ -14,6 +14,9 @@ from __future__ import annotations +from collections.abc import Iterator +from typing import Any + import networkx as nx import pytest @@ -69,7 +72,6 @@ def _assert_is_monomorphism( def test_path_embeds_on_small_grid() -> None: - # Small (<= 10) always embeddable on 4x4 grid, should be very fast. circuit = construct_path_circuit(10) device = cirq.testing.construct_grid_device(4, 4) # 16 physical g = device.metadata.nx_graph @@ -82,7 +84,6 @@ def test_path_embeds_on_small_grid() -> None: def test_star_embeds_on_small_grid() -> None: - # Degree-3 center requires a physical node with degree >= 3 (grid has it). circuit = construct_star_circuit_5q() device = cirq.testing.construct_grid_device(4, 4) g = device.metadata.nx_graph @@ -95,7 +96,6 @@ def test_star_embeds_on_small_grid() -> None: def test_star_fails_on_ring() -> None: - # Ring max degree is 2, but star requires degree 3 -> impossible. circuit = construct_star_circuit_5q() device = cirq.testing.construct_ring_device(10, directed=True) g = device.metadata.nx_graph @@ -106,7 +106,6 @@ def test_star_fails_on_ring() -> None: def test_path_embeds_on_ring() -> None: - # A path (max degree 2) should embed on a ring. circuit = construct_path_circuit(6) device = cirq.testing.construct_ring_device(10, directed=True) g = device.metadata.nx_graph @@ -119,7 +118,6 @@ def test_path_embeds_on_ring() -> None: def test_more_logical_than_physical_fails_fast() -> None: - # Keep small, but verify the early size check. circuit = construct_path_circuit(17) # 17 logical device = cirq.testing.construct_grid_device(4, 4) # 16 physical g = device.metadata.nx_graph @@ -129,7 +127,90 @@ def test_more_logical_than_physical_fails_fast() -> None: mapper.initial_mapping(circuit) -def test_repr() -> None: - g = cirq.testing.construct_grid_device(4, 4).metadata.nx_graph +def test_empty_circuit_returns_empty_mapping() -> None: + g = cirq.testing.construct_grid_device(2, 2).metadata.nx_graph + mapper = cirq.GraphMonomorphismMapper(g, max_matches=_MAX_MATCHES, timeout_steps=_TIMEOUT_STEPS) + assert mapper.initial_mapping(cirq.Circuit()) == {} + + +def test_make_interaction_graph_skips_non_two_qubit_ops() -> None: + g = cirq.testing.construct_grid_device(2, 2).metadata.nx_graph + mapper = cirq.GraphMonomorphismMapper(g, max_matches=_MAX_MATCHES, timeout_steps=_TIMEOUT_STEPS) + + q = cirq.NamedQubit("q") + a, b = cirq.NamedQubit("a"), cirq.NamedQubit("b") + + circuit = cirq.Circuit( + cirq.X(q), # 1q -> ignored + cirq.CZ(a, b), # 2q -> included + ) + + cg = mapper._make_circuit_interaction_graph(circuit) + assert cg.has_edge(a, b) + assert q in cg.nodes + assert cg.degree(q) == 0 + + +def test_timeout_steps_breaks_out_and_raises(monkeypatch: pytest.MonkeyPatch) -> None: + # Forces coverage of: + # if self.timeout_steps is not None and steps > self.timeout_steps: break + g = cirq.testing.construct_grid_device(2, 2).metadata.nx_graph + mapper = cirq.GraphMonomorphismMapper(g, max_matches=10_000, timeout_steps=0) + + circuit = construct_path_circuit(2) + + class FakeMatcher: + def subgraph_isomorphisms_iter(self) -> Iterator[dict[cirq.Qid, cirq.Qid]]: + # Infinite-ish generator; timeout should stop it immediately. + while True: + yield {} + + monkeypatch.setattr(nx.algorithms.isomorphism, "GraphMatcher", lambda *_args, **_kw: FakeMatcher()) + + with pytest.raises(ValueError, match="No graph monomorphism embedding found"): + mapper.initial_mapping(circuit) + + +def test_defensive_incomplete_mapping_is_skipped(monkeypatch: pytest.MonkeyPatch) -> None: + # Covers: + # if len(logical_to_physical) != circuit_g.number_of_nodes(): continue + g = cirq.testing.construct_grid_device(2, 2).metadata.nx_graph + mapper = cirq.GraphMonomorphismMapper(g, max_matches=10, timeout_steps=_TIMEOUT_STEPS) + + circuit = construct_path_circuit(2) # 2 logical nodes + + class FakeMatcher: + def subgraph_isomorphisms_iter(self) -> Iterator[dict[cirq.Qid, cirq.Qid]]: + # physical -> logical mapping with only 1 logical node -> after inversion it's incomplete + yield {cirq.LineQubit(0): cirq.LineQubit(0)} + + monkeypatch.setattr(nx.algorithms.isomorphism, "GraphMatcher", lambda *_args, **_kw: FakeMatcher()) + + with pytest.raises(ValueError, match="No graph monomorphism embedding found"): + mapper.initial_mapping(circuit) + + +def test_score_embedding_uses_default_large_distance() -> None: + g = cirq.testing.construct_grid_device(2, 2).metadata.nx_graph + mapper = cirq.GraphMonomorphismMapper(g, max_matches=_MAX_MATCHES, timeout_steps=_TIMEOUT_STEPS) + + # Pick an actual physical node from the graph. + pq = next(iter(mapper.device_graph.nodes)) + lq = cirq.NamedQubit("lq") + + # dist_to_center missing pq -> should fall back to 10**9. + score = mapper._score_embedding({lq: pq}, dist_to_center={}) + assert score[0] >= 10**9 + + +def test_value_equality_values_and_repr() -> None: + g = cirq.testing.construct_grid_device(2, 2).metadata.nx_graph mapper = cirq.GraphMonomorphismMapper(g, max_matches=123, timeout_steps=456) - cirq.testing.assert_equivalent_repr(mapper, setup_code="import cirq\nimport networkx as nx") + + # Covers _value_equality_values_ explicitly. + vals = mapper._value_equality_values_() + assert vals[2] == 123 + assert vals[3] == 456 + + # Covers __repr__ (and keeps the existing equivalent repr check). + cirq.testing.assert_equivalent_repr(mapper, setup_code="import cirq\nimport networkx as nx") \ No newline at end of file From 0d9a24a8ff0cf0f7570fe1c9b0e48831eed5e789 Mon Sep 17 00:00:00 2001 From: Sefunmi Ashiru Date: Wed, 7 Jan 2026 19:43:08 +0000 Subject: [PATCH 6/7] added formating, lint, and test coverage changes --- .../routing/graph_monomorphism_mapper.py | 11 ++-- .../routing/graph_monomorphism_mapper_test.py | 54 ++++++++++++++----- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper.py b/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper.py index facc5c210dd..5473ccc5d2d 100644 --- a/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper.py +++ b/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper.py @@ -27,7 +27,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import Optional, TYPE_CHECKING import networkx as nx @@ -89,9 +89,7 @@ def _make_circuit_interaction_graph(self, circuit: cirq.AbstractCircuit) -> nx.G return g def _score_embedding( - self, - logical_to_physical: dict[cirq.Qid, cirq.Qid], - dist_to_center: dict[cirq.Qid, int], + self, logical_to_physical: dict[cirq.Qid, cirq.Qid], dist_to_center: dict[cirq.Qid, int] ) -> tuple[int, int]: """Scores an embedding; lower score is better. @@ -114,7 +112,8 @@ def _score_embedding( return (total_dist, -total_degree) def initial_mapping(self, circuit: cirq.AbstractCircuit) -> dict[cirq.Qid, cirq.Qid]: - """Finds an initial mapping by embedding the circuit interaction graph into the device graph. + """Finds an initial mapping by embedding the circuit interaction graph + into the device graph. Args: circuit: The input circuit with logical qubits. @@ -196,4 +195,4 @@ def __repr__(self): "cirq.GraphMonomorphismMapper(" f"nx.{graph_type}({dict(self.device_graph.adjacency())}), " f"max_matches={self.max_matches}, timeout_steps={self.timeout_steps})" - ) \ No newline at end of file + ) diff --git a/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py b/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py index ee8f1efdf59..97028917ed5 100644 --- a/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py +++ b/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py @@ -15,7 +15,7 @@ from __future__ import annotations from collections.abc import Iterator -from typing import Any +from typing import cast import networkx as nx import pytest @@ -133,21 +133,46 @@ def test_empty_circuit_returns_empty_mapping() -> None: assert mapper.initial_mapping(cirq.Circuit()) == {} -def test_make_interaction_graph_skips_non_two_qubit_ops() -> None: +def test_make_interaction_graph_skips_self_edges() -> None: g = cirq.testing.construct_grid_device(2, 2).metadata.nx_graph mapper = cirq.GraphMonomorphismMapper(g, max_matches=_MAX_MATCHES, timeout_steps=_TIMEOUT_STEPS) + class TwoQubitSelfOp(cirq.Operation): + def __init__(self, q: cirq.Qid) -> None: + self._q = q + + @property + def qubits(self) -> tuple[cirq.Qid, cirq.Qid]: + return (self._q, self._q) + + def with_qubits(self, *new_qubits: cirq.Qid) -> "TwoQubitSelfOp": + assert len(new_qubits) == 1 + return TwoQubitSelfOp(new_qubits[0]) + + def _num_qubits_(self) -> int: + return 2 + q = cirq.NamedQubit("q") a, b = cirq.NamedQubit("a"), cirq.NamedQubit("b") - circuit = cirq.Circuit( - cirq.X(q), # 1q -> ignored - cirq.CZ(a, b), # 2q -> included - ) + ops = [TwoQubitSelfOp(q), cirq.CZ(a, b)] + qubits = {q, a, b} + + class FakeCircuit: + def all_qubits(self): + return qubits - cg = mapper._make_circuit_interaction_graph(circuit) + def all_operations(self): + return iter(ops) + + # cover with_qubits for incremental coverage + _ = TwoQubitSelfOp(q).with_qubits(q) + + cg = mapper._make_circuit_interaction_graph(cast(cirq.AbstractCircuit, FakeCircuit())) + + assert a in cg.nodes and b in cg.nodes and q in cg.nodes assert cg.has_edge(a, b) - assert q in cg.nodes + assert not cg.has_edge(q, q) assert cg.degree(q) == 0 @@ -165,7 +190,9 @@ def subgraph_isomorphisms_iter(self) -> Iterator[dict[cirq.Qid, cirq.Qid]]: while True: yield {} - monkeypatch.setattr(nx.algorithms.isomorphism, "GraphMatcher", lambda *_args, **_kw: FakeMatcher()) + monkeypatch.setattr( + nx.algorithms.isomorphism, "GraphMatcher", lambda *_args, **_kw: FakeMatcher() + ) with pytest.raises(ValueError, match="No graph monomorphism embedding found"): mapper.initial_mapping(circuit) @@ -181,10 +208,13 @@ def test_defensive_incomplete_mapping_is_skipped(monkeypatch: pytest.MonkeyPatch class FakeMatcher: def subgraph_isomorphisms_iter(self) -> Iterator[dict[cirq.Qid, cirq.Qid]]: - # physical -> logical mapping with only 1 logical node -> after inversion it's incomplete + # physical -> logical mapping with only 1 logical node + # -> after inversion it's incomplete yield {cirq.LineQubit(0): cirq.LineQubit(0)} - monkeypatch.setattr(nx.algorithms.isomorphism, "GraphMatcher", lambda *_args, **_kw: FakeMatcher()) + monkeypatch.setattr( + nx.algorithms.isomorphism, "GraphMatcher", lambda *_args, **_kw: FakeMatcher() + ) with pytest.raises(ValueError, match="No graph monomorphism embedding found"): mapper.initial_mapping(circuit) @@ -213,4 +243,4 @@ def test_value_equality_values_and_repr() -> None: assert vals[3] == 456 # Covers __repr__ (and keeps the existing equivalent repr check). - cirq.testing.assert_equivalent_repr(mapper, setup_code="import cirq\nimport networkx as nx") \ No newline at end of file + cirq.testing.assert_equivalent_repr(mapper, setup_code="import cirq\nimport networkx as nx") From 119ad8c75645e827c939e6a8efacb54207b070ab Mon Sep 17 00:00:00 2001 From: Sefunmi Ashiru Date: Thu, 8 Jan 2026 18:39:38 +0000 Subject: [PATCH 7/7] added fix for lint test --- .../cirq/transformers/routing/graph_monomorphism_mapper_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py b/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py index 97028917ed5..e9fe62dcb83 100644 --- a/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py +++ b/cirq-core/cirq/transformers/routing/graph_monomorphism_mapper_test.py @@ -145,7 +145,7 @@ def __init__(self, q: cirq.Qid) -> None: def qubits(self) -> tuple[cirq.Qid, cirq.Qid]: return (self._q, self._q) - def with_qubits(self, *new_qubits: cirq.Qid) -> "TwoQubitSelfOp": + def with_qubits(self, *new_qubits: cirq.Qid) -> TwoQubitSelfOp: assert len(new_qubits) == 1 return TwoQubitSelfOp(new_qubits[0])