Skip to content

Commit

Permalink
Add fast-path construction to DAGCircuit methods (#10753)
Browse files Browse the repository at this point in the history
* Add fast-path construction to `DAGCircuit` methods

This optimises the constructor functions
`DAGCircuit.apply_operation_back`, `apply_operation_front` and `compose`
by giving callers a way to skip the validity checking when passing
known-good values.  In most cases when we're operating on the DAG, we
are certain that we're already upholding the invariants of the data
structure by nature of having a `copy_empty_like` or similar, and the
user should not pay a runtime price for what we should catch during
testing.

* Check for possibility of bits before iterating

---------

Co-authored-by: John Lapeyre <jlapeyre@users.noreply.github.com>
  • Loading branch information
jakelishman and jlapeyre authored Sep 7, 2023
1 parent c5c6b11 commit 3059193
Show file tree
Hide file tree
Showing 24 changed files with 137 additions and 90 deletions.
14 changes: 7 additions & 7 deletions qiskit/converters/ast_to_dag.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,11 +236,11 @@ def _process_cnot(self, node):
cx_gate = std.CXGate()
cx_gate.condition = self.condition
if len(id0) > 1 and len(id1) > 1:
self.dag.apply_operation_back(cx_gate, [id0[idx], id1[idx]], [])
self.dag.apply_operation_back(cx_gate, [id0[idx], id1[idx]], [], check=False)
elif len(id0) > 1:
self.dag.apply_operation_back(cx_gate, [id0[idx], id1[0]], [])
self.dag.apply_operation_back(cx_gate, [id0[idx], id1[0]], [], check=False)
else:
self.dag.apply_operation_back(cx_gate, [id0[0], id1[idx]], [])
self.dag.apply_operation_back(cx_gate, [id0[0], id1[idx]], [], check=False)

def _process_measure(self, node):
"""Process a measurement node."""
Expand All @@ -253,7 +253,7 @@ def _process_measure(self, node):
for idx, idy in zip(id0, id1):
meas_gate = Measure()
meas_gate.condition = self.condition
self.dag.apply_operation_back(meas_gate, [idx], [idy])
self.dag.apply_operation_back(meas_gate, [idx], [idy], check=False)

def _process_if(self, node):
"""Process an if node."""
Expand Down Expand Up @@ -335,14 +335,14 @@ def _process_node(self, node):
for qubit in ids:
for j, _ in enumerate(qubit):
qubits.append(qubit[j])
self.dag.apply_operation_back(Barrier(len(qubits)), qubits, [])
self.dag.apply_operation_back(Barrier(len(qubits)), qubits, [], check=False)

elif node.type == "reset":
id0 = self._process_bit_id(node.children[0])
for i, _ in enumerate(id0):
reset = Reset()
reset.condition = self.condition
self.dag.apply_operation_back(reset, [id0[i]], [])
self.dag.apply_operation_back(reset, [id0[i]], [], check=False)

elif node.type == "if":
self._process_if(node)
Expand Down Expand Up @@ -399,7 +399,7 @@ def _create_dag_op(self, name, params, qargs):
"""
op = self._create_op(name, params)
op.condition = self.condition
self.dag.apply_operation_back(op, qargs, [])
self.dag.apply_operation_back(op, qargs, [], check=False)

def _create_op(self, name, params):
if name in self.standard_extension:
Expand Down
2 changes: 1 addition & 1 deletion qiskit/converters/circuit_to_dag.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def circuit_to_dag(circuit, copy_operations=True, *, qubit_order=None, clbit_ord
op = instruction.operation
if copy_operations:
op = copy.deepcopy(op)
dagcircuit.apply_operation_back(op, instruction.qubits, instruction.clbits)
dagcircuit.apply_operation_back(op, instruction.qubits, instruction.clbits, check=False)

dagcircuit.duration = circuit.duration
dagcircuit.unit = circuit.unit
Expand Down
112 changes: 69 additions & 43 deletions qiskit/dagcircuit/dagcircuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
"""
from collections import OrderedDict, defaultdict, deque, namedtuple
import copy
import itertools
import math
from typing import Dict, Generator, Any, List

Expand Down Expand Up @@ -569,6 +568,7 @@ def _bits_in_operation(operation):
Returns:
Iterable[Clbit]: the :class:`.Clbit`\\ s involved.
"""
# If updating this, also update the fast-path checker `DAGCirucit._operation_may_have_bits`.
if (condition := getattr(operation, "condition", None)) is not None:
yield from condition_resources(condition).clbits
if isinstance(operation, SwitchCaseOp):
Expand All @@ -580,6 +580,22 @@ def _bits_in_operation(operation):
else:
yield from node_resources(target).clbits

@staticmethod
def _operation_may_have_bits(operation) -> bool:
"""Return whether a given :class:`.Operation` may contain any :class:`.Clbit` instances
in itself (e.g. a control-flow operation).
Args:
operation (qiskit.circuit.Operation): the operation to check.
"""
# This is separate to `_bits_in_operation` because most of the time there won't be any bits,
# so we want a fast path to be able to skip creating and testing a generator for emptiness.
#
# If updating this, also update `DAGCirucit._bits_in_operation`.
return getattr(operation, "condition", None) is not None or isinstance(
operation, SwitchCaseOp
)

def _increment_op(self, op):
if op.name in self._op_names:
self._op_names[op.name] += 1
Expand All @@ -592,23 +608,6 @@ def _decrement_op(self, op):
else:
self._op_names[op.name] -= 1

def _add_op_node(self, op, qargs, cargs):
"""Add a new operation node to the graph and assign properties.
Args:
op (qiskit.circuit.Operation): the operation associated with the DAG node
qargs (list[Qubit]): list of quantum wires to attach to.
cargs (list[Clbit]): list of classical wires to attach to.
Returns:
int: The integer node index for the new op node on the DAG
"""
# Add a new operation node to the graph
new_node = DAGOpNode(op=op, qargs=qargs, cargs=cargs, dag=self)
node_index = self._multi_graph.add_node(new_node)
new_node._node_id = node_index
self._increment_op(op)
return node_index

@deprecate_func(
additional_msg="Instead, use :meth:`~copy_empty_like()`, which acts identically.",
since="0.20.0",
Expand Down Expand Up @@ -647,13 +646,18 @@ def copy_empty_like(self):

return target_dag

def apply_operation_back(self, op, qargs=(), cargs=()):
def apply_operation_back(self, op, qargs=(), cargs=(), *, check=True):
"""Apply an operation to the output of the circuit.
Args:
op (qiskit.circuit.Operation): the operation associated with the DAG node
qargs (tuple[Qubit]): qubits that op will be applied to
cargs (tuple[Clbit]): cbits that op will be applied to
check (bool): If ``True`` (default), this function will enforce that the
:class:`.DAGCircuit` data-structure invariants are maintained (all ``qargs`` are
:class:`.Qubit`\\ s, all are in the DAG, etc). If ``False``, the caller *must*
uphold these invariants itself, but the cost of several checks will be skipped.
This is most useful when building a new DAG from a source of known-good nodes.
Returns:
DAGOpNode: the node for the op that was added to the dag
Expand All @@ -664,52 +668,74 @@ def apply_operation_back(self, op, qargs=(), cargs=()):
qargs = tuple(qargs) if qargs is not None else ()
cargs = tuple(cargs) if cargs is not None else ()

all_cbits = set(self._bits_in_operation(op)).union(cargs)
if self._operation_may_have_bits(op):
# This is the slow path; most of the time, this won't happen.
all_cbits = set(self._bits_in_operation(op)).union(cargs)
else:
all_cbits = cargs

self._check_condition(op.name, getattr(op, "condition", None))
self._check_bits(qargs, self.output_map)
self._check_bits(all_cbits, self.output_map)
if check:
self._check_condition(op.name, getattr(op, "condition", None))
self._check_bits(qargs, self.output_map)
self._check_bits(all_cbits, self.output_map)

node_index = self._add_op_node(op, qargs, cargs)
node = DAGOpNode(op=op, qargs=qargs, cargs=cargs, dag=self)
node._node_id = self._multi_graph.add_node(node)
self._increment_op(op)

# Add new in-edges from predecessors of the output nodes to the
# operation node while deleting the old in-edges of the output nodes
# and adding new edges from the operation node to each output node

al = [qargs, all_cbits]
self._multi_graph.insert_node_on_in_edges_multiple(
node_index, [self.output_map[q]._node_id for q in itertools.chain(*al)]
node._node_id,
[self.output_map[bit]._node_id for bits in (qargs, all_cbits) for bit in bits],
)
return self._multi_graph[node_index]
return node

def apply_operation_front(self, op, qargs=(), cargs=()):
def apply_operation_front(self, op, qargs=(), cargs=(), *, check=True):
"""Apply an operation to the input of the circuit.
Args:
op (qiskit.circuit.Operation): the operation associated with the DAG node
qargs (tuple[Qubit]): qubits that op will be applied to
cargs (tuple[Clbit]): cbits that op will be applied to
check (bool): If ``True`` (default), this function will enforce that the
:class:`.DAGCircuit` data-structure invariants are maintained (all ``qargs`` are
:class:`.Qubit`\\ s, all are in the DAG, etc). If ``False``, the caller *must*
uphold these invariants itself, but the cost of several checks will be skipped.
This is most useful when building a new DAG from a source of known-good nodes.
Returns:
DAGOpNode: the node for the op that was added to the dag
Raises:
DAGCircuitError: if initial nodes connected to multiple out edges
"""
all_cbits = set(self._bits_in_operation(op)).union(cargs)
qargs = tuple(qargs) if qargs is not None else ()
cargs = tuple(cargs) if cargs is not None else ()

if self._operation_may_have_bits(op):
# This is the slow path; most of the time, this won't happen.
all_cbits = set(self._bits_in_operation(op)).union(cargs)
else:
all_cbits = cargs

self._check_condition(op.name, getattr(op, "condition", None))
self._check_bits(qargs, self.input_map)
self._check_bits(all_cbits, self.input_map)
node_index = self._add_op_node(op, qargs, cargs)
if check:
self._check_condition(op.name, getattr(op, "condition", None))
self._check_bits(qargs, self.input_map)
self._check_bits(all_cbits, self.input_map)

node = DAGOpNode(op=op, qargs=qargs, cargs=cargs, dag=self)
node._node_id = self._multi_graph.add_node(node)
self._increment_op(op)

# Add new out-edges to successors of the input nodes from the
# operation node while deleting the old out-edges of the input nodes
# and adding new edges to the operation node from each input node
al = [qargs, all_cbits]
self._multi_graph.insert_node_on_out_edges_multiple(
node_index, [self.input_map[q]._node_id for q in itertools.chain(*al)]
node._node_id,
[self.input_map[bit]._node_id for bits in (qargs, all_cbits) for bit in bits],
)
return self._multi_graph[node_index]
return node

def compose(self, other, qubits=None, clbits=None, front=False, inplace=True):
"""Compose the ``other`` circuit onto the output of this circuit.
Expand Down Expand Up @@ -819,7 +845,7 @@ def _reject_new_register(reg):
op.condition = variable_mapper.map_condition(condition, allow_reorder=True)
elif isinstance(op, SwitchCaseOp):
op.target = variable_mapper.map_target(op.target)
dag.apply_operation_back(op, m_qargs, m_cargs)
dag.apply_operation_back(op, m_qargs, m_cargs, check=False)
else:
raise DAGCircuitError("bad node type %s" % type(nd))

Expand Down Expand Up @@ -1191,7 +1217,7 @@ def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condit
node_wire_order = list(node.qargs) + list(node.cargs)
# If we're not propagating it, the number of wires in the input DAG should include the
# condition as well.
if not propagate_condition:
if not propagate_condition and self._operation_may_have_bits(node.op):
node_wire_order += [
bit for bit in self._bits_in_operation(node.op) if bit not in node_cargs
]
Expand Down Expand Up @@ -1260,7 +1286,7 @@ def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condit
)
new_op = copy.copy(in_node.op)
new_op.condition = new_condition
in_dag.apply_operation_back(new_op, in_node.qargs, in_node.cargs)
in_dag.apply_operation_back(new_op, in_node.qargs, in_node.cargs, check=False)
else:
in_dag = input_dag

Expand Down Expand Up @@ -1483,7 +1509,7 @@ def _key(x):
subgraph_is_classical = False
if not isinstance(node, DAGOpNode):
continue
new_dag.apply_operation_back(node.op, node.qargs, node.cargs)
new_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False)

# Ignore DAGs created for empty clbits
if not subgraph_is_classical:
Expand Down Expand Up @@ -1801,7 +1827,7 @@ def layers(self):

for node in op_nodes:
# this creates new DAGOpNodes in the new_layer
new_layer.apply_operation_back(node.op, node.qargs, node.cargs)
new_layer.apply_operation_back(node.op, node.qargs, node.cargs, check=False)

# The quantum registers that have an operation in this layer.
support_list = [
Expand Down Expand Up @@ -1829,7 +1855,7 @@ def serial_layers(self):
cargs = copy.copy(next_node.cargs)

# Add node to new_layer
new_layer.apply_operation_back(op, qargs, cargs)
new_layer.apply_operation_back(op, qargs, cargs, check=False)
# Add operation to partition
if not getattr(next_node.op, "_directive", False):
support_list.append(list(qargs))
Expand Down
2 changes: 1 addition & 1 deletion qiskit/quantum_info/synthesis/one_qubit_decompose.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ def build_circuit(self, gates, global_phase):
else:
gate = gate_entry

dag.apply_operation_back(gate, [qr[0]])
dag.apply_operation_back(gate, (qr[0],), check=False)
return dag
else:
circuit = QuantumCircuit(qr, global_phase=global_phase)
Expand Down
6 changes: 3 additions & 3 deletions qiskit/synthesis/discrete_basis/gate_sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,18 +114,18 @@ def to_dag(self):
"""
from qiskit.dagcircuit import DAGCircuit

qreg = [Qubit()]
qreg = (Qubit(),)
dag = DAGCircuit()
dag.add_qubits(qreg)

if len(self.gates) == 0 and not np.allclose(self.product, np.identity(3)):
su2 = _convert_so3_to_su2(self.product)
dag.apply_operation_back(UnitaryGate(su2), qreg)
dag.apply_operation_back(UnitaryGate(su2), qreg, check=False)
return dag

dag.global_phase = self.global_phase
for gate in self.gates:
dag.apply_operation_back(gate, qreg)
dag.apply_operation_back(gate, qreg, check=False)

return dag

Expand Down
2 changes: 1 addition & 1 deletion qiskit/transpiler/passes/basis/basis_translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ def _compose_transforms(basis_transforms, source_basis, source_dag):
dag = DAGCircuit()
qr = QuantumRegister(gate_num_qubits)
dag.add_qreg(qr)
dag.apply_operation_back(placeholder_gate, qr[:], [])
dag.apply_operation_back(placeholder_gate, qr, (), check=False)
mapped_instrs[gate_name, gate_num_qubits] = placeholder_params, dag

for gate_name, gate_num_qubits, equiv_params, equiv in basis_transforms:
Expand Down
2 changes: 1 addition & 1 deletion qiskit/transpiler/passes/basis/translate_parameterized.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,6 @@ def _instruction_to_dag(op: Instruction) -> DAGCircuit:
dag = DAGCircuit()
dag.add_qubits([Qubit() for _ in range(op.num_qubits)])
dag.add_qubits([Clbit() for _ in range(op.num_clbits)])
dag.apply_operation_back(op, dag.qubits, dag.clbits)
dag.apply_operation_back(op, dag.qubits, dag.clbits, check=False)

return dag
4 changes: 2 additions & 2 deletions qiskit/transpiler/passes/layout/apply_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def run(self, dag):
virtual_phsyical_map = layout.get_virtual_bits()
for node in dag.topological_op_nodes():
qargs = [q[virtual_phsyical_map[qarg]] for qarg in node.qargs]
new_dag.apply_operation_back(node.op, qargs, node.cargs)
new_dag.apply_operation_back(node.op, qargs, node.cargs, check=False)
else:
# First build a new layout object going from:
# old virtual -> old phsyical -> new virtual -> new physical
Expand All @@ -94,7 +94,7 @@ def run(self, dag):
# Apply new layout to the circuit
for node in dag.topological_op_nodes():
qargs = [q[new_virtual_to_physical[qarg]] for qarg in node.qargs]
new_dag.apply_operation_back(node.op, qargs, node.cargs)
new_dag.apply_operation_back(node.op, qargs, node.cargs, check=False)
self.property_set["layout"] = full_layout
if (final_layout := self.property_set["final_layout"]) is not None:
final_layout_mapping = {
Expand Down
6 changes: 4 additions & 2 deletions qiskit/transpiler/passes/layout/disjoint_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ def split_barriers(dag: DAGCircuit):
split_dag.add_qubits([Qubit() for _ in range(num_qubits)])
for i in range(num_qubits):
split_dag.apply_operation_back(
Barrier(1, label=barrier_uuid), qargs=[split_dag.qubits[i]]
Barrier(1, label=barrier_uuid),
qargs=(split_dag.qubits[i],),
check=False,
)
dag.substitute_node_with_dag(node, split_dag)

Expand Down Expand Up @@ -191,7 +193,7 @@ def separate_dag(dag: DAGCircuit) -> List[DAGCircuit]:
new_dag.global_phase = 0
for node in dag.topological_op_nodes():
if dag_qubits.issuperset(node.qargs):
new_dag.apply_operation_back(node.op, node.qargs, node.cargs)
new_dag.apply_operation_back(node.op, node.qargs, node.cargs, check=False)
idle_clbits = []
for bit, node in new_dag.input_map.items():
succ_node = next(new_dag.successors(node))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,13 @@ def _resynthesize_run(self, matrix, qubit=None):
return best_synth_circuit

def _gate_sequence_to_dag(self, best_synth_circuit):
qubits = [Qubit()]
qubits = (Qubit(),)
out_dag = DAGCircuit()
out_dag.add_qubits(qubits)
out_dag.global_phase = best_synth_circuit.global_phase

for gate_name, angles in best_synth_circuit:
out_dag.apply_operation_back(NAME_MAP[gate_name](*angles), qubits)
out_dag.apply_operation_back(NAME_MAP[gate_name](*angles), qubits, check=False)
return out_dag

def _substitution_checks(self, dag, old_run, new_circ, basis, qubit):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def run(self, dag):
old_measure_qarg = successor.qargs[0]
new_measure_qarg = swap_qargs[swap_qargs.index(old_measure_qarg) - 1]
measure_layer.apply_operation_back(
Measure(), [new_measure_qarg], [successor.cargs[0]]
Measure(), (new_measure_qarg,), (successor.cargs[0],), check=False
)
dag.compose(measure_layer)
dag.remove_op_node(swap)
Expand Down
Loading

0 comments on commit 3059193

Please sign in to comment.