diff --git a/cirq/__init__.py b/cirq/__init__.py index 38d20130f5f..342a95f706d 100644 --- a/cirq/__init__.py +++ b/cirq/__init__.py @@ -30,6 +30,7 @@ from cirq.circuits import ( Circuit, + CircuitDag, DropEmptyMoments, DropNegligible, ExpandComposite, diff --git a/cirq/circuits/__init__.py b/cirq/circuits/__init__.py index ce212a0e6fb..2ccbca0a9df 100644 --- a/cirq/circuits/__init__.py +++ b/cirq/circuits/__init__.py @@ -23,6 +23,9 @@ from cirq.circuits.circuit import ( Circuit, ) +from cirq.circuits.circuit_dag import ( + CircuitDag, +) from cirq.circuits.drop_empty_moments import ( DropEmptyMoments, ) diff --git a/cirq/circuits/circuit_dag.py b/cirq/circuits/circuit_dag.py new file mode 100644 index 00000000000..590d04afc4a --- /dev/null +++ b/cirq/circuits/circuit_dag.py @@ -0,0 +1,154 @@ +# Copyright 2018 The ops Developers +# +# 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 typing import Any, Callable, Generic, Iterator, TypeVar + +import networkx + +from cirq import ops, devices +from cirq.circuits import circuit + + +T = TypeVar('T') + +class Unique(Generic[T]): + """A wrapper for a value that doesn't compare equal to other instances. + + For example: 5 == 5 but Unique(5) != Unique(5). + + Unique is used by CircuitDag to wrap operations because nodes in a graph + are considered the same node if they compare equal to each other. X(q0) + in one moment of a Circuit and X(q0) in another moment of the Circuit are + wrapped by Unique(X(q0)) so they are distinct nodes in the graph. + """ + def __init__(self, val: T) -> None: + self.val = val + + def __repr__(self): + return 'Unique({}, {!r})'.format(id(self), self.val) + + +def _disjoint_qubits(op1: ops.Operation, op2: ops.Operation) -> bool: + """Returns true only if the operations have qubits in common.""" + return not set(op1.qubits) & set(op2.qubits) + + +class CircuitDag(networkx.DiGraph): + """A representation of a Circuit as a directed acyclic graph. + + Nodes of the graph are instances of Unique containing each operation of a + circuit. + + Edges of the graph are tuples of nodes. Each edge specifies a required + application order between two operations. The first must be applied before + the second. + + The graph is maximalist (transitive completion). + """ + + disjoint_qubits = staticmethod(_disjoint_qubits) + + def __init__(self, + can_reorder: Callable[[ops.Operation, ops.Operation], + bool] = _disjoint_qubits, + incoming_graph_data: Any = None, + device: devices.Device = devices.UnconstrainedDevice + ) -> None: + """Initializes a CircuitDag. + + Args: + can_reorder: A predicate that determines if two operations may be + reordered. Graph edges are created for pairs of operations + where this returns False. + + The default predicate allows reordering only when the operations + don't share common qubits. + incoming_graph_data: Data in initialize the graph. This can be any + value supported by networkx.DiGraph() e.g. an edge list or + another graph. + device: Hardware that the circuit should be able to run on. + """ + super().__init__(incoming_graph_data) + self.can_reorder = can_reorder + self.device = device + + @staticmethod + def make_node(op: ops.Operation) -> Unique: + return Unique(op) + + @staticmethod + def from_circuit(circuit: circuit.Circuit, + can_reorder: Callable[[ops.Operation, ops.Operation], + bool] = _disjoint_qubits + ) -> 'CircuitDag': + return CircuitDag.from_ops(circuit.all_operations(), + can_reorder=can_reorder, + device=circuit.device) + + @staticmethod + def from_ops(*operations: ops.OP_TREE, + can_reorder: Callable[[ops.Operation, ops.Operation], + bool] = _disjoint_qubits, + device: devices.Device = devices.UnconstrainedDevice + ) -> 'CircuitDag': + dag = CircuitDag(can_reorder=can_reorder, device=device) + for op in ops.flatten_op_tree(operations): + dag.append(op) + return dag + + def append(self, op: ops.Operation) -> None: + new_node = self.make_node(op) + self.add_edges_from([(node, new_node) + for node in self.nodes + if not self.can_reorder(node.val, new_node.val)]) + self.add_node(new_node) + + def all_operations(self) -> Iterator[ops.Operation]: + if not self.nodes: + return + g = self.copy() + + def get_root_node(some_node: Unique[ops.Operation] + ) -> Unique[ops.Operation]: + pred = g.pred + while pred[some_node]: + some_node = next(iter(pred[some_node])) + return some_node + + def get_first_node() -> Unique[ops.Operation]: + return get_root_node(next(iter(g.nodes))) + + def get_next_node(succ: networkx.classes.coreviews.AtlasView + ) -> Unique[ops.Operation]: + if succ: + return get_root_node(next(iter(succ))) + else: + return get_first_node() + + node = get_first_node() + while True: + yield node.val + succ = g.succ[node] + g.remove_node(node) + + if not g.nodes: + return + + node = get_next_node(succ) + + def to_circuit(self) -> circuit.Circuit: + return circuit.Circuit.from_ops( + self.all_operations(), + strategy=circuit.InsertStrategy.EARLIEST, + device=self.device) diff --git a/cirq/circuits/circuit_dag_test.py b/cirq/circuits/circuit_dag_test.py new file mode 100644 index 00000000000..f4453d56b30 --- /dev/null +++ b/cirq/circuits/circuit_dag_test.py @@ -0,0 +1,170 @@ +# Copyright 2018 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. +# 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. + +import networkx + +import cirq + + +def test_wrapper_eq(): + q0, q1 = cirq.LineQubit.range(2) + eq = cirq.testing.EqualsTester() + eq.add_equality_group(cirq.CircuitDag.make_node(cirq.X(q0))) + eq.add_equality_group(cirq.CircuitDag.make_node(cirq.X(q0))) + eq.add_equality_group(cirq.CircuitDag.make_node(cirq.Y(q0))) + eq.add_equality_group(cirq.CircuitDag.make_node(cirq.X(q1))) + + +def test_wrapper_repr(): + q0 = cirq.LineQubit(0) + + node = cirq.CircuitDag.make_node(cirq.X(q0)) + assert (repr(node) == + 'Unique(' + str(id(node)) + ', GateOperation(X, (LineQubit(0),)))') + + +def test_init(): + dag = cirq.CircuitDag() + assert networkx.dag.is_directed_acyclic_graph(dag) + assert list(dag.nodes) == [] + assert list(dag.edges) == [] + + +def test_append(): + q0 = cirq.LineQubit(0) + dag = cirq.CircuitDag() + dag.append(cirq.X(q0)) + dag.append(cirq.Y(q0)) + print(dag.edges) + assert networkx.dag.is_directed_acyclic_graph(dag) + assert len(dag.nodes) == 2 + assert ([(n1.val, n2.val) for n1, n2 in dag.edges] == + [(cirq.X(q0), cirq.Y(q0))]) + + +def test_two_identical_ops(): + q0 = cirq.LineQubit(0) + dag = cirq.CircuitDag() + dag.append(cirq.X(q0)) + dag.append(cirq.Y(q0)) + dag.append(cirq.X(q0)) + assert networkx.dag.is_directed_acyclic_graph(dag) + assert len(dag.nodes) == 3 + assert (set((n1.val, n2.val) for n1, n2 in dag.edges) == + set(((cirq.X(q0), cirq.Y(q0)), + (cirq.X(q0), cirq.X(q0)), + (cirq.Y(q0), cirq.X(q0))))) + + +def test_from_ops(): + q0 = cirq.LineQubit(0) + dag = cirq.CircuitDag.from_ops( + cirq.X(q0), + cirq.Y(q0)) + assert networkx.dag.is_directed_acyclic_graph(dag) + assert len(dag.nodes) == 2 + assert ([(n1.val, n2.val) for n1, n2 in dag.edges] == + [(cirq.X(q0), cirq.Y(q0))]) + + +def test_from_circuit(): + q0 = cirq.LineQubit(0) + circuit = cirq.Circuit.from_ops( + cirq.X(q0), + cirq.Y(q0)) + dag = cirq.CircuitDag.from_circuit(circuit) + assert networkx.dag.is_directed_acyclic_graph(dag) + assert len(dag.nodes) == 2 + assert ([(n1.val, n2.val) for n1, n2 in dag.edges] == + [(cirq.X(q0), cirq.Y(q0))]) + + +def test_from_circuit_with_device(): + q0 = cirq.GridQubit(5, 5) + circuit = cirq.Circuit.from_ops( + cirq.X(q0), + cirq.Y(q0), + device=cirq.google.Bristlecone) + dag = cirq.CircuitDag.from_circuit(circuit) + assert networkx.dag.is_directed_acyclic_graph(dag) + assert dag.device == circuit.device + assert len(dag.nodes) == 2 + assert ([(n1.val, n2.val) for n1, n2 in dag.edges] == + [(cirq.X(q0), cirq.Y(q0))]) + + +def test_to_empty_circuit(): + circuit = cirq.Circuit() + dag = cirq.CircuitDag.from_circuit(circuit) + assert networkx.dag.is_directed_acyclic_graph(dag) + assert circuit == dag.to_circuit() + + +def test_to_circuit(): + q0 = cirq.LineQubit(0) + circuit = cirq.Circuit.from_ops( + cirq.X(q0), + cirq.Y(q0)) + dag = cirq.CircuitDag.from_circuit(circuit) + + assert networkx.dag.is_directed_acyclic_graph(dag) + # Only one possible output circuit for this simple case + assert circuit == dag.to_circuit() + + cirq.testing.assert_allclose_up_to_global_phase( + circuit.to_unitary_matrix(), + dag.to_circuit().to_unitary_matrix(), + atol=1e-7) + + +def test_larger_circuit(): + q0, q1, q2, q3 = cirq.google.Bristlecone.col(5)[:4] + # This circuit does not have CZ gates on adjacent qubits because the order + # dag.to_circuit() would append them is non-deterministic. + circuit = cirq.Circuit.from_ops( + cirq.X(q0), + cirq.CZ(q1, q2), + cirq.CZ(q0, q1), + cirq.Y(q0), + cirq.Z(q0), + cirq.CZ(q1, q2), + cirq.X(q0), + cirq.Y(q0), + cirq.CZ(q0, q1), + cirq.T(q3), + strategy=cirq.InsertStrategy.EARLIEST, + device=cirq.google.Bristlecone) + + dag = cirq.CircuitDag.from_circuit(circuit) + + assert networkx.dag.is_directed_acyclic_graph(dag) + assert circuit.device == dag.to_circuit().device + # Operation order within a moment is non-deterministic + # but text diagrams still look the same. + assert (circuit.to_text_diagram() == + dag.to_circuit().to_text_diagram() == +""" +(0, 5): ───X───@───Y───Z───X───Y───@─── + │ │ +(1, 5): ───@───@───@───────────────@─── + │ │ +(2, 5): ───@───────@─────────────────── + +(3, 5): ───T─────────────────────────── +""".strip()) + + cirq.testing.assert_allclose_up_to_global_phase( + circuit.to_unitary_matrix(), + dag.to_circuit().to_unitary_matrix(), + atol=1e-7) diff --git a/continuous-integration/mypy.ini b/continuous-integration/mypy.ini index 901e05669ea..ad908593530 100644 --- a/continuous-integration/mypy.ini +++ b/continuous-integration/mypy.ini @@ -5,6 +5,6 @@ follow_imports = silent ignore_missing_imports = true # 3rd-party libs for which we don't have stubs -[mypy-absl.*,apiclient.*,google.protobuf.*,matplotlib.*,multiprocessing.dummy,numpy.*,oauth2client.*,pytest.*,scipy.*,sortedcontainers.*,setuptools.*,pylatex.*] +[mypy-absl.*,apiclient.*,google.protobuf.*,matplotlib.*,multiprocessing.dummy,numpy.*,oauth2client.*,pytest.*,scipy.*,sortedcontainers.*,setuptools.*,pylatex.*,networkx.*] follow_imports = silent ignore_missing_imports = true diff --git a/python2.7-runtime-requirements.txt b/python2.7-runtime-requirements.txt index 16705664124..70614690a2a 100644 --- a/python2.7-runtime-requirements.txt +++ b/python2.7-runtime-requirements.txt @@ -1,5 +1,6 @@ google-api-python-client~=1.6 matplotlib~=2.1 +networkx~=2.1 numpy~=1.13 protobuf~=3.5 sortedcontainers~=1.5 diff --git a/runtime-requirements.txt b/runtime-requirements.txt index 7ca5348136c..28aaebabba5 100644 --- a/runtime-requirements.txt +++ b/runtime-requirements.txt @@ -1,5 +1,6 @@ google-api-python-client~=1.6 matplotlib~=2.2 +networkx~=2.1 numpy~=1.14 protobuf~=3.5 requests~=2.18