Skip to content

Commit 90c2fe6

Browse files
DAGCircuit: Add get_causal_cone method (Qiskit#10325)
* 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]>
1 parent 35f9e7c commit 90c2fe6

File tree

3 files changed

+231
-1
lines changed

3 files changed

+231
-1
lines changed

qiskit/dagcircuit/dagcircuit.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
composed, and modified. Some natural properties like depth can be computed
2121
directly from the graph.
2222
"""
23-
from collections import OrderedDict, defaultdict
23+
from collections import OrderedDict, defaultdict, deque
2424
import copy
2525
import itertools
2626
import math
@@ -1891,6 +1891,59 @@ def count_ops_longest_path(self):
18911891
op_dict[name] += 1
18921892
return op_dict
18931893

1894+
def quantum_causal_cone(self, qubit):
1895+
"""
1896+
Returns causal cone of a qubit.
1897+
1898+
A qubit's causal cone is the set of qubits that can influence the output of that
1899+
qubit through interactions, whether through multi-qubit gates or operations. Knowing
1900+
the causal cone of a qubit can be useful when debugging faulty circuits, as it can
1901+
help identify which wire(s) may be causing the problem.
1902+
1903+
This method does not consider any classical data dependency in the ``DAGCircuit``,
1904+
classical bit wires are ignored for the purposes of building the causal cone.
1905+
1906+
Args:
1907+
qubit (Qubit): The output qubit for which we want to find the causal cone.
1908+
1909+
Returns:
1910+
Set[Qubit]: The set of qubits whose interactions affect ``qubit``.
1911+
"""
1912+
# Retrieve the output node from the qubit
1913+
output_node = self.output_map.get(qubit, None)
1914+
if not output_node:
1915+
raise DAGCircuitError(f"Qubit {qubit} is not part of this circuit.")
1916+
# Add the qubit to the causal cone.
1917+
qubits_to_check = {qubit}
1918+
# Add predecessors of output node to the queue.
1919+
queue = deque(self.predecessors(output_node))
1920+
1921+
# While queue isn't empty
1922+
while queue:
1923+
# Pop first element.
1924+
node_to_check = queue.popleft()
1925+
# Check whether element is input or output node.
1926+
if isinstance(node_to_check, DAGOpNode):
1927+
# Keep all the qubits in the operation inside a set.
1928+
qubit_set = set(node_to_check.qargs)
1929+
# Check if there are any qubits in common and that the operation is not a barrier.
1930+
if (
1931+
len(qubit_set.intersection(qubits_to_check)) > 0
1932+
and node_to_check.op.name != "barrier"
1933+
and not getattr(node_to_check.op, "_directive")
1934+
):
1935+
# If so, add all the qubits to the causal cone.
1936+
qubits_to_check = qubits_to_check.union(qubit_set)
1937+
# For each predecessor of the current node, filter input/output nodes,
1938+
# also make sure it has at least one qubit in common. Then append.
1939+
for node in self.quantum_predecessors(node_to_check):
1940+
if (
1941+
isinstance(node, DAGOpNode)
1942+
and len(qubits_to_check.intersection(set(node.qargs))) > 0
1943+
):
1944+
queue.append(node)
1945+
return qubits_to_check
1946+
18941947
def properties(self):
18951948
"""Return a dictionary of circuit properties."""
18961949
summary = {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
features:
3+
- |
4+
Added :meth:`.DAGCircuit.quantum_causal_cone` to obtain the causal cone of a qubit
5+
in a :class:`~.DAGCircuit`.
6+
The following example shows its correct usage::
7+
8+
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
9+
from qiskit.circuit.library import CXGate, CZGate
10+
from qiskit.dagcircuit import DAGCircuit
11+
12+
# Build a DAGCircuit
13+
dag = DAGCircuit()
14+
qreg = QuantumRegister(5)
15+
creg = ClassicalRegister(5)
16+
dag.add_qreg(qreg)
17+
dag.add_creg(creg)
18+
dag.apply_operation_back(CXGate(), qreg[[1, 2]], [])
19+
dag.apply_operation_back(CXGate(), qreg[[0, 3]], [])
20+
dag.apply_operation_back(CZGate(), qreg[[1, 4]], [])
21+
dag.apply_operation_back(CZGate(), qreg[[2, 4]], [])
22+
dag.apply_operation_back(CXGate(), qreg[[3, 4]], [])
23+
24+
# Get the causal cone of qubit at index 0
25+
result = dag.quantum_causal_cone(qreg[0])

test/python/dagcircuit/test_dagcircuit.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2385,5 +2385,157 @@ def test_2q_swap_partially_connected_prepost_spectators(self):
23852385
self.assertEqual(dag, expected)
23862386

23872387

2388+
class TestDagCausalCone(QiskitTestCase):
2389+
"""Test `get_causal_node` function"""
2390+
2391+
def test_causal_cone_regular_circuit(self):
2392+
"""Test causal cone with a regular circuit"""
2393+
2394+
# q_0: ───────■─────────────
2395+
# │
2396+
# q_1: ──■────┼───■─────────
2397+
# ┌─┴─┐ │ │
2398+
# q_2: ┤ X ├──┼───┼──■──────
2399+
# └───┘┌─┴─┐ │ │
2400+
# q_3: ─────┤ X ├─┼──┼───■──
2401+
# └───┘ │ │ ┌─┴─┐
2402+
# q_4: ───────────■──■─┤ X ├
2403+
# └───┘
2404+
# c: 5/═════════════════════
2405+
2406+
dag = DAGCircuit()
2407+
qreg = QuantumRegister(5)
2408+
creg = ClassicalRegister(5)
2409+
dag.add_qreg(qreg)
2410+
dag.add_creg(creg)
2411+
dag.apply_operation_back(CXGate(), qreg[[1, 2]], [])
2412+
dag.apply_operation_back(CXGate(), qreg[[0, 3]], [])
2413+
dag.apply_operation_back(CZGate(), qreg[[1, 4]], [])
2414+
dag.apply_operation_back(CZGate(), qreg[[2, 4]], [])
2415+
dag.apply_operation_back(CXGate(), qreg[[3, 4]], [])
2416+
2417+
# Get causal cone of qubit at index 0
2418+
result = dag.quantum_causal_cone(qreg[0])
2419+
2420+
# Expected result
2421+
expected = set(qreg[[0, 3]])
2422+
self.assertEqual(result, expected)
2423+
2424+
def test_causal_cone_invalid_qubit(self):
2425+
"""Test causal cone with invalid qubit"""
2426+
2427+
# q_0: ───────■─────────────
2428+
# │
2429+
# q_1: ──■────┼───■─────────
2430+
# ┌─┴─┐ │ │
2431+
# q_2: ┤ X ├──┼───┼──■──────
2432+
# └───┘┌─┴─┐ │ │
2433+
# q_3: ─────┤ X ├─┼──┼───■──
2434+
# └───┘ │ │ ┌─┴─┐
2435+
# q_4: ───────────■──■─┤ X ├
2436+
# └───┘
2437+
# c: 5/═════════════════════
2438+
2439+
dag = DAGCircuit()
2440+
qreg = QuantumRegister(5)
2441+
creg = ClassicalRegister(5)
2442+
dag.add_qreg(qreg)
2443+
dag.add_creg(creg)
2444+
dag.apply_operation_back(CXGate(), qreg[[1, 2]], [])
2445+
dag.apply_operation_back(CXGate(), qreg[[0, 3]], [])
2446+
dag.apply_operation_back(CZGate(), qreg[[1, 4]], [])
2447+
dag.apply_operation_back(CZGate(), qreg[[2, 4]], [])
2448+
dag.apply_operation_back(CXGate(), qreg[[3, 4]], [])
2449+
2450+
# Raise error due to invalid index
2451+
self.assertRaises(DAGCircuitError, dag.quantum_causal_cone, Qubit())
2452+
2453+
def test_causal_cone_no_neighbor(self):
2454+
"""Test causal cone with no neighbor"""
2455+
2456+
# q_0: ───────────
2457+
2458+
# q_1: ──■───■────
2459+
# ┌─┴─┐ │
2460+
# q_2: ┤ X ├─┼──■─
2461+
# ├───┤ │ │
2462+
# q_3: ┤ X ├─┼──┼─
2463+
# └───┘ │ │
2464+
# q_4: ──────■──■─
2465+
2466+
# c: 5/═══════════
2467+
2468+
dag = DAGCircuit()
2469+
qreg = QuantumRegister(5)
2470+
creg = ClassicalRegister(5)
2471+
dag.add_qreg(qreg)
2472+
dag.add_creg(creg)
2473+
dag.apply_operation_back(CXGate(), qreg[[1, 2]], [])
2474+
dag.apply_operation_back(CZGate(), qreg[[1, 4]], [])
2475+
dag.apply_operation_back(CZGate(), qreg[[2, 4]], [])
2476+
dag.apply_operation_back(XGate(), qreg[[3]], [])
2477+
2478+
# Get causal cone of Qubit at index 3.
2479+
result = dag.quantum_causal_cone(qreg[3])
2480+
# Expect only a set with Qubit at index 3
2481+
expected = set(qreg[[3]])
2482+
self.assertEqual(result, expected)
2483+
2484+
def test_causal_cone_empty_circuit(self):
2485+
"""Test causal cone for circuit with no operations"""
2486+
dag = DAGCircuit()
2487+
qreg = QuantumRegister(5)
2488+
creg = ClassicalRegister(5)
2489+
dag.add_qreg(qreg)
2490+
dag.add_creg(creg)
2491+
2492+
# Get causal cone of qubit at index 4
2493+
result = dag.quantum_causal_cone(qreg[4])
2494+
# Expect only a set with Qubit at index 4
2495+
expected = set(qreg[[4]])
2496+
2497+
self.assertEqual(result, expected)
2498+
2499+
def test_causal_cone_barriers(self):
2500+
"""Test causal cone for circuit with barriers"""
2501+
2502+
# ┌───┐ ░ ░
2503+
# q1_0: ┤ X ├──■───░───────────────░───────────
2504+
# └───┘┌─┴─┐ ░ ░
2505+
# q1_1: ─────┤ X ├─░───────■───■───░───────────
2506+
# └───┘ ░ ┌───┐ │ │ ░ ┌───┐
2507+
# q1_2: ───────────░─┤ H ├─■───┼───░─┤ Y ├─────
2508+
# ░ └───┘ ┌─┴─┐ ░ └─┬─┘┌───┐
2509+
# q1_3: ───────────░─────────┤ Y ├─░───■──┤ X ├
2510+
# ░ └───┘ ░ └─┬─┘
2511+
# q1_4: ───────────░───────────────░────────■──
2512+
# ░ ░
2513+
2514+
# Build the circuit:
2515+
qreg = QuantumRegister(5)
2516+
qc = QuantumCircuit(qreg)
2517+
qc.x(0)
2518+
qc.cx(0, 1)
2519+
qc.barrier()
2520+
2521+
qc.h(2)
2522+
qc.cz(2, 1)
2523+
qc.cy(1, 3)
2524+
qc.barrier()
2525+
2526+
qc.cy(3, 2)
2527+
qc.cx(4, 3)
2528+
2529+
# Transform into a dag:
2530+
dag = circuit_to_dag(qc)
2531+
2532+
# Compute result:
2533+
result = dag.quantum_causal_cone(qreg[1])
2534+
# Expected:
2535+
expected = {qreg[0], qreg[1], qreg[2], qreg[3]}
2536+
2537+
self.assertEqual(result, expected)
2538+
2539+
23882540
if __name__ == "__main__":
23892541
unittest.main()

0 commit comments

Comments
 (0)