cirq-core 1.7.0.dev20251015163856__py3-none-any.whl → 1.7.0.dev20251017194258__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cirq-core might be problematic. Click here for more details.

cirq/_version.py CHANGED
@@ -28,4 +28,4 @@ if sys.version_info < (3, 11, 0): # pragma: no cover
28
28
  'of Cirq (e.g. "python -m pip install cirq==1.5.0")'
29
29
  )
30
30
 
31
- __version__ = "1.7.0.dev20251015163856"
31
+ __version__ = "1.7.0.dev20251017194258"
cirq/_version_test.py CHANGED
@@ -3,4 +3,4 @@ import cirq
3
3
 
4
4
 
5
5
  def test_version() -> None:
6
- assert cirq.__version__ == "1.7.0.dev20251015163856"
6
+ assert cirq.__version__ == "1.7.0.dev20251017194258"
@@ -0,0 +1,230 @@
1
+ # Copyright 2025 The Cirq Developers
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Defines a connected component of operations, to be used in merge transformers."""
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Callable, cast, Sequence, TYPE_CHECKING
20
+
21
+ from scipy.cluster.hierarchy import DisjointSet
22
+
23
+ from cirq import ops, protocols
24
+
25
+ if TYPE_CHECKING:
26
+ import cirq
27
+
28
+
29
+ class Component:
30
+ """Internal representation for a connected component of operations."""
31
+
32
+ # Circuit moment containing the component
33
+ moment_id: int
34
+ # Union of all op qubits in the component
35
+ qubits: frozenset[cirq.Qid]
36
+ # Union of all measurement keys in the component
37
+ mkeys: frozenset[cirq.MeasurementKey]
38
+ # Union of all control keys in the component
39
+ ckeys: frozenset[cirq.MeasurementKey]
40
+ # Initial operation in the component
41
+ op: cirq.Operation
42
+
43
+ # True if the component can be merged with other components
44
+ is_mergeable: bool
45
+
46
+ def __init__(self, op: cirq.Operation, moment_id: int, is_mergeable=True):
47
+ """Initializes a singleton component."""
48
+ self.op = op
49
+ self.is_mergeable = is_mergeable
50
+ self.moment_id = moment_id
51
+ self.qubits = frozenset(op.qubits)
52
+ self.mkeys = protocols.measurement_key_objs(op)
53
+ self.ckeys = protocols.control_keys(op)
54
+
55
+
56
+ class ComponentWithOps(Component):
57
+ """Component that keeps track of operations."""
58
+
59
+ # List of all operations in the component
60
+ ops: list[cirq.Operation]
61
+
62
+ def __init__(self, op: cirq.Operation, moment_id: int, is_mergeable=True):
63
+ super().__init__(op, moment_id, is_mergeable)
64
+ self.ops = [op]
65
+
66
+
67
+ class ComponentWithCircuitOp(Component):
68
+ """Component that keeps track of operations as a CircuitOperation."""
69
+
70
+ # CircuitOperation containing all the operations in the component,
71
+ # or a single Operation if the component is a singleton
72
+ circuit_op: cirq.Operation
73
+
74
+ def __init__(self, op: cirq.Operation, moment_id: int, is_mergeable=True):
75
+ super().__init__(op, moment_id, is_mergeable)
76
+ self.circuit_op = op
77
+
78
+
79
+ class ComponentSet:
80
+ """Represents a set of mergeable components of operations."""
81
+
82
+ _comp_type: type[Component]
83
+
84
+ _disjoint_set: DisjointSet
85
+
86
+ # Callable to decide if a component is mergeable
87
+ _is_mergeable: Callable[[cirq.Operation], bool]
88
+
89
+ # List of components in creation order
90
+ _components: list[Component]
91
+
92
+ def __init__(self, is_mergeable: Callable[[cirq.Operation], bool]):
93
+ self._is_mergeable = is_mergeable
94
+ self._disjoint_set = DisjointSet()
95
+ self._components = []
96
+ self._comp_type = Component
97
+
98
+ def new_component(self, op: cirq.Operation, moment_id: int, is_mergeable=True) -> Component:
99
+ """Creates a new component and adds it to the set."""
100
+ c = self._comp_type(op, moment_id, is_mergeable and self._is_mergeable(op))
101
+ self._disjoint_set.add(c)
102
+ self._components.append(c)
103
+ return c
104
+
105
+ def components(self) -> list[Component]:
106
+ """Returns the initial components in creation order."""
107
+ return self._components
108
+
109
+ def find(self, x: Component) -> Component:
110
+ """Finds the representative for a merged component."""
111
+ return self._disjoint_set[x]
112
+
113
+ def merge(self, x: Component, y: Component, merge_left=True) -> Component | None:
114
+ """Attempts to merge two components.
115
+
116
+ If merge_left is True, y is merged into x, and the representative will keep
117
+ y's moment. If merge_left is False, x is merged into y, and the representative
118
+ will keep y's moment.
119
+
120
+ Args:
121
+ x: First component to merge.
122
+ y: Second component to merge.
123
+ merge_left: True to keep x's moment for the merged component, False to
124
+ keep y's moment for the merged component.
125
+
126
+ Returns:
127
+ None, if the components can't be merged.
128
+ Otherwise the new component representative.
129
+ """
130
+ x = self._disjoint_set[x]
131
+ y = self._disjoint_set[y]
132
+
133
+ if not x.is_mergeable or not y.is_mergeable:
134
+ return None
135
+
136
+ if not self._disjoint_set.merge(x, y):
137
+ return x
138
+
139
+ root = self._disjoint_set[x]
140
+ root.moment_id = x.moment_id if merge_left else y.moment_id
141
+ root.qubits = x.qubits.union(y.qubits)
142
+ root.mkeys = x.mkeys.union(y.mkeys)
143
+ root.ckeys = x.ckeys.union(y.ckeys)
144
+
145
+ return root
146
+
147
+
148
+ class ComponentWithOpsSet(ComponentSet):
149
+ """Represents a set of mergeable components, where each component tracks operations."""
150
+
151
+ # Callable that returns if two components can be merged based on their operations
152
+ _can_merge: Callable[[Sequence[cirq.Operation], Sequence[cirq.Operation]], bool]
153
+
154
+ def __init__(
155
+ self,
156
+ is_mergeable: Callable[[cirq.Operation], bool],
157
+ can_merge: Callable[[Sequence[cirq.Operation], Sequence[cirq.Operation]], bool],
158
+ ):
159
+ super().__init__(is_mergeable)
160
+ self._can_merge = can_merge
161
+ self._comp_type = ComponentWithOps
162
+
163
+ def merge(self, x: Component, y: Component, merge_left=True) -> Component | None:
164
+ """Attempts to merge two components.
165
+
166
+ Returns:
167
+ None if can_merge is False or the merge doesn't succeed, otherwise the
168
+ new representative. The representative will have ops = x.ops + y.ops.
169
+ """
170
+ x = cast(ComponentWithOps, self._disjoint_set[x])
171
+ y = cast(ComponentWithOps, self._disjoint_set[y])
172
+
173
+ if x is y:
174
+ return x
175
+
176
+ if not x.is_mergeable or not y.is_mergeable or not self._can_merge(x.ops, y.ops):
177
+ return None
178
+
179
+ root = cast(ComponentWithOps, super().merge(x, y, merge_left))
180
+ root.ops = x.ops + y.ops
181
+ # Clear the ops list in the non-representative component to avoid duplication
182
+ other = y if x is root else x
183
+ other.ops = []
184
+ return root
185
+
186
+
187
+ class ComponentWithCircuitOpSet(ComponentSet):
188
+ """Represents a set of mergeable components, with operations as a CircuitOperation."""
189
+
190
+ # Callable that merges CircuitOperations from two components
191
+ _merge_func: Callable[[ops.Operation, ops.Operation], ops.Operation | None]
192
+
193
+ def __init__(
194
+ self,
195
+ is_mergeable: Callable[[cirq.Operation], bool],
196
+ merge_func: Callable[[ops.Operation, ops.Operation], ops.Operation | None],
197
+ ):
198
+ super().__init__(is_mergeable)
199
+ self._merge_func = merge_func
200
+ self._comp_type = ComponentWithCircuitOp
201
+
202
+ def merge(self, x: Component, y: Component, merge_left=True) -> Component | None:
203
+ """Attempts to merge two components.
204
+
205
+ Returns:
206
+ None if merge_func returns None or the merge doesn't succeed,
207
+ otherwise the new representative.
208
+ """
209
+ x = cast(ComponentWithCircuitOp, self._disjoint_set[x])
210
+ y = cast(ComponentWithCircuitOp, self._disjoint_set[y])
211
+
212
+ if x is y:
213
+ return x
214
+
215
+ if not x.is_mergeable or not y.is_mergeable:
216
+ return None
217
+
218
+ new_op = self._merge_func(x.circuit_op, y.circuit_op)
219
+ if not new_op:
220
+ return None
221
+
222
+ root = cast(ComponentWithCircuitOp, super().merge(x, y, merge_left))
223
+
224
+ root.circuit_op = new_op
225
+ # The merge_func can be arbitrary, so we need to recompute the component properties
226
+ root.qubits = frozenset(new_op.qubits)
227
+ root.mkeys = protocols.measurement_key_objs(new_op)
228
+ root.ckeys = protocols.control_keys(new_op)
229
+
230
+ return root
@@ -0,0 +1,200 @@
1
+ # Copyright 2025 The Cirq Developers
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import pytest
18
+
19
+ import cirq
20
+ from cirq.transformers._connected_component import (
21
+ ComponentSet,
22
+ ComponentWithCircuitOpSet,
23
+ ComponentWithOpsSet,
24
+ )
25
+
26
+
27
+ def _always_mergeable(_: cirq.Operation) -> bool:
28
+ return True
29
+
30
+
31
+ def _never_mergeable(_: cirq.Operation) -> bool:
32
+ return False
33
+
34
+
35
+ def _always_can_merge(_ops1: list[cirq.Operation], _ops2: list[cirq.Operation]) -> bool:
36
+ return True
37
+
38
+
39
+ def _never_can_merge(_ops1: list[cirq.Operation], _ops2: list[cirq.Operation]) -> bool:
40
+ return False
41
+
42
+
43
+ def _merge_as_first_operation(op1: cirq.Operation, _op2: cirq.Operation) -> cirq.Operation:
44
+ return op1
45
+
46
+
47
+ def _merge_never_successful(op1: cirq.Operation, _op2: cirq.Operation) -> None:
48
+ return None
49
+
50
+
51
+ def test_find_returns_itself_for_singleton():
52
+ cset = ComponentSet(_always_mergeable)
53
+
54
+ q = cirq.NamedQubit('x')
55
+ c = cset.new_component(op=cirq.X(q), moment_id=0)
56
+ assert cset.find(c) is c
57
+
58
+
59
+ def test_merge_components():
60
+ cset = ComponentSet(_always_mergeable)
61
+
62
+ q = cirq.NamedQubit('x')
63
+ c = [cset.new_component(op=cirq.X(q), moment_id=i) for i in range(5)]
64
+ cset.merge(c[1], c[0])
65
+ cset.merge(c[2], c[1])
66
+ cset.merge(c[4], c[3])
67
+ cset.merge(c[3], c[0])
68
+
69
+ for i in range(5):
70
+ assert cset.find(c[i]) is cset.find(c[0])
71
+
72
+
73
+ def test_merge_same_component():
74
+ cset = ComponentSet(_always_mergeable)
75
+
76
+ q = cirq.NamedQubit('x')
77
+ c = [cset.new_component(op=cirq.X(q), moment_id=i) for i in range(3)]
78
+ cset.merge(c[1], c[0])
79
+ cset.merge(c[2], c[1])
80
+
81
+ root = cset.find(c[0])
82
+
83
+ assert cset.merge(c[0], c[2]) is root
84
+
85
+
86
+ def test_merge_returns_None_if_one_component_is_not_mergeable():
87
+ cset = ComponentSet(_always_mergeable)
88
+
89
+ q = cirq.NamedQubit('x')
90
+ c0 = cset.new_component(op=cirq.X(q), moment_id=0, is_mergeable=True)
91
+ c1 = cset.new_component(op=cirq.X(q), moment_id=1, is_mergeable=False)
92
+ assert cset.merge(c0, c1) is None
93
+
94
+
95
+ def test_cset_merge_returns_None_if_is_mergeable_is_false():
96
+ q = cirq.NamedQubit('x')
97
+ cset = ComponentSet(is_mergeable=_never_mergeable)
98
+
99
+ c0 = cset.new_component(op=cirq.X(q), moment_id=0, is_mergeable=True)
100
+ c1 = cset.new_component(op=cirq.X(q), moment_id=1, is_mergeable=True)
101
+ assert cset.merge(c0, c1) is None
102
+
103
+
104
+ @pytest.mark.parametrize("merge_left,expected_moment_id", [(True, 0), (False, 1)])
105
+ def test_merge_qubits_with_merge_left(merge_left: bool, expected_moment_id: int) -> None:
106
+ cset = ComponentSet(_always_mergeable)
107
+
108
+ q0 = cirq.NamedQubit('x')
109
+ q1 = cirq.NamedQubit('y')
110
+ c0 = cset.new_component(op=cirq.X(q0), moment_id=0)
111
+ c1 = cset.new_component(op=cirq.X(q1), moment_id=1)
112
+ c2 = cset.new_component(op=cirq.X(q1), moment_id=2)
113
+ cset.merge(c1, c2)
114
+ cset.merge(c0, c1, merge_left=merge_left)
115
+ assert cset.find(c1).qubits == frozenset([q0, q1])
116
+ assert cset.find(c1).moment_id == expected_moment_id
117
+
118
+
119
+ def test_component_with_ops_merge():
120
+ cset = ComponentWithOpsSet(_always_mergeable, _always_can_merge)
121
+
122
+ q = cirq.LineQubit.range(3)
123
+ ops = [cirq.X(q[i]) for i in range(3)]
124
+ c = [cset.new_component(op=ops[i], moment_id=i) for i in range(3)]
125
+ cset.merge(c[0], c[1])
126
+ cset.merge(c[1], c[2])
127
+ assert cset.find(c[0]).ops == ops
128
+ # check merge of indirectly merged components does not make a difference
129
+ assert cset.merge(c[0], c[2]).ops == ops
130
+
131
+
132
+ def test_component_with_ops_merge_when_merge_fails():
133
+ cset = ComponentWithOpsSet(_always_mergeable, _never_can_merge)
134
+
135
+ q = cirq.LineQubit.range(3)
136
+ ops = [cirq.X(q[i]) for i in range(3)]
137
+ c = [cset.new_component(op=ops[i], moment_id=i) for i in range(3)]
138
+
139
+ cset.merge(c[0], c[1])
140
+ cset.merge(c[1], c[2])
141
+ # No merge happened
142
+ for i in range(3):
143
+ assert cset.find(c[i]) is c[i]
144
+
145
+
146
+ def test_component_with_ops_merge_when_is_mergeable_is_false():
147
+ cset = ComponentWithOpsSet(_never_mergeable, _always_can_merge)
148
+
149
+ q = cirq.LineQubit.range(3)
150
+ ops = [cirq.X(q[i]) for i in range(3)]
151
+ c = [cset.new_component(op=ops[i], moment_id=i) for i in range(3)]
152
+
153
+ cset.merge(c[0], c[1])
154
+ cset.merge(c[1], c[2])
155
+ # No merge happened
156
+ for i in range(3):
157
+ assert cset.find(c[i]) is c[i]
158
+
159
+
160
+ def test_component_with_circuit_op_merge():
161
+ cset = ComponentWithCircuitOpSet(_always_mergeable, _merge_as_first_operation)
162
+
163
+ q = cirq.LineQubit.range(3)
164
+ ops = [cirq.X(q[i]) for i in range(3)]
165
+ c = [cset.new_component(op=ops[i], moment_id=i) for i in range(3)]
166
+
167
+ cset.merge(c[0], c[1])
168
+ cset.merge(c[1], c[2])
169
+ for i in range(3):
170
+ assert cset.find(c[i]).circuit_op == ops[0]
171
+ # check merge of indirectly merged components does not make a difference
172
+ assert cset.merge(c[0], c[2]) is cset.find(c[1])
173
+
174
+
175
+ def test_component_with_circuit_op_merge_func_is_none():
176
+ cset = ComponentWithCircuitOpSet(_always_mergeable, _merge_never_successful)
177
+
178
+ q = cirq.LineQubit.range(3)
179
+ ops = [cirq.X(q[i]) for i in range(3)]
180
+ c = [cset.new_component(op=ops[i], moment_id=i) for i in range(3)]
181
+
182
+ cset.merge(c[0], c[1])
183
+ cset.merge(c[1], c[2])
184
+ # No merge happened
185
+ for i in range(3):
186
+ assert cset.find(c[i]) is c[i]
187
+
188
+
189
+ def test_component_with_circuit_op_merge_when_is_mergeable_is_false():
190
+ cset = ComponentWithCircuitOpSet(_never_mergeable, _merge_as_first_operation)
191
+
192
+ q = cirq.LineQubit.range(3)
193
+ ops = [cirq.X(q[i]) for i in range(3)]
194
+ c = [cset.new_component(op=ops[i], moment_id=i) for i in range(3)]
195
+
196
+ cset.merge(c[0], c[1])
197
+ cset.merge(c[1], c[2])
198
+ # No merge happened
199
+ for i in range(3):
200
+ assert cset.find(c[i]) is c[i]
@@ -17,12 +17,21 @@
17
17
  from __future__ import annotations
18
18
 
19
19
  import bisect
20
+ import copy
20
21
  import dataclasses
22
+ import itertools
21
23
  from collections import defaultdict
22
24
  from typing import Callable, cast, Hashable, Sequence, TYPE_CHECKING
23
25
 
24
26
  from cirq import circuits, ops, protocols
25
27
  from cirq.circuits.circuit import CIRCUIT_TYPE
28
+ from cirq.transformers._connected_component import (
29
+ Component,
30
+ ComponentSet,
31
+ ComponentWithCircuitOp,
32
+ ComponentWithCircuitOpSet,
33
+ ComponentWithOpsSet,
34
+ )
26
35
 
27
36
  if TYPE_CHECKING:
28
37
  import cirq
@@ -49,6 +58,33 @@ def _create_target_circuit_type(ops: ops.OP_TREE, target_circuit: CIRCUIT_TYPE)
49
58
  return cast(CIRCUIT_TYPE, circuits.Circuit(ops))
50
59
 
51
60
 
61
+ def _insort_last(indices: list[int], value: int) -> None:
62
+ """Insert value to a sorted list of indices.
63
+
64
+ Optimized for the majority case when the value is the last in the list.
65
+ """
66
+ if indices and value < indices[-1]:
67
+ bisect.insort(indices, value)
68
+ else:
69
+ indices.append(value)
70
+
71
+
72
+ def _remove_last(indices: list[int], value: int) -> None:
73
+ """Remove value from a sorted list of indices.
74
+
75
+ Optimized for the majority case when the value is the last in the list.
76
+
77
+ Raises:
78
+ ValueError: If the value is not in the list.
79
+ """
80
+ if indices and (
81
+ indices[pos := -1] == value or indices[pos := bisect.bisect_left(indices, value)] == value
82
+ ):
83
+ indices.pop(pos)
84
+ else:
85
+ raise ValueError("The value is not in the list of indices") # pragma: no cover
86
+
87
+
52
88
  def map_moments(
53
89
  circuit: CIRCUIT_TYPE,
54
90
  map_func: Callable[[circuits.Moment, int], circuits.Moment | Sequence[circuits.Moment]],
@@ -282,17 +318,23 @@ def map_operations_and_unroll(
282
318
 
283
319
  @dataclasses.dataclass
284
320
  class _MergedCircuit:
285
- """An optimized internal representation of a circuit, tailored for `cirq.merge_operations`
321
+ """An optimized internal representation of a circuit, tailored for merge operations.
322
+
323
+ Operations are represented as mergeable components.
324
+ Each component has a moment id, a set of qubits, a set of measurement keys, and a set of
325
+ control keys. The moment id is the index of the moment that contains the component.
286
326
 
287
327
  Attributes:
288
- qubit_indexes: Mapping from qubits to (sorted) list of moment indexes containing operations
289
- acting on the qubit.
290
- mkey_indexes: Mapping from measurement keys to (sorted) list of moment indexes containing
291
- measurement operations with the same key.
292
- ckey_indexes: Mapping from measurement keys to (sorted) list of moment indexes containing
293
- classically controlled operations controlled on the same key.
294
- ops_by_index: List of circuit moments containing operations. We use a dictionary instead
295
- of a set to store operations to preserve insertion order.
328
+ qubit_indexes: Mapping from qubits to (sorted) list of component moments containing
329
+ operations acting on the qubit.
330
+ mkey_indexes: Mapping from measurement keys to (sorted) list of component moments
331
+ containing measurement operations with the same key.
332
+ ckey_indexes: Mapping from measurement keys to (sorted) list of component moments
333
+ containing classically controlled operations controlled on the same key.
334
+ components_by_index: List of components indexed by moment id.
335
+ For a moment id, we use a dictionary to keep track of the
336
+ components in the moment. The dictionary instead of a set is used to preserve
337
+ insertion order and the dictionary's values are intentionally unused.
296
338
  """
297
339
 
298
340
  qubit_indexes: dict[cirq.Qid, list[int]] = dataclasses.field(
@@ -304,54 +346,203 @@ class _MergedCircuit:
304
346
  ckey_indexes: dict[cirq.MeasurementKey, list[int]] = dataclasses.field(
305
347
  default_factory=lambda: defaultdict(lambda: [-1])
306
348
  )
307
- ops_by_index: list[dict[cirq.Operation, int]] = dataclasses.field(default_factory=list)
349
+ components_by_index: list[dict[Component, None]] = dataclasses.field(default_factory=list)
308
350
 
309
351
  def append_empty_moment(self) -> None:
310
- self.ops_by_index.append({})
311
-
312
- def add_op_to_moment(self, moment_index: int, op: cirq.Operation) -> None:
313
- self.ops_by_index[moment_index][op] = 0
314
- for q in op.qubits:
315
- if moment_index > self.qubit_indexes[q][-1]:
316
- self.qubit_indexes[q].append(moment_index)
317
- else:
318
- bisect.insort(self.qubit_indexes[q], moment_index)
319
- for mkey in protocols.measurement_key_objs(op):
320
- bisect.insort(self.mkey_indexes[mkey], moment_index)
321
- for ckey in protocols.control_keys(op):
322
- bisect.insort(self.ckey_indexes[ckey], moment_index)
323
-
324
- def remove_op_from_moment(self, moment_index: int, op: cirq.Operation) -> None:
325
- self.ops_by_index[moment_index].pop(op)
326
- for q in op.qubits:
327
- if self.qubit_indexes[q][-1] == moment_index:
328
- self.qubit_indexes[q].pop()
329
- else:
330
- self.qubit_indexes[q].remove(moment_index)
331
- for mkey in protocols.measurement_key_objs(op):
332
- self.mkey_indexes[mkey].remove(moment_index)
333
- for ckey in protocols.control_keys(op):
334
- self.ckey_indexes[ckey].remove(moment_index)
335
-
336
- def get_mergeable_ops(
337
- self, op: cirq.Operation, op_qs: set[cirq.Qid]
338
- ) -> tuple[int, list[cirq.Operation]]:
339
- # Find the index of previous moment which can be merged with `op`.
340
- idx = max([self.qubit_indexes[q][-1] for q in op_qs], default=-1)
341
- idx = max([idx] + [self.mkey_indexes[ckey][-1] for ckey in protocols.control_keys(op)])
352
+ self.components_by_index.append({})
353
+
354
+ def add_component(self, c: Component) -> None:
355
+ """Adds a new components to merged circuit."""
356
+ self.components_by_index[c.moment_id][c] = None
357
+ for q in c.qubits:
358
+ _insort_last(self.qubit_indexes[q], c.moment_id)
359
+ for mkey in c.mkeys:
360
+ _insort_last(self.mkey_indexes[mkey], c.moment_id)
361
+ for ckey in c.ckeys:
362
+ _insort_last(self.ckey_indexes[ckey], c.moment_id)
363
+
364
+ def remove_component(self, c: Component, c_data: Component) -> None:
365
+ """Removes a component from the merged circuit.
366
+
367
+ Args:
368
+ c: Reference to the component to be removed.
369
+ c_data: Copy of the data in c before any component merges involving c
370
+ (this is necessary as component merges alter the component data).
371
+ """
372
+ self.components_by_index[c_data.moment_id].pop(c)
373
+ for q in c_data.qubits:
374
+ _remove_last(self.qubit_indexes[q], c_data.moment_id)
375
+ for mkey in c_data.mkeys:
376
+ _remove_last(self.mkey_indexes[mkey], c_data.moment_id)
377
+ for ckey in c_data.ckeys:
378
+ _remove_last(self.ckey_indexes[ckey], c_data.moment_id)
379
+
380
+ def get_mergeable_components(self, c: Component, c_qs: set[cirq.Qid]) -> list[Component]:
381
+ """Finds all components that can be merged with c.
382
+
383
+ Args:
384
+ c: Component to be merged with existing components.
385
+ c_qs: Subset of c.qubits used to decide which components are mergeable.
386
+
387
+ Returns:
388
+ List of mergeable components.
389
+ """
390
+ # Find the index of previous moment which can be merged with `c`.
342
391
  idx = max(
343
- [idx] + [self.ckey_indexes[mkey][-1] for mkey in protocols.measurement_key_objs(op)]
392
+ itertools.chain(
393
+ (self.qubit_indexes[q][-1] for q in c_qs),
394
+ (self.mkey_indexes[ckey][-1] for ckey in c.ckeys),
395
+ (self.ckey_indexes[mkey][-1] for mkey in c.mkeys),
396
+ ),
397
+ default=-1,
344
398
  )
345
- # Return the set of overlapping ops in moment with index `idx`.
399
+ # Return the set of overlapping components in moment with index `idx`.
346
400
  if idx == -1:
347
- return idx, []
401
+ return []
402
+
403
+ return [c for c in self.components_by_index[idx] if not c_qs.isdisjoint(c.qubits)]
404
+
405
+ def get_cirq_circuit(self, cset: ComponentSet, merged_circuit_op_tag: str) -> cirq.Circuit:
406
+ """Returns the merged circuit.
407
+
408
+ Args:
409
+ cset: Disjoint set data structure containing the components.
410
+ merged_circuit_op_tag: Tag to use for CircuitOperations.
411
+
412
+ Returns:
413
+ The circuit with merged components as a CircuitOperation.
414
+ """
415
+ component_ops: dict[Component, list[cirq.Operation]] = defaultdict(list)
416
+
417
+ # Traverse the components in creation order and collect operations
418
+ for c in cset.components():
419
+ root = cset.find(c)
420
+ component_ops[root].append(c.op)
421
+
422
+ moments = []
423
+ for m in self.components_by_index:
424
+ ops = []
425
+ for c in m.keys():
426
+ if isinstance(c, ComponentWithCircuitOp):
427
+ ops.append(c.circuit_op)
428
+ continue
429
+ if len(component_ops[c]) == 1:
430
+ ops.append(component_ops[c][0])
431
+ else:
432
+ ops.append(
433
+ circuits.CircuitOperation(
434
+ circuits.FrozenCircuit(component_ops[c])
435
+ ).with_tags(merged_circuit_op_tag)
436
+ )
437
+ moments.append(circuits.Moment(ops))
438
+ return circuits.Circuit(moments)
348
439
 
349
- return idx, [
350
- left_op for left_op in self.ops_by_index[idx] if not op_qs.isdisjoint(left_op.qubits)
351
- ]
352
440
 
353
- def get_cirq_circuit(self) -> cirq.Circuit:
354
- return circuits.Circuit(circuits.Moment(m.keys()) for m in self.ops_by_index)
441
+ def _merge_operations_impl(
442
+ circuit: CIRCUIT_TYPE,
443
+ cset: ComponentSet,
444
+ *,
445
+ merged_circuit_op_tag: str = "Merged connected component",
446
+ tags_to_ignore: Sequence[Hashable] = (),
447
+ deep: bool = False,
448
+ ) -> CIRCUIT_TYPE:
449
+ """Merges operations in a circuit.
450
+
451
+ Two operations op1 and op2 are merge-able if
452
+ - There is no other operations between op1 and op2 in the circuit
453
+ - is_subset(op1.qubits, op2.qubits) or is_subset(op2.qubits, op1.qubits)
454
+
455
+ The method iterates on the input circuit moment-by-moment from left to right and attempts
456
+ to repeatedly merge each operation in the latest moment with all the corresponding merge-able
457
+ operations to its left.
458
+
459
+ Operations are wrapped in a component and then cset.merge() is called to merge two
460
+ components.
461
+
462
+ If op1 and op2 are merged, both op1 and op2 are deleted from the circuit and
463
+ the merged component is inserted at the index corresponding to the larger
464
+ of op1/op2. If both op1 and op2 act on the same number of qubits, the merged component is
465
+ inserted in the smaller moment index to minimize circuit depth.
466
+
467
+ At the end every component with more than one operation is replaced by a CircuitOperation.
468
+
469
+ Args:
470
+ circuit: Input circuit to apply the transformations on. The input circuit is not mutated.
471
+ cset: Disjoint set data structure that is used to create and merge components.
472
+ merged_circuit_op_tag: Tag used for CircuitOperations created from merged components.
473
+ tags_to_ignore: Sequence of tags which should be ignored during the merge: operations with
474
+ these tags will not be merged.
475
+ deep: If true, the transformer primitive will be recursively applied to all circuits
476
+ wrapped inside circuit operations.
477
+
478
+
479
+ Returns:
480
+ Copy of input circuit with merged operations.
481
+ """
482
+ tags_to_ignore_set = set(tags_to_ignore)
483
+
484
+ merged_circuit = _MergedCircuit()
485
+ for moment_idx, current_moment in enumerate(circuit):
486
+ merged_circuit.append_empty_moment()
487
+ for op in sorted(current_moment.operations, key=lambda op: op.qubits):
488
+ if (
489
+ deep
490
+ and isinstance(op.untagged, circuits.CircuitOperation)
491
+ and tags_to_ignore_set.isdisjoint(op.tags)
492
+ ):
493
+ op_untagged = op.untagged
494
+ merged_op = op_untagged.replace(
495
+ circuit=_merge_operations_impl(
496
+ op_untagged.circuit,
497
+ cset,
498
+ merged_circuit_op_tag=merged_circuit_op_tag,
499
+ tags_to_ignore=tags_to_ignore,
500
+ deep=True,
501
+ )
502
+ ).with_tags(*op.tags)
503
+ c = cset.new_component(merged_op, moment_idx, is_mergeable=False)
504
+ merged_circuit.add_component(c)
505
+ continue
506
+
507
+ c = cset.new_component(
508
+ op, moment_idx, is_mergeable=tags_to_ignore_set.isdisjoint(op.tags)
509
+ )
510
+ if not c.is_mergeable:
511
+ merged_circuit.add_component(c)
512
+ continue
513
+
514
+ c_qs = set(c.qubits)
515
+ left_comp = merged_circuit.get_mergeable_components(c, c_qs)
516
+ if len(left_comp) == 1 and c_qs.issubset(left_comp[0].qubits):
517
+ # Make a shallow copy of the left component data before merge
518
+ left_c_data = copy.copy(left_comp[0])
519
+ # Case-1: Try to merge c with the larger component on the left.
520
+ new_comp = cset.merge(left_comp[0], c, merge_left=True)
521
+ if new_comp is not None:
522
+ merged_circuit.remove_component(left_comp[0], left_c_data)
523
+ merged_circuit.add_component(new_comp)
524
+ else:
525
+ merged_circuit.add_component(c)
526
+ continue
527
+
528
+ while left_comp and c_qs:
529
+ # Case-2: left_c will merge right into `c` whenever possible.
530
+ for left_c in left_comp:
531
+ is_merged = False
532
+ if c_qs.issuperset(left_c.qubits):
533
+ # Make a shallow copy of the left component data before merge
534
+ left_c_data = copy.copy(left_c)
535
+ # Try to merge left_c into c
536
+ new_comp = cset.merge(left_c, c, merge_left=False)
537
+ if new_comp is not None:
538
+ merged_circuit.remove_component(left_c, left_c_data)
539
+ c, is_merged = new_comp, True
540
+ if not is_merged:
541
+ c_qs -= left_c.qubits
542
+ left_comp = merged_circuit.get_mergeable_components(c, c_qs)
543
+ merged_circuit.add_component(c)
544
+ ret_circuit = merged_circuit.get_cirq_circuit(cset, merged_circuit_op_tag)
545
+ return _to_target_circuit_type(ret_circuit, circuit)
355
546
 
356
547
 
357
548
  def merge_operations(
@@ -407,12 +598,8 @@ def merge_operations(
407
598
  ValueError if the merged operation acts on new qubits outside the set of qubits
408
599
  corresponding to the original operations to be merged.
409
600
  """
410
- _circuit_op_tag = "_internal_tag_to_mark_circuit_ops_in_circuit"
411
- tags_to_ignore_set = set(tags_to_ignore) | {_circuit_op_tag}
412
601
 
413
602
  def apply_merge_func(op1: ops.Operation, op2: ops.Operation) -> ops.Operation | None:
414
- if not all(tags_to_ignore_set.isdisjoint(op.tags) for op in [op1, op2]):
415
- return None
416
603
  new_op = merge_func(op1, op2)
417
604
  qubit_set = frozenset(op1.qubits + op2.qubits)
418
605
  if new_op is not None and not qubit_set.issuperset(new_op.qubits):
@@ -422,63 +609,15 @@ def merge_operations(
422
609
  )
423
610
  return new_op
424
611
 
425
- merged_circuit = _MergedCircuit()
426
- for moment_idx, current_moment in enumerate(cast(list['cirq.Moment'], circuit)):
427
- merged_circuit.append_empty_moment()
428
- for op in sorted(current_moment.operations, key=lambda op: op.qubits):
429
- if (
430
- deep
431
- and isinstance(op.untagged, circuits.CircuitOperation)
432
- and tags_to_ignore_set.isdisjoint(op.tags)
433
- ):
434
- op_untagged = op.untagged
435
- merged_circuit.add_op_to_moment(
436
- moment_idx,
437
- op_untagged.replace(
438
- circuit=merge_operations(
439
- op_untagged.circuit,
440
- merge_func,
441
- tags_to_ignore=tags_to_ignore,
442
- deep=True,
443
- )
444
- ).with_tags(*op.tags, _circuit_op_tag),
445
- )
446
- continue
447
-
448
- op_qs = set(op.qubits)
449
- left_idx, left_ops = merged_circuit.get_mergeable_ops(op, op_qs)
450
- if len(left_ops) == 1 and op_qs.issubset(left_ops[0].qubits):
451
- # Case-1: Try to merge op with the larger operation on the left.
452
- new_op = apply_merge_func(left_ops[0], op)
453
- if new_op is not None:
454
- merged_circuit.remove_op_from_moment(left_idx, left_ops[0])
455
- merged_circuit.add_op_to_moment(left_idx, new_op)
456
- else:
457
- merged_circuit.add_op_to_moment(moment_idx, op)
458
- continue
612
+ def is_mergeable(_: cirq.Operation):
613
+ return True
459
614
 
460
- while left_ops and op_qs:
461
- # Case-2: left_ops will merge right into `op` whenever possible.
462
- for left_op in left_ops:
463
- is_merged = False
464
- if op_qs.issuperset(left_op.qubits):
465
- # Try to merge left_op into op
466
- new_op = apply_merge_func(left_op, op)
467
- if new_op is not None:
468
- merged_circuit.remove_op_from_moment(left_idx, left_op)
469
- op, is_merged = new_op, True
470
- if not is_merged:
471
- op_qs -= frozenset(left_op.qubits)
472
- left_idx, left_ops = merged_circuit.get_mergeable_ops(op, op_qs)
473
- merged_circuit.add_op_to_moment(moment_idx, op)
474
- ret_circuit = merged_circuit.get_cirq_circuit()
475
- if deep:
476
- ret_circuit = map_operations(
477
- ret_circuit,
478
- lambda o, _: o.untagged.with_tags(*(set(o.tags) - {_circuit_op_tag})),
479
- deep=True,
480
- )
481
- return _to_target_circuit_type(ret_circuit, circuit)
615
+ return _merge_operations_impl(
616
+ circuit,
617
+ ComponentWithCircuitOpSet(is_mergeable, apply_merge_func),
618
+ tags_to_ignore=tags_to_ignore,
619
+ deep=deep,
620
+ )
482
621
 
483
622
 
484
623
  def merge_operations_to_circuit_op(
@@ -491,10 +630,9 @@ def merge_operations_to_circuit_op(
491
630
  ) -> CIRCUIT_TYPE:
492
631
  """Merges connected components of operations and wraps each component into a circuit operation.
493
632
 
494
- Uses `cirq.merge_operations` to identify connected components of operations. Moment structure
495
- is preserved for operations that do not participate in merging. For merged operations, the
496
- newly created circuit operations are constructed by inserting operations using EARLIEST
497
- strategy.
633
+ Moment structure is preserved for operations that do not participate in merging.
634
+ For merged operations, the newly created circuit operations are constructed by inserting
635
+ operations using EARLIEST strategy.
498
636
  If you need more control on moment structure of newly created circuit operations, consider
499
637
  using `cirq.merge_operations` directly with a custom `merge_func`.
500
638
 
@@ -514,24 +652,16 @@ def merge_operations_to_circuit_op(
514
652
  Copy of input circuit with valid connected components wrapped in tagged circuit operations.
515
653
  """
516
654
 
517
- def merge_func(op1: cirq.Operation, op2: cirq.Operation) -> cirq.Operation | None:
518
- def get_ops(op: cirq.Operation):
519
- op_untagged = op.untagged
520
- return (
521
- [*op_untagged.circuit.all_operations()]
522
- if isinstance(op_untagged, circuits.CircuitOperation)
523
- and merged_circuit_op_tag in op.tags
524
- else [op]
525
- )
655
+ def is_mergeable(_: cirq.Operation):
656
+ return True
526
657
 
527
- left_ops, right_ops = get_ops(op1), get_ops(op2)
528
- if not can_merge(left_ops, right_ops):
529
- return None
530
- return circuits.CircuitOperation(circuits.FrozenCircuit(left_ops, right_ops)).with_tags(
531
- merged_circuit_op_tag
532
- )
533
-
534
- return merge_operations(circuit, merge_func, tags_to_ignore=tags_to_ignore, deep=deep)
658
+ return _merge_operations_impl(
659
+ circuit,
660
+ ComponentWithOpsSet(is_mergeable, can_merge),
661
+ merged_circuit_op_tag=merged_circuit_op_tag,
662
+ tags_to_ignore=tags_to_ignore,
663
+ deep=deep,
664
+ )
535
665
 
536
666
 
537
667
  def merge_k_qubit_unitaries_to_circuit_op(
@@ -544,10 +674,9 @@ def merge_k_qubit_unitaries_to_circuit_op(
544
674
  ) -> CIRCUIT_TYPE:
545
675
  """Merges connected components of operations, acting on <= k qubits, into circuit operations.
546
676
 
547
- Uses `cirq.merge_operations_to_circuit_op` to identify and merge connected components of
548
- unitary operations acting on at-most k-qubits. Moment structure is preserved for operations
549
- that do not participate in merging. For merged operations, the newly created circuit operations
550
- are constructed by inserting operations using EARLIEST strategy.
677
+ Moment structure is preserved for operations that do not participate in merging.
678
+ For merged operations, the newly created circuit operations are constructed by inserting
679
+ operations using EARLIEST strategy.
551
680
 
552
681
  Args:
553
682
  circuit: Input circuit to apply the transformations on. The input circuit is not mutated.
@@ -563,18 +692,14 @@ def merge_k_qubit_unitaries_to_circuit_op(
563
692
  Copy of input circuit with valid connected components wrapped in tagged circuit operations.
564
693
  """
565
694
 
566
- def can_merge(ops1: Sequence[cirq.Operation], ops2: Sequence[cirq.Operation]) -> bool:
567
- return all(
568
- protocols.num_qubits(op) <= k and protocols.has_unitary(op)
569
- for op_list in [ops1, ops2]
570
- for op in op_list
571
- )
695
+ def is_mergeable(op: cirq.Operation):
696
+ return protocols.num_qubits(op) <= k and protocols.has_unitary(op)
572
697
 
573
- return merge_operations_to_circuit_op(
698
+ return _merge_operations_impl(
574
699
  circuit,
575
- can_merge,
576
- tags_to_ignore=tags_to_ignore,
700
+ ComponentSet(is_mergeable),
577
701
  merged_circuit_op_tag=merged_circuit_op_tag or f"Merged {k}q unitary connected component.",
702
+ tags_to_ignore=tags_to_ignore,
578
703
  deep=deep,
579
704
  )
580
705
 
@@ -877,3 +877,187 @@ def test_merge_operations_does_not_merge_measurements_behind_ccos():
877
877
  cirq.testing.assert_same_circuits(
878
878
  cirq.align_left(cirq.merge_operations(circuit, merge_func)), expected_circuit
879
879
  )
880
+
881
+
882
+ def test_merge_3q_unitaries_to_circuit_op_3q_gate_absorbs_overlapping_2q_gates():
883
+ q = cirq.LineQubit.range(3)
884
+ c_orig = cirq.Circuit(
885
+ cirq.Moment(
886
+ cirq.H(q[0]).with_tags("ignore"),
887
+ cirq.H(q[1]).with_tags("ignore"),
888
+ cirq.H(q[2]).with_tags("ignore"),
889
+ ),
890
+ cirq.Moment(cirq.CNOT(q[0], q[2]), cirq.X(q[1]).with_tags("ignore")),
891
+ cirq.CNOT(q[0], q[1]),
892
+ cirq.CNOT(q[1], q[2]),
893
+ cirq.CCZ(*q),
894
+ strategy=cirq.InsertStrategy.NEW,
895
+ )
896
+ cirq.testing.assert_has_diagram(
897
+ c_orig,
898
+ '''
899
+ ┌──────────┐
900
+ 0: ───H[ignore]────@─────────────@───────@───
901
+ │ │ │
902
+ 1: ───H[ignore]────┼X[ignore]────X───@───@───
903
+ │ │ │
904
+ 2: ───H[ignore]────X─────────────────X───@───
905
+ └──────────┘
906
+ ''',
907
+ )
908
+
909
+ c_new = cirq.merge_k_qubit_unitaries_to_circuit_op(
910
+ c_orig, k=3, merged_circuit_op_tag="merged", tags_to_ignore=["ignore"]
911
+ )
912
+ cirq.testing.assert_has_diagram(
913
+ cirq.drop_empty_moments(c_new),
914
+ '''
915
+ [ 0: ───@───@───────@─── ]
916
+ [ │ │ │ ]
917
+ 0: ───H[ignore]───────────────[ 1: ───┼───X───@───@─── ]───────────
918
+ [ │ │ │ ]
919
+ [ 2: ───X───────X───@─── ][merged]
920
+
921
+ 1: ───H[ignore]───X[ignore]───#2───────────────────────────────────
922
+
923
+ 2: ───H[ignore]───────────────#3───────────────────────────────────
924
+ ''',
925
+ )
926
+
927
+
928
+ def test_merge_3q_unitaries_to_circuit_op_3q_gate_absorbs_disjoint_gates():
929
+ q = cirq.LineQubit.range(3)
930
+ c_orig = cirq.Circuit(
931
+ cirq.Moment(cirq.CNOT(q[0], q[1]), cirq.X(q[2])),
932
+ cirq.CCZ(*q),
933
+ strategy=cirq.InsertStrategy.NEW,
934
+ )
935
+ cirq.testing.assert_has_diagram(
936
+ c_orig,
937
+ '''
938
+ 0: ───@───@───
939
+ │ │
940
+ 1: ───X───@───
941
+
942
+ 2: ───X───@───
943
+ ''',
944
+ )
945
+
946
+ c_new = cirq.merge_k_qubit_unitaries_to_circuit_op(
947
+ c_orig, k=3, merged_circuit_op_tag="merged", tags_to_ignore=["ignore"]
948
+ )
949
+ cirq.testing.assert_has_diagram(
950
+ cirq.drop_empty_moments(c_new),
951
+ '''
952
+ [ 0: ───@───@─── ]
953
+ [ │ │ ]
954
+ 0: ───[ 1: ───X───@─── ]───────────
955
+ [ │ ]
956
+ [ 2: ───X───@─── ][merged]
957
+
958
+ 1: ───#2───────────────────────────
959
+
960
+ 2: ───#3───────────────────────────
961
+ ''',
962
+ )
963
+
964
+
965
+ def test_merge_3q_unitaries_to_circuit_op_3q_gate_doesnt_absorb_unmergeable_gate():
966
+ q = cirq.LineQubit.range(3)
967
+ c_orig = cirq.Circuit(
968
+ cirq.CCZ(*q),
969
+ cirq.Moment(cirq.CNOT(q[0], q[1]), cirq.X(q[2]).with_tags("ignore")),
970
+ cirq.CCZ(*q),
971
+ strategy=cirq.InsertStrategy.NEW,
972
+ )
973
+ cirq.testing.assert_has_diagram(
974
+ c_orig,
975
+ '''
976
+ 0: ───@───@───────────@───
977
+ │ │ │
978
+ 1: ───@───X───────────@───
979
+ │ │
980
+ 2: ───@───X[ignore]───@───
981
+ ''',
982
+ )
983
+
984
+ c_new = cirq.merge_k_qubit_unitaries_to_circuit_op(
985
+ c_orig, k=3, merged_circuit_op_tag="merged", tags_to_ignore=["ignore"]
986
+ )
987
+ cirq.testing.assert_has_diagram(
988
+ cirq.drop_empty_moments(c_new),
989
+ '''
990
+ [ 0: ───@───@─── ]
991
+ [ │ │ ]
992
+ 0: ───[ 1: ───@───X─── ]───────────────────────@───
993
+ [ │ ] │
994
+ [ 2: ───@─────── ][merged] │
995
+ │ │
996
+ 1: ───#2───────────────────────────────────────@───
997
+ │ │
998
+ 2: ───#3───────────────────────────X[ignore]───@───
999
+ ''',
1000
+ )
1001
+
1002
+
1003
+ def test_merge_3q_unitaries_to_circuit_op_prefer_to_merge_into_earlier_op():
1004
+ q = cirq.LineQubit.range(6)
1005
+ c_orig = cirq.Circuit(
1006
+ cirq.Moment(
1007
+ cirq.CCZ(*q[0:3]), cirq.X(q[3]), cirq.H(q[4]), cirq.H(q[5]).with_tags("ignore")
1008
+ ),
1009
+ cirq.Moment(cirq.CNOT(q[0], q[1]), cirq.X(q[2]).with_tags("ignore"), cirq.CCZ(*q[3:6])),
1010
+ cirq.Moment(
1011
+ cirq.X(q[0]),
1012
+ cirq.X(q[1]),
1013
+ cirq.X(q[2]),
1014
+ cirq.X(q[3]).with_tags("ignore"),
1015
+ cirq.CNOT(*q[4:6]),
1016
+ ),
1017
+ cirq.Moment(cirq.CCZ(*q[0:3]), cirq.CCZ(*q[3:6])),
1018
+ strategy=cirq.InsertStrategy.NEW,
1019
+ )
1020
+ cirq.testing.assert_has_diagram(
1021
+ c_orig,
1022
+ '''
1023
+ 0: ───@───────────@───────────X───────────@───
1024
+ │ │ │
1025
+ 1: ───@───────────X───────────X───────────@───
1026
+ │ │
1027
+ 2: ───@───────────X[ignore]───X───────────@───
1028
+
1029
+ 3: ───X───────────@───────────X[ignore]───@───
1030
+ │ │
1031
+ 4: ───H───────────@───────────@───────────@───
1032
+ │ │ │
1033
+ 5: ───H[ignore]───@───────────X───────────@───
1034
+ ''',
1035
+ )
1036
+
1037
+ c_new = cirq.merge_k_qubit_unitaries_to_circuit_op(
1038
+ c_orig, k=3, merged_circuit_op_tag="merged", tags_to_ignore=["ignore"]
1039
+ )
1040
+ cirq.testing.assert_has_diagram(
1041
+ cirq.drop_empty_moments(c_new),
1042
+ '''
1043
+ [ 0: ───@───@───X─── ] [ 0: ───────@─── ]
1044
+ [ │ │ ] [ │ ]
1045
+ 0: ───[ 1: ───@───X───X─── ]────────────────────────────────────────────────────────[ 1: ───────@─── ]───────────
1046
+ [ │ ] [ │ ]
1047
+ [ 2: ───@─────────── ][merged] [ 2: ───X───@─── ][merged]
1048
+ │ │
1049
+ 1: ───#2────────────────────────────────────────────────────────────────────────────#2───────────────────────────
1050
+ │ │
1051
+ 2: ───#3───────────────────────────────X[ignore]────────────────────────────────────#3───────────────────────────
1052
+
1053
+ [ 3: ───X───@─────── ]
1054
+ [ │ ]
1055
+ 3: ────────────────────────────────────[ 4: ───H───@───@─── ]───────────X[ignore]───@────────────────────────────
1056
+ [ │ │ ] │
1057
+ [ 5: ───────@───X─── ][merged] │
1058
+ │ │
1059
+ 4: ────────────────────────────────────#2───────────────────────────────────────────@────────────────────────────
1060
+ │ │
1061
+ 5: ───H[ignore]────────────────────────#3───────────────────────────────────────────@────────────────────────────
1062
+ ''', # noqa: E501
1063
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cirq-core
3
- Version: 1.7.0.dev20251015163856
3
+ Version: 1.7.0.dev20251017194258
4
4
  Summary: A framework for creating, editing, and invoking Noisy Intermediate Scale Quantum (NISQ) circuits.
5
5
  Home-page: http://github.com/quantumlib/cirq
6
6
  Author: The Cirq Developers
@@ -4,8 +4,8 @@ cirq/_compat_test.py,sha256=emXpdD5ZvwLRlFAoQB8YatmZyU3b4e9jg6FppMTUhkU,33900
4
4
  cirq/_doc.py,sha256=28ZskY9ZtZ_4GS1oXPUgklKnJqmAE-rkUfzcsJ0--nA,2941
5
5
  cirq/_import.py,sha256=ixBu4EyGl46Ram2cP3p5eZVEFDW5L2DS-VyTjz4N9iw,8429
6
6
  cirq/_import_test.py,sha256=oF4izzOVZLc7NZ0aZHFcGv-r01eiFFt_JORx_x7_D4s,1089
7
- cirq/_version.py,sha256=qzM4X-bbsX0kROwBVHnPFbyLmQuSNurNMGdWyUzfq0Q,1206
8
- cirq/_version_test.py,sha256=ZD6xOnsS-7kp4HrfFahqC40Cc47lnMWHJH5zKtradm8,155
7
+ cirq/_version.py,sha256=IMZibsH2TjYICrU180KQdB1dmupy9Va1_pKNqcvjn4o,1206
8
+ cirq/_version_test.py,sha256=-O7DI69zPqWXjQmHgCno_e5fNyp_wnL-rEYQaiu7l4A,155
9
9
  cirq/conftest.py,sha256=wSDKNdIQRDfLnXvOCWD3erheOw8JHRhdfQ53EyTUIXg,1239
10
10
  cirq/json_resolver_cache.py,sha256=A5DIgFAY1hUNt9vai_C3-gGBv24116CJMzQxMcXOax4,13726
11
11
  cirq/py.typed,sha256=VFSlmh_lNwnaXzwY-ZuW-C2Ws5PkuDoVgBdNCs0jXJE,63
@@ -1070,6 +1070,8 @@ cirq/testing/test_data/test_module_missing_json_test_data/__init__.py,sha256=47D
1070
1070
  cirq/testing/test_data/test_module_missing_testspec/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1071
1071
  cirq/testing/test_data/test_module_missing_testspec/json_test_data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1072
1072
  cirq/transformers/__init__.py,sha256=6noA2FZMgzwYorHZuyi1aMB8mQzohE2FIdNxbNxMLsM,7343
1073
+ cirq/transformers/_connected_component.py,sha256=Qn8rTKCgNzpbB-fiAKG8MHQYiFPb2v_JSZRavNJETVM,8072
1074
+ cirq/transformers/_connected_component_test.py,sha256=BNpoDshQWHw9uvHQpPkkM6yAfQe9s0DR5D25jz8kiUM,6204
1073
1075
  cirq/transformers/align.py,sha256=95iI2sIil7V02hRPaXB-wG_eiMb5yF6ffUZU3fHmPP4,3341
1074
1076
  cirq/transformers/align_test.py,sha256=blaroKF825RMDCdt403u98OoGck1AXSRCyw1KuWnqB0,7791
1075
1077
  cirq/transformers/drop_empty_moments.py,sha256=uZJG9FpUNyA1Mi0xLDuVuhj_siZhPZ1_s08Ry9xQ-1M,1535
@@ -1110,8 +1112,8 @@ cirq/transformers/tag_transformers.py,sha256=xGTEe9_H857Zd-GJ_g1tlCz_zH3kWKGBq38
1110
1112
  cirq/transformers/tag_transformers_test.py,sha256=PqIcYFgiLU7VgC1EHkFYhxNCf0D9zKDCZ_Gwtnykkt4,3439
1111
1113
  cirq/transformers/transformer_api.py,sha256=woyAJu8NIbo7IP9jgIZNSOVIIj0x4wpFyzpYlto1wCg,16794
1112
1114
  cirq/transformers/transformer_api_test.py,sha256=vz_zTDPJIfjfqORGKCxeAs3U1F3X2dFNbe50o79uY-4,13273
1113
- cirq/transformers/transformer_primitives.py,sha256=U7eN9UQ3YpW6D8UPJTHxsSO4ERpZxRxt14rqbMTk71Q,36521
1114
- cirq/transformers/transformer_primitives_test.py,sha256=QAayPS74Ro4TTol-IOPnd7S49DKhvXYmWci0nOsk05A,41712
1115
+ cirq/transformers/transformer_primitives.py,sha256=UEGmKYZf3_vzfU_RkIcchmhEpmryW1H7KiU4LlMzC84,40832
1116
+ cirq/transformers/transformer_primitives_test.py,sha256=qQL95Rp_SUmJDSuMVauhVmLeKUBoIGPH3Tzomc3CFAc,51645
1115
1117
  cirq/transformers/analytical_decompositions/__init__.py,sha256=Rw7X6hPh14k-cDTcdWI7fQu8v5oU9d1vHuwulUBv-8o,3694
1116
1118
  cirq/transformers/analytical_decompositions/clifford_decomposition.py,sha256=_Wikc4KpE3hRH5WWtkYXvTzPxXnJr5-ObHtnpLnKV2c,6781
1117
1119
  cirq/transformers/analytical_decompositions/clifford_decomposition_test.py,sha256=DPVqB0rlDsA0BZJOin5QTkVSEfNTVNuf8seGCp49ttg,7178
@@ -1244,8 +1246,8 @@ cirq/work/sampler.py,sha256=rxbMWvrhu3gfNSBjZKozw28lLKVvBAS_1EGyPdYe8Xg,19041
1244
1246
  cirq/work/sampler_test.py,sha256=SsMrRvLDYELyOAWLKISjkdEfrBwLYWRsT6D8WrsLM3Q,13533
1245
1247
  cirq/work/zeros_sampler.py,sha256=Fs2JWwq0n9zv7_G5Rm-9vPeHUag7uctcMOHg0JTkZpc,2371
1246
1248
  cirq/work/zeros_sampler_test.py,sha256=lQLgQDGBLtfImryys2HzQ2jOSGxHgc7-koVBUhv8qYk,3345
1247
- cirq_core-1.7.0.dev20251015163856.dist-info/licenses/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
1248
- cirq_core-1.7.0.dev20251015163856.dist-info/METADATA,sha256=jhXUZRI84J1an8uy8u7iZYAqJWsabFvAChf5cVtMkw4,4757
1249
- cirq_core-1.7.0.dev20251015163856.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
1250
- cirq_core-1.7.0.dev20251015163856.dist-info/top_level.txt,sha256=Sz9iOxHU0IEMLSFGwiwOCaN2e9K-jFbBbtpPN1hB73g,5
1251
- cirq_core-1.7.0.dev20251015163856.dist-info/RECORD,,
1249
+ cirq_core-1.7.0.dev20251017194258.dist-info/licenses/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
1250
+ cirq_core-1.7.0.dev20251017194258.dist-info/METADATA,sha256=qzy3lCdh84WWUo752Q79w6Kob6Ye011keqXeJjMxwoA,4757
1251
+ cirq_core-1.7.0.dev20251017194258.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
1252
+ cirq_core-1.7.0.dev20251017194258.dist-info/top_level.txt,sha256=Sz9iOxHU0IEMLSFGwiwOCaN2e9K-jFbBbtpPN1hB73g,5
1253
+ cirq_core-1.7.0.dev20251017194258.dist-info/RECORD,,