Skip to content

Commit

Permalink
DAGCircuit: Add get_causal_cone method (#10325)
Browse files Browse the repository at this point in the history
* Feat: Add `get_causal_node` to `DAGCircuit`:
- Also added `get_qubit_input_node` and `get_qubit_output_node`.

* Test: Added tests to `dagcircuit.py`

* Docs: Added release note

* Chore: Remove type-checking in `dagcircuit.py`
- Type checking was causing strange behavior during linting.

* Added changes to speed up get_causal_cone (#1)

- Replace lists with deque for the iteration.

* Docs: Modify docstring and release note

* Fix: Wrong comparison in `_get_input_output_node`

* Remove: input and output node methods.

* Lint: Fixed formatting

* Docs: Fixed release-note

* Docs: Fixed docstring and release note.

* Fix: Output map double-lookup.

* Docs: Fix inline comments.

* Test: Added test for circuits with barriers

* Refactor: rename to `quantum_causal_cone`

* FIx: Use quantum_sucessors and docstring

---------

Co-authored-by: danielleodigie <[email protected]>
  • Loading branch information
raynelfss and danielleodigie authored Jul 18, 2023
1 parent 35f9e7c commit 90c2fe6
Show file tree
Hide file tree
Showing 3 changed files with 231 additions and 1 deletion.
55 changes: 54 additions & 1 deletion qiskit/dagcircuit/dagcircuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
composed, and modified. Some natural properties like depth can be computed
directly from the graph.
"""
from collections import OrderedDict, defaultdict
from collections import OrderedDict, defaultdict, deque
import copy
import itertools
import math
Expand Down Expand Up @@ -1891,6 +1891,59 @@ def count_ops_longest_path(self):
op_dict[name] += 1
return op_dict

def quantum_causal_cone(self, qubit):
"""
Returns causal cone of a qubit.
A qubit's causal cone is the set of qubits that can influence the output of that
qubit through interactions, whether through multi-qubit gates or operations. Knowing
the causal cone of a qubit can be useful when debugging faulty circuits, as it can
help identify which wire(s) may be causing the problem.
This method does not consider any classical data dependency in the ``DAGCircuit``,
classical bit wires are ignored for the purposes of building the causal cone.
Args:
qubit (Qubit): The output qubit for which we want to find the causal cone.
Returns:
Set[Qubit]: The set of qubits whose interactions affect ``qubit``.
"""
# Retrieve the output node from the qubit
output_node = self.output_map.get(qubit, None)
if not output_node:
raise DAGCircuitError(f"Qubit {qubit} is not part of this circuit.")
# Add the qubit to the causal cone.
qubits_to_check = {qubit}
# Add predecessors of output node to the queue.
queue = deque(self.predecessors(output_node))

# While queue isn't empty
while queue:
# Pop first element.
node_to_check = queue.popleft()
# Check whether element is input or output node.
if isinstance(node_to_check, DAGOpNode):
# Keep all the qubits in the operation inside a set.
qubit_set = set(node_to_check.qargs)
# Check if there are any qubits in common and that the operation is not a barrier.
if (
len(qubit_set.intersection(qubits_to_check)) > 0
and node_to_check.op.name != "barrier"
and not getattr(node_to_check.op, "_directive")
):
# If so, add all the qubits to the causal cone.
qubits_to_check = qubits_to_check.union(qubit_set)
# For each predecessor of the current node, filter input/output nodes,
# also make sure it has at least one qubit in common. Then append.
for node in self.quantum_predecessors(node_to_check):
if (
isinstance(node, DAGOpNode)
and len(qubits_to_check.intersection(set(node.qargs))) > 0
):
queue.append(node)
return qubits_to_check

def properties(self):
"""Return a dictionary of circuit properties."""
summary = {
Expand Down
25 changes: 25 additions & 0 deletions releasenotes/notes/add-dag-causal-cone-5a19311e40fbb3af.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
features:
- |
Added :meth:`.DAGCircuit.quantum_causal_cone` to obtain the causal cone of a qubit
in a :class:`~.DAGCircuit`.
The following example shows its correct usage::
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.circuit.library import CXGate, CZGate
from qiskit.dagcircuit import DAGCircuit
# Build a DAGCircuit
dag = DAGCircuit()
qreg = QuantumRegister(5)
creg = ClassicalRegister(5)
dag.add_qreg(qreg)
dag.add_creg(creg)
dag.apply_operation_back(CXGate(), qreg[[1, 2]], [])
dag.apply_operation_back(CXGate(), qreg[[0, 3]], [])
dag.apply_operation_back(CZGate(), qreg[[1, 4]], [])
dag.apply_operation_back(CZGate(), qreg[[2, 4]], [])
dag.apply_operation_back(CXGate(), qreg[[3, 4]], [])
# Get the causal cone of qubit at index 0
result = dag.quantum_causal_cone(qreg[0])
152 changes: 152 additions & 0 deletions test/python/dagcircuit/test_dagcircuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2385,5 +2385,157 @@ def test_2q_swap_partially_connected_prepost_spectators(self):
self.assertEqual(dag, expected)


class TestDagCausalCone(QiskitTestCase):
"""Test `get_causal_node` function"""

def test_causal_cone_regular_circuit(self):
"""Test causal cone with a regular circuit"""

# q_0: ───────■─────────────
# │
# q_1: ──■────┼───■─────────
# ┌─┴─┐ │ │
# q_2: ┤ X ├──┼───┼──■──────
# └───┘┌─┴─┐ │ │
# q_3: ─────┤ X ├─┼──┼───■──
# └───┘ │ │ ┌─┴─┐
# q_4: ───────────■──■─┤ X ├
# └───┘
# c: 5/═════════════════════

dag = DAGCircuit()
qreg = QuantumRegister(5)
creg = ClassicalRegister(5)
dag.add_qreg(qreg)
dag.add_creg(creg)
dag.apply_operation_back(CXGate(), qreg[[1, 2]], [])
dag.apply_operation_back(CXGate(), qreg[[0, 3]], [])
dag.apply_operation_back(CZGate(), qreg[[1, 4]], [])
dag.apply_operation_back(CZGate(), qreg[[2, 4]], [])
dag.apply_operation_back(CXGate(), qreg[[3, 4]], [])

# Get causal cone of qubit at index 0
result = dag.quantum_causal_cone(qreg[0])

# Expected result
expected = set(qreg[[0, 3]])
self.assertEqual(result, expected)

def test_causal_cone_invalid_qubit(self):
"""Test causal cone with invalid qubit"""

# q_0: ───────■─────────────
# │
# q_1: ──■────┼───■─────────
# ┌─┴─┐ │ │
# q_2: ┤ X ├──┼───┼──■──────
# └───┘┌─┴─┐ │ │
# q_3: ─────┤ X ├─┼──┼───■──
# └───┘ │ │ ┌─┴─┐
# q_4: ───────────■──■─┤ X ├
# └───┘
# c: 5/═════════════════════

dag = DAGCircuit()
qreg = QuantumRegister(5)
creg = ClassicalRegister(5)
dag.add_qreg(qreg)
dag.add_creg(creg)
dag.apply_operation_back(CXGate(), qreg[[1, 2]], [])
dag.apply_operation_back(CXGate(), qreg[[0, 3]], [])
dag.apply_operation_back(CZGate(), qreg[[1, 4]], [])
dag.apply_operation_back(CZGate(), qreg[[2, 4]], [])
dag.apply_operation_back(CXGate(), qreg[[3, 4]], [])

# Raise error due to invalid index
self.assertRaises(DAGCircuitError, dag.quantum_causal_cone, Qubit())

def test_causal_cone_no_neighbor(self):
"""Test causal cone with no neighbor"""

# q_0: ───────────

# q_1: ──■───■────
# ┌─┴─┐ │
# q_2: ┤ X ├─┼──■─
# ├───┤ │ │
# q_3: ┤ X ├─┼──┼─
# └───┘ │ │
# q_4: ──────■──■─

# c: 5/═══════════

dag = DAGCircuit()
qreg = QuantumRegister(5)
creg = ClassicalRegister(5)
dag.add_qreg(qreg)
dag.add_creg(creg)
dag.apply_operation_back(CXGate(), qreg[[1, 2]], [])
dag.apply_operation_back(CZGate(), qreg[[1, 4]], [])
dag.apply_operation_back(CZGate(), qreg[[2, 4]], [])
dag.apply_operation_back(XGate(), qreg[[3]], [])

# Get causal cone of Qubit at index 3.
result = dag.quantum_causal_cone(qreg[3])
# Expect only a set with Qubit at index 3
expected = set(qreg[[3]])
self.assertEqual(result, expected)

def test_causal_cone_empty_circuit(self):
"""Test causal cone for circuit with no operations"""
dag = DAGCircuit()
qreg = QuantumRegister(5)
creg = ClassicalRegister(5)
dag.add_qreg(qreg)
dag.add_creg(creg)

# Get causal cone of qubit at index 4
result = dag.quantum_causal_cone(qreg[4])
# Expect only a set with Qubit at index 4
expected = set(qreg[[4]])

self.assertEqual(result, expected)

def test_causal_cone_barriers(self):
"""Test causal cone for circuit with barriers"""

# ┌───┐ ░ ░
# q1_0: ┤ X ├──■───░───────────────░───────────
# └───┘┌─┴─┐ ░ ░
# q1_1: ─────┤ X ├─░───────■───■───░───────────
# └───┘ ░ ┌───┐ │ │ ░ ┌───┐
# q1_2: ───────────░─┤ H ├─■───┼───░─┤ Y ├─────
# ░ └───┘ ┌─┴─┐ ░ └─┬─┘┌───┐
# q1_3: ───────────░─────────┤ Y ├─░───■──┤ X ├
# ░ └───┘ ░ └─┬─┘
# q1_4: ───────────░───────────────░────────■──
# ░ ░

# Build the circuit:
qreg = QuantumRegister(5)
qc = QuantumCircuit(qreg)
qc.x(0)
qc.cx(0, 1)
qc.barrier()

qc.h(2)
qc.cz(2, 1)
qc.cy(1, 3)
qc.barrier()

qc.cy(3, 2)
qc.cx(4, 3)

# Transform into a dag:
dag = circuit_to_dag(qc)

# Compute result:
result = dag.quantum_causal_cone(qreg[1])
# Expected:
expected = {qreg[0], qreg[1], qreg[2], qreg[3]}

self.assertEqual(result, expected)


if __name__ == "__main__":
unittest.main()

0 comments on commit 90c2fe6

Please sign in to comment.