Repository: quantumlib/Cirq

Issue:
Simulate factorizable circuit
**Is your feature request related to a use case or problem? Please describe.**

If you're running many "independent" circuits in parallel on a given set of qubits (where independent means there are no entangling gates between them; i.e. you can factorize the circuit (and therefore wavefunction)) you can simulate it much more efficiently than working in the full 2^(all n qubits) space.

**Describe the solution you'd like**

I think it should be possible to transform a circuit into an interaction graph, use a connected components algorithm to figure out how to split it into circuits operating on a subsets of qubits, and return a dictionary of state vectors keyed by the set of qubits.


**What is the urgency from your perspective for this issue? Is it blocking important work?**

Any sort of characterization that you run in parallel and requires simulating the circuit (read: XEB) would benefit from this! There's a version of this hacked into cirq.experiments.parallel_two_qubit_xeb
Simulate factorizable circuit
**Is your feature request related to a use case or problem? Please describe.**

If you're running many "independent" circuits in parallel on a given set of qubits (where independent means there are no entangling gates between them; i.e. you can factorize the circuit (and therefore wavefunction)) you can simulate it much more efficiently than working in the full 2^(all n qubits) space.

**Describe the solution you'd like**

I think it should be possible to transform a circuit into an interaction graph, use a connected components algorithm to figure out how to split it into circuits operating on a subsets of qubits, and return a dictionary of state vectors keyed by the set of qubits.


**What is the urgency from your perspective for this issue? Is it blocking important work?**

Any sort of characterization that you run in parallel and requires simulating the circuit (read: XEB) would benefit from this! There's a version of this hacked into cirq.experiments.parallel_two_qubit_xeb


Discussion:
Is someone already looking at this, or can I give it a shot?
(By the way, I couldn't locate cirq.experiments.parallel_two_qubit_xeb in this repository)
Let's chat about it on Cirq Cynq today @Ashalynd - thanks for volunteering!!
Sure, sounds good! (I'll be there today ~15 minutes later, schedule mangled due to the winter time).
Discussed on Cirq Cynque: 

Use case: mostly characterization, example: two qubit parallel XEB 

Initial design ideas: 

 1. `factorize_circuit`: Factor the circuit into smaller circuits if possible  - simulators could use this to attempt to factorize circuits in general 
 2. the reverse: create a function to run smaller circuits "in parallel" on the device/simulator and then post-processing: return the measurement results in a nicely sliceable form

Other thoughts: when SubCircuits are implemented keeping the factorized structure would be nice.




We just discussed this with Alan, who suggested that the reverse might be more interesting (and physically relevant) problem (with the reverse being understood as: given a number of circuits and the specific device (or a simulator), and the information about two-qubit error rates, figure out how many of them could be reliably run together on this device.

Will open another issue for that.
Thinking further about this.

Matthew @mpharrigan : a few clarifying questions :) 

1. Is it correct to assume that the circuits in two examples below (taken from tutorial) both have two factors (both could be split into [0] and [1, 2] in terms of the participating qubits): 

```
(0, 0): ───────H───────

(1, 0): ───@───@───H───
           │   │
(2, 0): ───@───@───H───
```

and here:

```
(0, 0): ───H───H───

(1, 0): ───────@───
               │
(2, 0): ───────@───
```

and from the point of factorization, this example:

```
(0, 0): ───H───────────

(1, 0): ───────H───────

(2, 0): ───────────H───
```

will be identical to this:

```
(0, 0): ───H───────────

(1, 0): ───H───────────

(2, 0): ───H───────────
```

and both will have three factors?


2. This seems to boil down to separating from each other groups of qubits, given the optrees defined on them. (Assumption is that every two qubits start independent, until their operation trees get merged because their descendants downstream have participated in a multi-qubit operation). Is this correct?

3. Does factorization still makes sense for the noisy circuits? (Intuitively, as the noise increases there is more chance that formerly independent " branches" will somehow interfere).
1: yes, all of your assertions are correct. The "H"s example: the two circuits are identical anyways (ignoring real-world noise, see below)

2. yes. You could probably implement this functionality by making a networkx graph representation of the circuit (where an edge is added for each two qubit interaction) and using the "connected components" algorithm to find the factors, as I mused in the original post

3. In the real world, cross-talk (noise which entangles qubits you wish to remain separate) can be a problem. This is difficult to model accurately in simulation. Usually we use a single qubit noise model (where noise operations are added to all the qubits but each noise operation only affects one qubit). If you use a noise model with 2-qubit noise operators, there's no reason why the factorization couldn't still work so long as the noisy operations are added to the circuit before you try to factorize it.
Given that there is already [CircuitDag](https://cirq.readthedocs.io/en/stable/generated/cirq.CircuitDag.html) (which is conveniently already based on [networkx.DiGraph](https://networkx.org/documentation/stable/reference/classes/digraph.html)), one way to achieve this is to use this class to convert the circuit to a DAG , factorize it as you suggest (e.g. with [networkx.algorithms.components.weakly_connected_components](https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.components.weakly_connected_components.html#networkx.algorithms.components.weakly_connected_components)) and then convert each of the resulting components back to CircuitDag and to a circuit.

CircuitDag already can convert to and from circuit (although at least within Cirq repo this doesn't seem to be used anywhere), the only missing stone here is finding the correspondence between the nodes in factors and the operations in the original graph to recreate CircuitDag objects.

If that seems reasonable, I am going to implement CircuitDag.factorize method to generate a sequence of DAGs, if that works out then the rest should be relatively easy.
I don't think CircuitDag is exactly what I had in mind. The documentation is a bit sketchy, but it seems like it is representing a graph of the time-like structure of the circuit ("this operation must happen after that operation") where I was thinking of a space-like structure ("this qubit talks to that qubit sometime(s)")
Looking at CircuitDag, it might serve in the way you suggest if a different `can_reorder` strategy gets implemented (currently it just checks whether the operations have qubits in common). 

On the other side, looking at the examples, many of them represent time-like circuits, and the docs don't talk much about the difference between the two. I feel like I am missing some piece of understanding here, having never seen the actual devices :) Could we discuss the space-like vs time-like circuits today during the sync?
Actually things became a bit clearer to me after I looked at cross_entropy_benchmarking example :)
Here is a proof of concept, to illustrate what I meant: https://github.com/quantumlib/Cirq/pull/3599 
(By the way, thanks for the chat today - it had an unblocking effect :) )
Is someone already looking at this, or can I give it a shot?
(By the way, I couldn't locate cirq.experiments.parallel_two_qubit_xeb in this repository)
Let's chat about it on Cirq Cynq today @Ashalynd - thanks for volunteering!!
Sure, sounds good! (I'll be there today ~15 minutes later, schedule mangled due to the winter time).
Discussed on Cirq Cynque: 

Use case: mostly characterization, example: two qubit parallel XEB 

Initial design ideas: 

 1. `factorize_circuit`: Factor the circuit into smaller circuits if possible  - simulators could use this to attempt to factorize circuits in general 
 2. the reverse: create a function to run smaller circuits "in parallel" on the device/simulator and then post-processing: return the measurement results in a nicely sliceable form

Other thoughts: when SubCircuits are implemented keeping the factorized structure would be nice.




We just discussed this with Alan, who suggested that the reverse might be more interesting (and physically relevant) problem (with the reverse being understood as: given a number of circuits and the specific device (or a simulator), and the information about two-qubit error rates, figure out how many of them could be reliably run together on this device.

Will open another issue for that.
Thinking further about this.

Matthew @mpharrigan : a few clarifying questions :) 

1. Is it correct to assume that the circuits in two examples below (taken from tutorial) both have two factors (both could be split into [0] and [1, 2] in terms of the participating qubits): 

```
(0, 0): ───────H───────

(1, 0): ───@───@───H───
           │   │
(2, 0): ───@───@───H───
```

and here:

```
(0, 0): ───H───H───

(1, 0): ───────@───
               │
(2, 0): ───────@───
```

and from the point of factorization, this example:

```
(0, 0): ───H───────────

(1, 0): ───────H───────

(2, 0): ───────────H───
```

will be identical to this:

```
(0, 0): ───H───────────

(1, 0): ───H───────────

(2, 0): ───H───────────
```

and both will have three factors?


2. This seems to boil down to separating from each other groups of qubits, given the optrees defined on them. (Assumption is that every two qubits start independent, until their operation trees get merged because their descendants downstream have participated in a multi-qubit operation). Is this correct?

3. Does factorization still makes sense for the noisy circuits? (Intuitively, as the noise increases there is more chance that formerly independent " branches" will somehow interfere).
1: yes, all of your assertions are correct. The "H"s example: the two circuits are identical anyways (ignoring real-world noise, see below)

2. yes. You could probably implement this functionality by making a networkx graph representation of the circuit (where an edge is added for each two qubit interaction) and using the "connected components" algorithm to find the factors, as I mused in the original post

3. In the real world, cross-talk (noise which entangles qubits you wish to remain separate) can be a problem. This is difficult to model accurately in simulation. Usually we use a single qubit noise model (where noise operations are added to all the qubits but each noise operation only affects one qubit). If you use a noise model with 2-qubit noise operators, there's no reason why the factorization couldn't still work so long as the noisy operations are added to the circuit before you try to factorize it.
Given that there is already [CircuitDag](https://cirq.readthedocs.io/en/stable/generated/cirq.CircuitDag.html) (which is conveniently already based on [networkx.DiGraph](https://networkx.org/documentation/stable/reference/classes/digraph.html)), one way to achieve this is to use this class to convert the circuit to a DAG , factorize it as you suggest (e.g. with [networkx.algorithms.components.weakly_connected_components](https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.components.weakly_connected_components.html#networkx.algorithms.components.weakly_connected_components)) and then convert each of the resulting components back to CircuitDag and to a circuit.

CircuitDag already can convert to and from circuit (although at least within Cirq repo this doesn't seem to be used anywhere), the only missing stone here is finding the correspondence between the nodes in factors and the operations in the original graph to recreate CircuitDag objects.

If that seems reasonable, I am going to implement CircuitDag.factorize method to generate a sequence of DAGs, if that works out then the rest should be relatively easy.
I don't think CircuitDag is exactly what I had in mind. The documentation is a bit sketchy, but it seems like it is representing a graph of the time-like structure of the circuit ("this operation must happen after that operation") where I was thinking of a space-like structure ("this qubit talks to that qubit sometime(s)")
Looking at CircuitDag, it might serve in the way you suggest if a different `can_reorder` strategy gets implemented (currently it just checks whether the operations have qubits in common). 

On the other side, looking at the examples, many of them represent time-like circuits, and the docs don't talk much about the difference between the two. I feel like I am missing some piece of understanding here, having never seen the actual devices :) Could we discuss the space-like vs time-like circuits today during the sync?
Actually things became a bit clearer to me after I looked at cross_entropy_benchmarking example :)
Here is a proof of concept, to illustrate what I meant: https://github.com/quantumlib/Cirq/pull/3599 
(By the way, thanks for the chat today - it had an unblocking effect :) )

Resolving patch:
diff --git a/cirq-core/cirq/circuits/circuit.py b/cirq-core/cirq/circuits/circuit.py
--- a/cirq-core/cirq/circuits/circuit.py
+++ b/cirq-core/cirq/circuits/circuit.py
@@ -47,6 +47,7 @@
 
 import abc
 import html
+import networkx
 import numpy as np
 
 from cirq import devices, ops, protocols, qis
@@ -108,6 +109,7 @@ class AbstractCircuit(abc.ABC):
     *   to_quil
     *   to_qasm
     *   save_qasm
+    *   get_independent_qubit_sets
     """
 
     @property
@@ -1387,6 +1389,87 @@ def tetris_concat(
 
         return cirq.Circuit(buffer[offset : offset + n_acc])
 
+    def get_independent_qubit_sets(self) -> List[Set['cirq.Qid']]:
+        """Divide circuit's qubits into independent qubit sets.
+
+        Independent qubit sets are the qubit sets such that there are
+        no entangling gates between qubits belonging to different sets.
+        If this is not possible, a sequence with a single factor (the whole set of
+        circuit's qubits) is returned.
+
+        >>> q0, q1, q2 = cirq.LineQubit.range(3)
+        >>> circuit = cirq.Circuit()
+        >>> circuit.append(cirq.Moment(cirq.H(q2)))
+        >>> circuit.append(cirq.Moment(cirq.CZ(q0,q1)))
+        >>> circuit.append(cirq.H(q0))
+        >>> print(circuit)
+        0: ───────@───H───
+                  │
+        1: ───────@───────
+        <BLANKLINE>
+        2: ───H───────────
+        >>> [sorted(qs) for qs in circuit.get_independent_qubit_sets()]
+        [[cirq.LineQubit(0), cirq.LineQubit(1)], [cirq.LineQubit(2)]]
+
+        Returns:
+            The list of independent qubit sets.
+
+        """
+        uf = networkx.utils.UnionFind(self.all_qubits())
+        for op in self.all_operations():
+            if len(op.qubits) > 1:
+                uf.union(*op.qubits)
+        return sorted([qs for qs in uf.to_sets()], key=min)
+
+    def factorize(self: CIRCUIT_TYPE) -> Iterable[CIRCUIT_TYPE]:
+        """Factorize circuit into a sequence of independent circuits (factors).
+
+        Factorization is possible when the circuit's qubits can be divided
+        into two or more independent qubit sets. Preserves the moments from
+        the original circuit.
+        If this is not possible, returns the set consisting of the single
+        circuit (this one).
+
+        >>> q0, q1, q2 = cirq.LineQubit.range(3)
+        >>> circuit = cirq.Circuit()
+        >>> circuit.append(cirq.Moment(cirq.H(q2)))
+        >>> circuit.append(cirq.Moment(cirq.CZ(q0,q1)))
+        >>> circuit.append(cirq.H(q0))
+        >>> print(circuit)
+        0: ───────@───H───
+                  │
+        1: ───────@───────
+        <BLANKLINE>
+        2: ───H───────────
+        >>> for i, f in enumerate(circuit.factorize()):
+        ...     print("Factor {}".format(i))
+        ...     print(f)
+        ...
+        Factor 0
+        0: ───────@───H───
+                  │
+        1: ───────@───────
+        Factor 1
+        2: ───H───────────
+
+        Returns:
+            The sequence of circuits, each including only the qubits from one
+            independent qubit set.
+
+        """
+
+        qubit_factors = self.get_independent_qubit_sets()
+        if len(qubit_factors) == 1:
+            return (self,)
+        # Note: In general, Moment.__getitem__ returns all operations on which
+        # any of the qubits operate. However, in this case we know that all of
+        # the qubits from one factor belong to a specific independent qubit set.
+        # This makes it possible to create independent circuits based on these
+        # moments.
+        return (
+            self._with_sliced_moments([m[qubits] for m in self.moments]) for qubits in qubit_factors
+        )
+
 
 def _overlap_collision_time(
     c1: Sequence['cirq.Moment'], c2: Sequence['cirq.Moment'], align: 'cirq.Alignment'
@@ -1460,6 +1543,7 @@ class Circuit(AbstractCircuit):
     *   to_quil
     *   to_qasm
     *   save_qasm
+    *   get_independent_qubit_sets
 
     Methods for mutation:
 
@@ -1503,6 +1587,10 @@ class Circuit(AbstractCircuit):
 
     and mutated,
     *    `circuit[1:7] = [Moment(...)]`
+
+    and factorized,
+    *   `circuit.factorize()` returns a sequence of Circuits which represent
+            independent 'factors' of the original Circuit.
     """
 
     def __init__(


Test patch:
diff --git a/cirq-core/cirq/circuits/circuit_test.py b/cirq-core/cirq/circuits/circuit_test.py
--- a/cirq-core/cirq/circuits/circuit_test.py
+++ b/cirq-core/cirq/circuits/circuit_test.py
@@ -4610,3 +4610,82 @@ def test_tetris_concat():
     assert type(v) is cirq.Circuit and v == ha
     v = cirq.FrozenCircuit.tetris_concat(ha, empty)
     assert type(v) is cirq.FrozenCircuit and v == ha.freeze()
+
+
+def test_factorize_one_factor():
+    circuit = cirq.Circuit()
+    q0, q1, q2 = cirq.LineQubit.range(3)
+    circuit.append(
+        [cirq.Moment([cirq.CZ(q0, q1), cirq.H(q2)]), cirq.Moment([cirq.H(q0), cirq.CZ(q1, q2)])]
+    )
+    factors = list(circuit.factorize())
+    assert len(factors) == 1
+    assert factors[0] == circuit
+    desired = """
+0: ───@───H───
+      │
+1: ───@───@───
+          │
+2: ───H───@───
+"""
+    cirq.testing.assert_has_diagram(factors[0], desired)
+
+
+def test_factorize_simple_circuit_two_factors():
+    circuit = cirq.Circuit()
+    q0, q1, q2 = cirq.LineQubit.range(3)
+    circuit.append([cirq.H(q1), cirq.CZ(q0, q1), cirq.H(q2), cirq.H(q0), cirq.H(q0)])
+    factors = list(circuit.factorize())
+    assert len(factors) == 2
+    desired = [
+        """
+0: ───────@───H───H───
+          │
+1: ───H───@───────────
+""",
+        """
+2: ───H───────────────
+""",
+    ]
+    for f, d in zip(factors, desired):
+        cirq.testing.assert_has_diagram(f, d)
+
+
+def test_factorize_large_circuit():
+    circuit = cirq.Circuit()
+    qubits = cirq.GridQubit.rect(3, 3)
+    circuit.append(cirq.Moment(cirq.X(q) for q in qubits))
+    pairset = [[(0, 2), (4, 6)], [(1, 2), (4, 8)]]
+    for pairs in pairset:
+        circuit.append(cirq.Moment(cirq.CZ(qubits[a], qubits[b]) for (a, b) in pairs))
+    circuit.append(cirq.Moment(cirq.Y(q) for q in qubits))
+    # expect 5 factors
+    factors = list(circuit.factorize())
+    desired = [
+        """
+(0, 0): ───X───@───────Y───
+               │
+(0, 1): ───X───┼───@───Y───
+               │   │
+(0, 2): ───X───@───@───Y───
+""",
+        """
+(1, 0): ───X───────────Y───
+""",
+        """
+(1, 1): ───X───@───@───Y───
+               │   │
+(2, 0): ───X───@───┼───Y───
+                   │
+(2, 2): ───X───────@───Y───
+""",
+        """
+(1, 2): ───X───────────Y───
+""",
+        """
+(2, 1): ───X───────────Y───
+    """,
+    ]
+    assert len(factors) == 5
+    for f, d in zip(factors, desired):
+        cirq.testing.assert_has_diagram(f, d)