From 9ed2227e06881753c80feb1e7edba88392d97355 Mon Sep 17 00:00:00 2001 From: daxfohl Date: Wed, 4 May 2022 19:58:05 -0700 Subject: [PATCH 01/24] Dedicated method for creating circuit from op tree with EARLIEST strategy --- cirq-core/cirq/circuits/circuit.py | 51 +++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/cirq-core/cirq/circuits/circuit.py b/cirq-core/cirq/circuits/circuit.py index 5013d183f3a..f66ded366d6 100644 --- a/cirq-core/cirq/circuits/circuit.py +++ b/cirq-core/cirq/circuits/circuit.py @@ -1704,7 +1704,56 @@ def __init__( """ self._moments: List['cirq.Moment'] = [] with _compat.block_overlapping_deprecation('.*'): - self.append(contents, strategy=strategy) + if strategy == InsertStrategy.EARLIEST: + self._create_from_earliest(contents) + else: + self.append(contents, strategy=strategy) + + def _create_from_earliest(self, contents): + moments_and_operations = list( + ops.flatten_to_ops_or_moments( + ops.transform_op_tree(contents, preserve_moments=True) + ) + ) + + qubits = defaultdict(lambda: -1) + mkeys = defaultdict(lambda: -1) + ckeys = defaultdict(lambda: -1) + moments = {} + opses = {} + length = 0 + moment = -1 + for moment_or_op in moments_and_operations: + if isinstance(moment_or_op, Moment): + moments[length] = moment_or_op + moment = length + length += 1 + else: + op = cast(ops.Operation, moment_or_op) + op_qubits = op.qubits + op_mkeys = protocols.measurement_key_objs(op) or frozenset() + op_ckeys = protocols.control_keys(op) + i = moment + i = max(i, i, *[qubits[q] for q in op_qubits]) + i = max(i, i, *[mkeys[k] for k in op_mkeys]) + i = max(i, i, *[ckeys[k] for k in op_mkeys]) + i = max(i, i, *[mkeys[k] for k in op_ckeys]) + i += 1 + if i not in opses: + opses[i] = set() + for q in op_qubits: + qubits[q] = i + for k in op_mkeys: + mkeys[k] = i + for k in op_ckeys: + ckeys[k] = i + opses[i].add(op) + length = max(length, i + 1) + for i in range(length): + if i in moments: + self._moments.append(moments[i]) + else: + self._moments.append(Moment(opses[i])) def __copy__(self) -> 'cirq.Circuit': return self.copy() From 6aec8469ce7f8dadab725cfa22e498760798092a Mon Sep 17 00:00:00 2001 From: daxfohl Date: Wed, 4 May 2022 21:43:53 -0700 Subject: [PATCH 02/24] fix tests --- cirq-core/cirq/circuits/circuit.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/cirq-core/cirq/circuits/circuit.py b/cirq-core/cirq/circuits/circuit.py index f66ded366d6..90af450c27f 100644 --- a/cirq-core/cirq/circuits/circuit.py +++ b/cirq-core/cirq/circuits/circuit.py @@ -1711,16 +1711,14 @@ def __init__( def _create_from_earliest(self, contents): moments_and_operations = list( - ops.flatten_to_ops_or_moments( - ops.transform_op_tree(contents, preserve_moments=True) - ) + ops.flatten_to_ops_or_moments(ops.transform_op_tree(contents, preserve_moments=True)) ) qubits = defaultdict(lambda: -1) mkeys = defaultdict(lambda: -1) ckeys = defaultdict(lambda: -1) + opses = defaultdict(list) moments = {} - opses = {} length = 0 moment = -1 for moment_or_op in moments_and_operations: @@ -1739,15 +1737,13 @@ def _create_from_earliest(self, contents): i = max(i, i, *[ckeys[k] for k in op_mkeys]) i = max(i, i, *[mkeys[k] for k in op_ckeys]) i += 1 - if i not in opses: - opses[i] = set() for q in op_qubits: qubits[q] = i for k in op_mkeys: mkeys[k] = i for k in op_ckeys: ckeys[k] = i - opses[i].add(op) + opses[i].append(op) length = max(length, i + 1) for i in range(length): if i in moments: From 4c7ddfed291923bee4973d731ccfca4db45897f2 Mon Sep 17 00:00:00 2001 From: daxfohl Date: Wed, 4 May 2022 21:57:31 -0700 Subject: [PATCH 03/24] Add speed test --- cirq-core/cirq/circuits/circuit.py | 6 +++--- cirq-core/cirq/circuits/circuit_test.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/cirq-core/cirq/circuits/circuit.py b/cirq-core/cirq/circuits/circuit.py index 90af450c27f..9b4396c3836 100644 --- a/cirq-core/cirq/circuits/circuit.py +++ b/cirq-core/cirq/circuits/circuit.py @@ -1710,8 +1710,8 @@ def __init__( self.append(contents, strategy=strategy) def _create_from_earliest(self, contents): - moments_and_operations = list( - ops.flatten_to_ops_or_moments(ops.transform_op_tree(contents, preserve_moments=True)) + moments_and_operations = ops.flatten_to_ops_or_moments( + ops.transform_op_tree(contents, preserve_moments=True) ) qubits = defaultdict(lambda: -1) @@ -1729,7 +1729,7 @@ def _create_from_earliest(self, contents): else: op = cast(ops.Operation, moment_or_op) op_qubits = op.qubits - op_mkeys = protocols.measurement_key_objs(op) or frozenset() + op_mkeys = protocols.measurement_key_objs(op) op_ckeys = protocols.control_keys(op) i = moment i = max(i, i, *[qubits[q] for q in op_qubits]) diff --git a/cirq-core/cirq/circuits/circuit_test.py b/cirq-core/cirq/circuits/circuit_test.py index 00f326dd02c..1c91fd740ca 100644 --- a/cirq-core/cirq/circuits/circuit_test.py +++ b/cirq-core/cirq/circuits/circuit_test.py @@ -13,6 +13,7 @@ # limitations under the License. import itertools import os +import time from collections import defaultdict from random import randint, random, sample, randrange from typing import Iterator, Optional, Tuple, TYPE_CHECKING @@ -4603,3 +4604,14 @@ def _circuit_diagram_info_(self, args) -> str: └────────┘ """, ) + + +def test_create_speed(): + qs = 100 + moments = 500 + xs = [cirq.X(cirq.LineQubit(i)) for i in range(qs)] + opa = [xs[i] for i in range(qs) for _ in range(moments)] + t = time.perf_counter() + _ = cirq.Circuit(opa) + assert time.perf_counter() - t < 1 + print(time.perf_counter() - t) From de68ec6d86483176efa0fa726400ec0e9f05fe4f Mon Sep 17 00:00:00 2001 From: daxfohl Date: Wed, 4 May 2022 22:03:30 -0700 Subject: [PATCH 04/24] Remove debug print --- cirq-core/cirq/circuits/circuit_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cirq-core/cirq/circuits/circuit_test.py b/cirq-core/cirq/circuits/circuit_test.py index 1c91fd740ca..b0821160714 100644 --- a/cirq-core/cirq/circuits/circuit_test.py +++ b/cirq-core/cirq/circuits/circuit_test.py @@ -4614,4 +4614,3 @@ def test_create_speed(): t = time.perf_counter() _ = cirq.Circuit(opa) assert time.perf_counter() - t < 1 - print(time.perf_counter() - t) From 001d70c7d0ccf33c79240dd3198179442aa9824c Mon Sep 17 00:00:00 2001 From: daxfohl Date: Wed, 4 May 2022 23:12:03 -0700 Subject: [PATCH 05/24] Allow ops to fall through moments --- cirq-core/cirq/circuits/circuit.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/cirq-core/cirq/circuits/circuit.py b/cirq-core/cirq/circuits/circuit.py index 9b4396c3836..76f290990b0 100644 --- a/cirq-core/cirq/circuits/circuit.py +++ b/cirq-core/cirq/circuits/circuit.py @@ -1720,22 +1720,29 @@ def _create_from_earliest(self, contents): opses = defaultdict(list) moments = {} length = 0 - moment = -1 for moment_or_op in moments_and_operations: if isinstance(moment_or_op, Moment): moments[length] = moment_or_op - moment = length + for q in moment_or_op.qubits: + qubits[q] = length + for k in moment_or_op._measurement_key_objs_(): + mkeys[k] = length + for k in moment_or_op._control_keys_(): + ckeys[k] = length length += 1 else: op = cast(ops.Operation, moment_or_op) op_qubits = op.qubits op_mkeys = protocols.measurement_key_objs(op) op_ckeys = protocols.control_keys(op) - i = moment - i = max(i, i, *[qubits[q] for q in op_qubits]) - i = max(i, i, *[mkeys[k] for k in op_mkeys]) - i = max(i, i, *[ckeys[k] for k in op_mkeys]) - i = max(i, i, *[mkeys[k] for k in op_ckeys]) + i = max( + -1, + -1, # in case none of the following exist + *[qubits[q] for q in op_qubits], + *[mkeys[k] for k in op_mkeys], + *[ckeys[k] for k in op_mkeys], + *[mkeys[k] for k in op_ckeys], + ) i += 1 for q in op_qubits: qubits[q] = i @@ -1747,7 +1754,7 @@ def _create_from_earliest(self, contents): length = max(length, i + 1) for i in range(length): if i in moments: - self._moments.append(moments[i]) + self._moments.append(moments[i].with_operations(opses[i])) else: self._moments.append(Moment(opses[i])) From db7ac0ba9b144ad278592b574a0f78038ed64341 Mon Sep 17 00:00:00 2001 From: daxfohl Date: Wed, 4 May 2022 23:25:52 -0700 Subject: [PATCH 06/24] reduce test flakiness --- cirq-core/cirq/circuits/circuit_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-core/cirq/circuits/circuit_test.py b/cirq-core/cirq/circuits/circuit_test.py index b0821160714..c5d3eb29d4d 100644 --- a/cirq-core/cirq/circuits/circuit_test.py +++ b/cirq-core/cirq/circuits/circuit_test.py @@ -4613,4 +4613,4 @@ def test_create_speed(): opa = [xs[i] for i in range(qs) for _ in range(moments)] t = time.perf_counter() _ = cirq.Circuit(opa) - assert time.perf_counter() - t < 1 + assert time.perf_counter() - t < 2 From 1027f914392d4d6a7684939434cb80737fa758d6 Mon Sep 17 00:00:00 2001 From: daxfohl Date: Thu, 5 May 2022 00:07:22 -0700 Subject: [PATCH 07/24] Small code cleanup --- cirq-core/cirq/circuits/circuit.py | 45 +++++++++++++----------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/cirq-core/cirq/circuits/circuit.py b/cirq-core/cirq/circuits/circuit.py index 76f290990b0..17181f9fac1 100644 --- a/cirq-core/cirq/circuits/circuit.py +++ b/cirq-core/cirq/circuits/circuit.py @@ -1720,38 +1720,31 @@ def _create_from_earliest(self, contents): opses = defaultdict(list) moments = {} length = 0 - for moment_or_op in moments_and_operations: - if isinstance(moment_or_op, Moment): - moments[length] = moment_or_op - for q in moment_or_op.qubits: - qubits[q] = length - for k in moment_or_op._measurement_key_objs_(): - mkeys[k] = length - for k in moment_or_op._control_keys_(): - ckeys[k] = length - length += 1 + for mop in moments_and_operations: + mop_qubits = mop.qubits + mop_mkeys = protocols.measurement_key_objs(mop) + mop_ckeys = protocols.control_keys(mop) + if isinstance(mop, Moment): + i = length + moments[i] = mop else: - op = cast(ops.Operation, moment_or_op) - op_qubits = op.qubits - op_mkeys = protocols.measurement_key_objs(op) - op_ckeys = protocols.control_keys(op) i = max( -1, -1, # in case none of the following exist - *[qubits[q] for q in op_qubits], - *[mkeys[k] for k in op_mkeys], - *[ckeys[k] for k in op_mkeys], - *[mkeys[k] for k in op_ckeys], + *[qubits[q] for q in mop_qubits], + *[mkeys[k] for k in mop_mkeys], + *[ckeys[k] for k in mop_mkeys], + *[mkeys[k] for k in mop_ckeys], ) i += 1 - for q in op_qubits: - qubits[q] = i - for k in op_mkeys: - mkeys[k] = i - for k in op_ckeys: - ckeys[k] = i - opses[i].append(op) - length = max(length, i + 1) + opses[i].append(mop) + for q in mop_qubits: + qubits[q] = i + for k in mop_mkeys: + mkeys[k] = i + for k in mop_ckeys: + ckeys[k] = i + length = max(length, i + 1) for i in range(length): if i in moments: self._moments.append(moments[i].with_operations(opses[i])) From cc1f8ede5032d02d791a6ed1cca756810edd57a7 Mon Sep 17 00:00:00 2001 From: daxfohl Date: Thu, 5 May 2022 09:45:39 -0700 Subject: [PATCH 08/24] Remove an unnecessary identity transform --- cirq-core/cirq/circuits/circuit.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/cirq-core/cirq/circuits/circuit.py b/cirq-core/cirq/circuits/circuit.py index 17181f9fac1..43342afae8f 100644 --- a/cirq-core/cirq/circuits/circuit.py +++ b/cirq-core/cirq/circuits/circuit.py @@ -1710,17 +1710,13 @@ def __init__( self.append(contents, strategy=strategy) def _create_from_earliest(self, contents): - moments_and_operations = ops.flatten_to_ops_or_moments( - ops.transform_op_tree(contents, preserve_moments=True) - ) - qubits = defaultdict(lambda: -1) mkeys = defaultdict(lambda: -1) ckeys = defaultdict(lambda: -1) opses = defaultdict(list) moments = {} length = 0 - for mop in moments_and_operations: + for mop in ops.flatten_to_ops_or_moments(contents): mop_qubits = mop.qubits mop_mkeys = protocols.measurement_key_objs(mop) mop_ckeys = protocols.control_keys(mop) From 24b20b84b455815076a6cbbf6115cf5aa5a785a2 Mon Sep 17 00:00:00 2001 From: daxfohl Date: Thu, 5 May 2022 13:42:05 -0700 Subject: [PATCH 09/24] Provide empty key protocols for eigengate, to preempt the dunder search --- cirq-core/cirq/ops/eigen_gate.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cirq-core/cirq/ops/eigen_gate.py b/cirq-core/cirq/ops/eigen_gate.py index 221a6aea501..319ef0accb2 100644 --- a/cirq-core/cirq/ops/eigen_gate.py +++ b/cirq-core/cirq/ops/eigen_gate.py @@ -393,6 +393,12 @@ def _equal_up_to_global_phase_(self, other, atol): def _json_dict_(self) -> Dict[str, Any]: return protocols.obj_to_dict_helper(self, ['exponent', 'global_shift']) + def _measurement_key_objs_(self): + return frozenset() + + def _control_keys_(self): + return frozenset() + def _lcm(vals: Iterable[int]) -> int: t = 1 From 9f349ef6317b956d6b21da45ea9e4a1e182168fb Mon Sep 17 00:00:00 2001 From: daxfohl Date: Thu, 5 May 2022 13:49:42 -0700 Subject: [PATCH 10/24] Improve operates_on efficiency --- cirq-core/cirq/circuits/moment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-core/cirq/circuits/moment.py b/cirq-core/cirq/circuits/moment.py index 75ddecfd612..f02cff47167 100644 --- a/cirq-core/cirq/circuits/moment.py +++ b/cirq-core/cirq/circuits/moment.py @@ -131,7 +131,7 @@ def operates_on(self, qubits: Iterable['cirq.Qid']) -> bool: Returns: Whether this moment has operations involving the qubits. """ - return bool(set(qubits) & self.qubits) + return not self._qubits.isdisjoint(qubits) def operation_at(self, qubit: raw_types.Qid) -> Optional['cirq.Operation']: """Returns the operation on a certain qubit for the moment. From e8bd15816ad0a711571180203d872750e88eb8da Mon Sep 17 00:00:00 2001 From: daxfohl Date: Thu, 5 May 2022 14:08:59 -0700 Subject: [PATCH 11/24] Remove unnecessary identity transform --- cirq-core/cirq/circuits/circuit.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/cirq-core/cirq/circuits/circuit.py b/cirq-core/cirq/circuits/circuit.py index 43342afae8f..685a35721b4 100644 --- a/cirq-core/cirq/circuits/circuit.py +++ b/cirq-core/cirq/circuits/circuit.py @@ -2007,14 +2007,9 @@ def insert( Raises: ValueError: Bad insertion strategy. """ - moments_and_operations = list( - ops.flatten_to_ops_or_moments( - ops.transform_op_tree(moment_or_operation_tree, preserve_moments=True) - ) - ) # limit index to 0..len(self._moments), also deal with indices smaller 0 k = max(min(index if index >= 0 else len(self._moments) + index, len(self._moments)), 0) - for moment_or_op in moments_and_operations: + for moment_or_op in ops.flatten_to_ops_or_moments(moment_or_operation_tree): if isinstance(moment_or_op, Moment): self._moments.insert(k, moment_or_op) k += 1 From 21189499d684d82a7435d09d5d817f18be83ead5 Mon Sep 17 00:00:00 2001 From: daxfohl Date: Thu, 5 May 2022 14:42:48 -0700 Subject: [PATCH 12/24] Remove dead code (GateOp does not have control keys) --- cirq-core/cirq/ops/eigen_gate.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cirq-core/cirq/ops/eigen_gate.py b/cirq-core/cirq/ops/eigen_gate.py index 319ef0accb2..bc16415dd56 100644 --- a/cirq-core/cirq/ops/eigen_gate.py +++ b/cirq-core/cirq/ops/eigen_gate.py @@ -396,9 +396,6 @@ def _json_dict_(self) -> Dict[str, Any]: def _measurement_key_objs_(self): return frozenset() - def _control_keys_(self): - return frozenset() - def _lcm(vals: Iterable[int]) -> int: t = 1 From 9ba3ebd0a2d9882754a942e2999297398579a13d Mon Sep 17 00:00:00 2001 From: daxfohl Date: Thu, 5 May 2022 16:46:55 -0700 Subject: [PATCH 13/24] Microoptimization --- cirq-core/cirq/circuits/circuit.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/cirq-core/cirq/circuits/circuit.py b/cirq-core/cirq/circuits/circuit.py index 685a35721b4..cc1d4e0186a 100644 --- a/cirq-core/cirq/circuits/circuit.py +++ b/cirq-core/cirq/circuits/circuit.py @@ -1724,14 +1724,13 @@ def _create_from_earliest(self, contents): i = length moments[i] = mop else: - i = max( - -1, - -1, # in case none of the following exist - *[qubits[q] for q in mop_qubits], - *[mkeys[k] for k in mop_mkeys], - *[ckeys[k] for k in mop_mkeys], - *[mkeys[k] for k in mop_ckeys], - ) + i = -1 + if mop_qubits: + i = max(i, *[qubits[q] for q in mop_qubits]) + if mop_mkeys: + i = max(i, *[mkeys[k] for k in mop_mkeys], *[ckeys[k] for k in mop_mkeys]) + if mop_ckeys: + i = max(i, *[mkeys[k] for k in mop_ckeys]) i += 1 opses[i].append(mop) for q in mop_qubits: From 9c1d3edcd5a3ecc04ff87931882df13c4d13142c Mon Sep 17 00:00:00 2001 From: daxfohl Date: Thu, 5 May 2022 17:02:59 -0700 Subject: [PATCH 14/24] Create moment unchecked --- cirq-core/cirq/circuits/circuit.py | 8 +++++--- cirq-core/cirq/circuits/moment.py | 10 +++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/cirq-core/cirq/circuits/circuit.py b/cirq-core/cirq/circuits/circuit.py index cc1d4e0186a..1991aa17ce6 100644 --- a/cirq-core/cirq/circuits/circuit.py +++ b/cirq-core/cirq/circuits/circuit.py @@ -1741,10 +1741,12 @@ def _create_from_earliest(self, contents): ckeys[k] = i length = max(length, i + 1) for i in range(length): + curr_ops = [] if i in moments: - self._moments.append(moments[i].with_operations(opses[i])) - else: - self._moments.append(Moment(opses[i])) + curr_ops.extend(moments[i]._operations) + if i in opses: + curr_ops.extend(opses[i]) + self._moments.append(Moment._create_unchecked(tuple(curr_ops))) def __copy__(self) -> 'cirq.Circuit': return self.copy() diff --git a/cirq-core/cirq/circuits/moment.py b/cirq-core/cirq/circuits/moment.py index f02cff47167..e49a4c177bb 100644 --- a/cirq-core/cirq/circuits/moment.py +++ b/cirq-core/cirq/circuits/moment.py @@ -94,7 +94,7 @@ def __init__(self, *contents: 'cirq.OP_TREE') -> None: # An internal dictionary to support efficient operation access by qubit. self._qubit_to_op: Dict['cirq.Qid', 'cirq.Operation'] = {} - for op in self.operations: + for op in self._operations: for q in op.qubits: # Check that operations don't overlap. if q in self._qubit_to_op: @@ -105,6 +105,14 @@ def __init__(self, *contents: 'cirq.OP_TREE') -> None: self._measurement_key_objs: Optional[AbstractSet['cirq.MeasurementKey']] = None self._control_keys: Optional[FrozenSet['cirq.MeasurementKey']] = None + @classmethod + def _create_unchecked(cls, operations: Tuple['cirq.Operation']): + moment = cls() + moment._operations = operations + moment._qubit_to_op = {q: op for op in operations for q in op.qubits} + moment._qubits = frozenset(moment._qubit_to_op.keys()) + return moment + @property def operations(self) -> Tuple['cirq.Operation', ...]: return self._operations From b0f87e74eebb55e2dd91424d9752ea49feb0928f Mon Sep 17 00:00:00 2001 From: daxfohl Date: Thu, 5 May 2022 18:13:16 -0700 Subject: [PATCH 15/24] Revert "Create moment unchecked" This reverts commit 9c1d3edcd5a3ecc04ff87931882df13c4d13142c. --- cirq-core/cirq/circuits/circuit.py | 8 +++----- cirq-core/cirq/circuits/moment.py | 10 +--------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/cirq-core/cirq/circuits/circuit.py b/cirq-core/cirq/circuits/circuit.py index 1991aa17ce6..cc1d4e0186a 100644 --- a/cirq-core/cirq/circuits/circuit.py +++ b/cirq-core/cirq/circuits/circuit.py @@ -1741,12 +1741,10 @@ def _create_from_earliest(self, contents): ckeys[k] = i length = max(length, i + 1) for i in range(length): - curr_ops = [] if i in moments: - curr_ops.extend(moments[i]._operations) - if i in opses: - curr_ops.extend(opses[i]) - self._moments.append(Moment._create_unchecked(tuple(curr_ops))) + self._moments.append(moments[i].with_operations(opses[i])) + else: + self._moments.append(Moment(opses[i])) def __copy__(self) -> 'cirq.Circuit': return self.copy() diff --git a/cirq-core/cirq/circuits/moment.py b/cirq-core/cirq/circuits/moment.py index e49a4c177bb..f02cff47167 100644 --- a/cirq-core/cirq/circuits/moment.py +++ b/cirq-core/cirq/circuits/moment.py @@ -94,7 +94,7 @@ def __init__(self, *contents: 'cirq.OP_TREE') -> None: # An internal dictionary to support efficient operation access by qubit. self._qubit_to_op: Dict['cirq.Qid', 'cirq.Operation'] = {} - for op in self._operations: + for op in self.operations: for q in op.qubits: # Check that operations don't overlap. if q in self._qubit_to_op: @@ -105,14 +105,6 @@ def __init__(self, *contents: 'cirq.OP_TREE') -> None: self._measurement_key_objs: Optional[AbstractSet['cirq.MeasurementKey']] = None self._control_keys: Optional[FrozenSet['cirq.MeasurementKey']] = None - @classmethod - def _create_unchecked(cls, operations: Tuple['cirq.Operation']): - moment = cls() - moment._operations = operations - moment._qubit_to_op = {q: op for op in operations for q in op.qubits} - moment._qubits = frozenset(moment._qubit_to_op.keys()) - return moment - @property def operations(self) -> Tuple['cirq.Operation', ...]: return self._operations From 69ce82f60c25e25b830dc97cb6d63973095dee78 Mon Sep 17 00:00:00 2001 From: daxfohl Date: Mon, 9 May 2022 14:30:44 -0700 Subject: [PATCH 16/24] docs --- cirq-core/cirq/circuits/circuit.py | 59 ++++++++++++++++++++++--- cirq-core/cirq/circuits/circuit_test.py | 7 ++- 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/cirq-core/cirq/circuits/circuit.py b/cirq-core/cirq/circuits/circuit.py index cc1d4e0186a..df44f3027ab 100644 --- a/cirq-core/cirq/circuits/circuit.py +++ b/cirq-core/cirq/circuits/circuit.py @@ -1705,26 +1705,65 @@ def __init__( self._moments: List['cirq.Moment'] = [] with _compat.block_overlapping_deprecation('.*'): if strategy == InsertStrategy.EARLIEST: - self._create_from_earliest(contents) + self._load_contents_with_earliest_strategy(contents) else: self.append(contents, strategy=strategy) - def _create_from_earliest(self, contents): - qubits = defaultdict(lambda: -1) - mkeys = defaultdict(lambda: -1) - ckeys = defaultdict(lambda: -1) - opses = defaultdict(list) - moments = {} + def _load_contents_with_earliest_strategy(self, contents: 'cirq.OP_TREE'): + """Optimized algorithm to load contents quickly. + + The default algorithm appends operations one-at-a-time, letting them + fall back until they encounter a moment they cannot commute with. This + is slow because it requires re-checking for conflicts at each moment. + + Here, we instead keep track of the greatest moment that contains each + qubit, measurement key, and control key, and append the operation to + the moment after the maximum of these. This avoids having to check each + moment. + + Args: + contents: The initial list of moments and operations defining the + circuit. You can also pass in operations, lists of operations, + or generally anything meeting the `cirq.OP_TREE` contract. + Non-moment entries will be inserted according to the specified + insertion strategy. + """ + # Initialize dicts from the qubit/key to the greatest moment index that has it. It is safe + # to default to `-1`, as that is interpreted as meaning the zeroth index onward does not + # have this value. + qubits: Dict['cirq.Qid', int] = defaultdict(lambda: -1) + mkeys: Dict['cirq.MeasurementKey', int] = defaultdict(lambda: -1) + ckeys: Dict['cirq.MeasurementKey', int] = defaultdict(lambda: -1) + + # We also maintain the dict from moment index to moments/ops that go into it, for use when + # building the actual moments at the end. + opses: Dict[int, List['cirq.Operation']] = defaultdict(list) + moments: Dict[int, 'cirq.Moment'] = {} + + # For keeping track of length of the circuit thus far. length = 0 + + # "mop" means current moment-or-operation for mop in ops.flatten_to_ops_or_moments(contents): mop_qubits = mop.qubits mop_mkeys = protocols.measurement_key_objs(mop) mop_ckeys = protocols.control_keys(mop) + + # Both branches define `i`, the moment index at which to place the mop. if isinstance(mop, Moment): + # We always append moment to the end, as does `self.append` i = length moments[i] = mop else: + # Initially we define `i` as the greatest moment index that has a conflict. We + # increment it at the end of the branch. i = -1 + + # Look for the maximum conflict; i.e. a moment that has the same qubit as this op, + # that has a measurement or control key the same as of of this op's measurement + # keys, or that has a measurement key the same as one of this op's control keys. + # (Control keys alone can commute past each other). The `ifs` are logically + # unnecessary but seem to make this slightly faster. if mop_qubits: i = max(i, *[qubits[q] for q in mop_qubits]) if mop_mkeys: @@ -1733,6 +1772,9 @@ def _create_from_earliest(self, contents): i = max(i, *[mkeys[k] for k in mop_ckeys]) i += 1 opses[i].append(mop) + + # Update our dicts with data from the latest mop placement. Note `i` will always be + # greater than the existing value for all of these, so this is safe. for q in mop_qubits: qubits[q] = i for k in mop_mkeys: @@ -1740,6 +1782,9 @@ def _create_from_earliest(self, contents): for k in mop_ckeys: ckeys[k] = i length = max(length, i + 1) + + # Finally once everything is placed, we can construct and append the actual moments for + # each index. for i in range(length): if i in moments: self._moments.append(moments[i].with_operations(opses[i])) diff --git a/cirq-core/cirq/circuits/circuit_test.py b/cirq-core/cirq/circuits/circuit_test.py index c5d3eb29d4d..d3faad57995 100644 --- a/cirq-core/cirq/circuits/circuit_test.py +++ b/cirq-core/cirq/circuits/circuit_test.py @@ -4607,10 +4607,15 @@ def _circuit_diagram_info_(self, args) -> str: def test_create_speed(): + # Added in https://github.com/quantumlib/Cirq/pull/5332 + # Previously this took ~30s to run. Now it should take ~150ms. However the coverage test can + # run this slowly, so allowing 2 sec to account for things like that. Feel free to increase the + # buffer time or delete the test entirely if it ends up causing flakes. qs = 100 moments = 500 xs = [cirq.X(cirq.LineQubit(i)) for i in range(qs)] opa = [xs[i] for i in range(qs) for _ in range(moments)] t = time.perf_counter() - _ = cirq.Circuit(opa) + c = cirq.Circuit(opa) + assert len(c) == moments assert time.perf_counter() - t < 2 From 968d1478775e2514a4ba2d914b40757415e2f6ec Mon Sep 17 00:00:00 2001 From: daxfohl Date: Mon, 9 May 2022 14:32:22 -0700 Subject: [PATCH 17/24] format --- cirq-core/cirq/circuits/circuit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-core/cirq/circuits/circuit.py b/cirq-core/cirq/circuits/circuit.py index df44f3027ab..79cde740fc9 100644 --- a/cirq-core/cirq/circuits/circuit.py +++ b/cirq-core/cirq/circuits/circuit.py @@ -1727,7 +1727,7 @@ def _load_contents_with_earliest_strategy(self, contents: 'cirq.OP_TREE'): or generally anything meeting the `cirq.OP_TREE` contract. Non-moment entries will be inserted according to the specified insertion strategy. - """ + """ # Initialize dicts from the qubit/key to the greatest moment index that has it. It is safe # to default to `-1`, as that is interpreted as meaning the zeroth index onward does not # have this value. From 1f97f273d5f7dc877d254d8741842d3a68bd3ae9 Mon Sep 17 00:00:00 2001 From: daxfohl Date: Mon, 9 May 2022 14:38:59 -0700 Subject: [PATCH 18/24] Fix docstring --- cirq-core/cirq/circuits/circuit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-core/cirq/circuits/circuit.py b/cirq-core/cirq/circuits/circuit.py index 79cde740fc9..9941be73bce 100644 --- a/cirq-core/cirq/circuits/circuit.py +++ b/cirq-core/cirq/circuits/circuit.py @@ -1725,7 +1725,7 @@ def _load_contents_with_earliest_strategy(self, contents: 'cirq.OP_TREE'): contents: The initial list of moments and operations defining the circuit. You can also pass in operations, lists of operations, or generally anything meeting the `cirq.OP_TREE` contract. - Non-moment entries will be inserted according to the specified + Non-moment entries will be inserted according to the EARLIEST insertion strategy. """ # Initialize dicts from the qubit/key to the greatest moment index that has it. It is safe From 8f3c02bd6a18ea8f681ceea5e9ba8b4a1ca5feaa Mon Sep 17 00:00:00 2001 From: daxfohl Date: Tue, 10 May 2022 19:11:50 -0700 Subject: [PATCH 19/24] Avoid protocol when getting keys from moment --- cirq-core/cirq/circuits/circuit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cirq-core/cirq/circuits/circuit.py b/cirq-core/cirq/circuits/circuit.py index 59c0b5fa80f..0f4515a0970 100644 --- a/cirq-core/cirq/circuits/circuit.py +++ b/cirq-core/cirq/circuits/circuit.py @@ -1975,11 +1975,11 @@ def earliest_available_moment( moment = self._moments[k] if moment.operates_on(op_qubits): return last_available - moment_measurement_keys = protocols.measurement_key_objs(moment) + moment_measurement_keys = moment._measurement_key_objs_() if ( not op_measurement_keys.isdisjoint(moment_measurement_keys) or not op_control_keys.isdisjoint(moment_measurement_keys) - or not protocols.control_keys(moment).isdisjoint(op_measurement_keys) + or not moment._control_keys_().isdisjoint(op_measurement_keys) ): return last_available if self._can_add_op_at(k, op): From 59fffc04602d5e993e94d436a0d445a13127bb4a Mon Sep 17 00:00:00 2001 From: daxfohl Date: Tue, 10 May 2022 19:19:09 -0700 Subject: [PATCH 20/24] Improve docs --- cirq-core/cirq/circuits/circuit.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/cirq-core/cirq/circuits/circuit.py b/cirq-core/cirq/circuits/circuit.py index 0f4515a0970..8713607e9da 100644 --- a/cirq-core/cirq/circuits/circuit.py +++ b/cirq-core/cirq/circuits/circuit.py @@ -1736,7 +1736,7 @@ def _load_contents_with_earliest_strategy(self, contents: 'cirq.OP_TREE'): Non-moment entries will be inserted according to the EARLIEST insertion strategy. """ - # Initialize dicts from the qubit/key to the greatest moment index that has it. It is safe + # These are dicts from the qubit/key to the greatest moment index that has it. It is safe # to default to `-1`, as that is interpreted as meaning the zeroth index onward does not # have this value. qubits: Dict['cirq.Qid', int] = defaultdict(lambda: -1) @@ -1763,15 +1763,16 @@ def _load_contents_with_earliest_strategy(self, contents: 'cirq.OP_TREE'): i = length moments[i] = mop else: - # Initially we define `i` as the greatest moment index that has a conflict. We - # increment it at the end of the branch. + # Initially we define `i` as the greatest moment index that has a conflict. `-1` is + # the initial conflict, and we search for larger ones. Once we get the largest one, + # we increment i by 1 to set the placement index. i = -1 - # Look for the maximum conflict; i.e. a moment that has the same qubit as this op, - # that has a measurement or control key the same as of of this op's measurement - # keys, or that has a measurement key the same as one of this op's control keys. - # (Control keys alone can commute past each other). The `ifs` are logically - # unnecessary but seem to make this slightly faster. + # Look for the maximum conflict; i.e. a moment that has a qubit the same as one of + # this op's qubits, that has a measurement or control key the same as one of this + # op's measurement keys, or that has a measurement key the same as one of this op's + # control keys. (Control keys alone can commute past each other). The `ifs` are + # logically unnecessary but seem to make this slightly faster. if mop_qubits: i = max(i, *[qubits[q] for q in mop_qubits]) if mop_mkeys: @@ -1782,7 +1783,8 @@ def _load_contents_with_earliest_strategy(self, contents: 'cirq.OP_TREE'): opses[i].append(mop) # Update our dicts with data from the latest mop placement. Note `i` will always be - # greater than the existing value for all of these, so this is safe. + # greater than the existing value for all of these, by construction, so there is no + # need to do a `max(i, existing)`. for q in mop_qubits: qubits[q] = i for k in mop_mkeys: From 1e01c355ab670e53ada35b1a1aa448a3a7c1725e Mon Sep 17 00:00:00 2001 From: daxfohl Date: Tue, 10 May 2022 19:21:26 -0700 Subject: [PATCH 21/24] Improve docs --- cirq-core/cirq/circuits/circuit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-core/cirq/circuits/circuit.py b/cirq-core/cirq/circuits/circuit.py index 8713607e9da..a144d8f58bb 100644 --- a/cirq-core/cirq/circuits/circuit.py +++ b/cirq-core/cirq/circuits/circuit.py @@ -1759,7 +1759,7 @@ def _load_contents_with_earliest_strategy(self, contents: 'cirq.OP_TREE'): # Both branches define `i`, the moment index at which to place the mop. if isinstance(mop, Moment): - # We always append moment to the end, as does `self.append` + # We always append moment to the end, to be consistent with `self.append` i = length moments[i] = mop else: From 75289c1609c5219ea456c38d9913d437b316256c Mon Sep 17 00:00:00 2001 From: Dax Fohl Date: Wed, 11 May 2022 06:09:13 -0700 Subject: [PATCH 22/24] rename vars --- cirq-core/cirq/circuits/circuit.py | 33 +++++++++++++++--------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/cirq-core/cirq/circuits/circuit.py b/cirq-core/cirq/circuits/circuit.py index a144d8f58bb..d95c9123408 100644 --- a/cirq-core/cirq/circuits/circuit.py +++ b/cirq-core/cirq/circuits/circuit.py @@ -1739,14 +1739,14 @@ def _load_contents_with_earliest_strategy(self, contents: 'cirq.OP_TREE'): # These are dicts from the qubit/key to the greatest moment index that has it. It is safe # to default to `-1`, as that is interpreted as meaning the zeroth index onward does not # have this value. - qubits: Dict['cirq.Qid', int] = defaultdict(lambda: -1) - mkeys: Dict['cirq.MeasurementKey', int] = defaultdict(lambda: -1) - ckeys: Dict['cirq.MeasurementKey', int] = defaultdict(lambda: -1) + qubit_indexes: Dict['cirq.Qid', int] = defaultdict(lambda: -1) + mkey_indexes: Dict['cirq.MeasurementKey', int] = defaultdict(lambda: -1) + ckey_indexes: Dict['cirq.MeasurementKey', int] = defaultdict(lambda: -1) # We also maintain the dict from moment index to moments/ops that go into it, for use when # building the actual moments at the end. - opses: Dict[int, List['cirq.Operation']] = defaultdict(list) - moments: Dict[int, 'cirq.Moment'] = {} + ops_at_index: Dict[int, List['cirq.Operation']] = defaultdict(list) + moment_at_index: Dict[int, 'cirq.Moment'] = {} # For keeping track of length of the circuit thus far. length = 0 @@ -1761,7 +1761,7 @@ def _load_contents_with_earliest_strategy(self, contents: 'cirq.OP_TREE'): if isinstance(mop, Moment): # We always append moment to the end, to be consistent with `self.append` i = length - moments[i] = mop + moment_at_index[i] = mop else: # Initially we define `i` as the greatest moment index that has a conflict. `-1` is # the initial conflict, and we search for larger ones. Once we get the largest one, @@ -1774,32 +1774,33 @@ def _load_contents_with_earliest_strategy(self, contents: 'cirq.OP_TREE'): # control keys. (Control keys alone can commute past each other). The `ifs` are # logically unnecessary but seem to make this slightly faster. if mop_qubits: - i = max(i, *[qubits[q] for q in mop_qubits]) + i = max(i, *[qubit_indexes[q] for q in mop_qubits]) if mop_mkeys: - i = max(i, *[mkeys[k] for k in mop_mkeys], *[ckeys[k] for k in mop_mkeys]) + i = max(i, *[mkey_indexes[k] for k in mop_mkeys]) + i = max(i, *[ckey_indexes[k] for k in mop_mkeys]) if mop_ckeys: - i = max(i, *[mkeys[k] for k in mop_ckeys]) + i = max(i, *[mkey_indexes[k] for k in mop_ckeys]) i += 1 - opses[i].append(mop) + ops_at_index[i].append(mop) # Update our dicts with data from the latest mop placement. Note `i` will always be # greater than the existing value for all of these, by construction, so there is no # need to do a `max(i, existing)`. for q in mop_qubits: - qubits[q] = i + qubit_indexes[q] = i for k in mop_mkeys: - mkeys[k] = i + mkey_indexes[k] = i for k in mop_ckeys: - ckeys[k] = i + ckey_indexes[k] = i length = max(length, i + 1) # Finally once everything is placed, we can construct and append the actual moments for # each index. for i in range(length): - if i in moments: - self._moments.append(moments[i].with_operations(opses[i])) + if i in moment_at_index: + self._moments.append(moment_at_index[i].with_operations(ops_at_index[i])) else: - self._moments.append(Moment(opses[i])) + self._moments.append(Moment(ops_at_index[i])) def __copy__(self) -> 'cirq.Circuit': return self.copy() From 2d3edb257e73d2e5efeb558359d9c271ecd2b205 Mon Sep 17 00:00:00 2001 From: Dax Fohl Date: Wed, 11 May 2022 06:34:10 -0700 Subject: [PATCH 23/24] comma --- cirq-core/cirq/circuits/circuit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-core/cirq/circuits/circuit.py b/cirq-core/cirq/circuits/circuit.py index d95c9123408..57c0ba10604 100644 --- a/cirq-core/cirq/circuits/circuit.py +++ b/cirq-core/cirq/circuits/circuit.py @@ -1794,7 +1794,7 @@ def _load_contents_with_earliest_strategy(self, contents: 'cirq.OP_TREE'): ckey_indexes[k] = i length = max(length, i + 1) - # Finally once everything is placed, we can construct and append the actual moments for + # Finally, once everything is placed, we can construct and append the actual moments for # each index. for i in range(length): if i in moment_at_index: From 09f80933bb30f87aba6fe10063a4fe28cab31484 Mon Sep 17 00:00:00 2001 From: Dax Fohl Date: Wed, 11 May 2022 17:23:56 -0700 Subject: [PATCH 24/24] dict names --- cirq-core/cirq/circuits/circuit.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cirq-core/cirq/circuits/circuit.py b/cirq-core/cirq/circuits/circuit.py index 57c0ba10604..13cc22a481d 100644 --- a/cirq-core/cirq/circuits/circuit.py +++ b/cirq-core/cirq/circuits/circuit.py @@ -1745,8 +1745,8 @@ def _load_contents_with_earliest_strategy(self, contents: 'cirq.OP_TREE'): # We also maintain the dict from moment index to moments/ops that go into it, for use when # building the actual moments at the end. - ops_at_index: Dict[int, List['cirq.Operation']] = defaultdict(list) - moment_at_index: Dict[int, 'cirq.Moment'] = {} + op_lists_by_index: Dict[int, List['cirq.Operation']] = defaultdict(list) + moments_by_index: Dict[int, 'cirq.Moment'] = {} # For keeping track of length of the circuit thus far. length = 0 @@ -1761,7 +1761,7 @@ def _load_contents_with_earliest_strategy(self, contents: 'cirq.OP_TREE'): if isinstance(mop, Moment): # We always append moment to the end, to be consistent with `self.append` i = length - moment_at_index[i] = mop + moments_by_index[i] = mop else: # Initially we define `i` as the greatest moment index that has a conflict. `-1` is # the initial conflict, and we search for larger ones. Once we get the largest one, @@ -1781,7 +1781,7 @@ def _load_contents_with_earliest_strategy(self, contents: 'cirq.OP_TREE'): if mop_ckeys: i = max(i, *[mkey_indexes[k] for k in mop_ckeys]) i += 1 - ops_at_index[i].append(mop) + op_lists_by_index[i].append(mop) # Update our dicts with data from the latest mop placement. Note `i` will always be # greater than the existing value for all of these, by construction, so there is no @@ -1797,10 +1797,10 @@ def _load_contents_with_earliest_strategy(self, contents: 'cirq.OP_TREE'): # Finally, once everything is placed, we can construct and append the actual moments for # each index. for i in range(length): - if i in moment_at_index: - self._moments.append(moment_at_index[i].with_operations(ops_at_index[i])) + if i in moments_by_index: + self._moments.append(moments_by_index[i].with_operations(op_lists_by_index[i])) else: - self._moments.append(Moment(ops_at_index[i])) + self._moments.append(Moment(op_lists_by_index[i])) def __copy__(self) -> 'cirq.Circuit': return self.copy()