From ac942ee340bf00f3c70e3e7c456d3ee11d2c27f6 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 4 Jul 2023 00:55:25 +0100 Subject: [PATCH] Add `Expr` support to `DAGCircuit.compose` The remapping facilities are (except for some esoteric behaviour for mapping registers to others in a different order) the same as for `QuantumCircuit`, so it makes sense to unify the handling. As part of switching the old `DAGCircuit` classical-resource-mapping methods over to the new general-purpose forms, this does most of the necessary work to upgrade `substitute_node_with_dag` as well. --- qiskit/circuit/_classical_resource_map.py | 148 ++++++++++++++++++++++ qiskit/circuit/quantumcircuit.py | 81 +----------- qiskit/dagcircuit/dagcircuit.py | 140 ++++---------------- test/python/dagcircuit/test_compose.py | 119 ++++++++++++++++- 4 files changed, 297 insertions(+), 191 deletions(-) create mode 100644 qiskit/circuit/_classical_resource_map.py diff --git a/qiskit/circuit/_classical_resource_map.py b/qiskit/circuit/_classical_resource_map.py new file mode 100644 index 000000000000..122fa5fb6e43 --- /dev/null +++ b/qiskit/circuit/_classical_resource_map.py @@ -0,0 +1,148 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Shared helper utility for mapping classical resources from one circuit or DAG to another.""" + +from __future__ import annotations + +import typing + +from .bit import Bit +from .classical import expr +from .classicalregister import ClassicalRegister, Clbit + + +class VariableMapper(expr.ExprVisitor[expr.Expr]): + """Stateful helper class that manages the mapping of variables in conditions and expressions. + + This is designed to be used by both :class:`.QuantumCircuit` and :class:`.DAGCircuit` when + managing operations that need to map classical resources from one circuit to another. + + The general usage is to initialise this at the start of a many-block mapping operation, then + call its :meth:`map_condition`, :meth:`map_target` or :meth:`map_expr` methods as appropriate, + which will return the new object that should be used. + + If an ``add_register`` callable is given to the initialiser, the mapper will use it to attempt + to add new aliasing registers to the outer circuit object, if there is not already a suitable + register for the mapping available in the circuit. If this parameter is not given, the mapping + function will raise an exception of type ``exc_type`` instead. + """ + + __slots__ = ("target_cregs", "register_map", "bit_map", "add_register", "exc_type") + + def __init__( + self, + target_cregs: typing.Iterable[ClassicalRegister], + bit_map: typing.Mapping[Bit, Bit], + add_register: typing.Callable[[ClassicalRegister], None] | None = None, + exc_type: typing.Type[Exception] = ValueError, + ): + self.target_cregs = tuple(target_cregs) + self.register_map = {} + self.bit_map = bit_map + self.add_register = add_register + self.exc_type = exc_type + + def _map_register(self, theirs: ClassicalRegister) -> ClassicalRegister: + """Map the target's registers to suitable equivalents in the destination, adding an + extra one if there's no exact match.""" + if (mapped_theirs := self.register_map.get(theirs.name)) is not None: + return mapped_theirs + mapped_bits = [self.bit_map[bit] for bit in theirs] + for ours in self.target_cregs: + if mapped_bits == list(ours): + mapped_theirs = ours + break + else: + if self.add_register is None: + raise self.exc_type( + f"Register '{theirs.name}' has no counterpart in the destination." + ) + mapped_theirs = ClassicalRegister(bits=mapped_bits) + self.add_register(mapped_theirs) + self.register_map[theirs.name] = mapped_theirs + return mapped_theirs + + def map_condition(self, condition, /, *, allow_reorder=False): + """Map the given ``condition`` so that it only references variables in the destination + circuit (as given to this class on initialisation). + + If ``allow_reorder`` is ``True``, then when a legacy condition (the two-tuple form) is made + on a register that has a counterpart in the destination with all the same (mapped) bits but + in a different order, then that register will be used and the value suitably modified to + make the equality condition work. This is maintaining legacy (tested) behaviour of + :meth:`.DAGCircuit.compose`; nowhere else does this, and in general this would require *far* + more complex classical rewriting than Terra needs to worry about in the full expression era. + """ + if condition is None: + return None + if isinstance(condition, expr.Expr): + return self.map_expr(condition) + target, value = condition + if isinstance(target, Clbit): + return (self.bit_map[target], value) + if not allow_reorder: + return (self._map_register(target), value) + # This is maintaining the legacy behaviour of `DAGCircuit.compose`. We don't attempt to + # speed-up this lookup with a cache, since that would just make the more standard cases more + # annoying to deal with. + mapped_bits_order = [self.bit_map[bit] for bit in target] + mapped_bits_set = set(mapped_bits_order) + for register in self.target_cregs: + if mapped_bits_set == set(register): + mapped_theirs = register + break + else: + if self.add_register is None: + raise self.exc_type( + f"Register '{target.name}' has no counterpart in the destination." + ) + mapped_theirs = ClassicalRegister(bits=mapped_bits_order) + self.add_register(mapped_theirs) + new_order = {bit: i for i, bit in enumerate(mapped_bits_order)} + value_bits = f"{value:0{len(target)}b}"[::-1] # Little-index-indexed binary bitstring. + mapped_value = int("".join(value_bits[new_order[bit]] for bit in mapped_theirs)[::-1], 2) + return (mapped_theirs, mapped_value) + + def map_target(self, target, /): + """Map the runtime variables in a ``target`` of a :class:`.SwitchCaseOp` to the new circuit, + as defined in the ``circuit`` argument of the initialiser of this class.""" + if isinstance(target, Clbit): + return self.bit_map[target] + if isinstance(target, ClassicalRegister): + return self._map_register(target) + return self.map_expr(target) + + def map_expr(self, node: expr.Expr, /) -> expr.Expr: + """Map the variables in an :class:`~.expr.Expr` node to the new circuit.""" + return node.accept(self) + + def visit_var(self, node, /): + if isinstance(node.var, Clbit): + return expr.Var(self.bit_map[node.var], node.type) + if isinstance(node.var, ClassicalRegister): + return expr.Var(self._map_register(node.var), node.type) + # Defensive against the expansion of the variable system; we don't want to silently do the + # wrong thing (which would be `return node` without mapping, right now). + raise RuntimeError(f"unhandled variable in 'compose': {node}") # pragma: no cover + + def visit_value(self, node, /): + return expr.Value(node.value, node.type) + + def visit_unary(self, node, /): + return expr.Unary(node.op, node.operand.accept(self), node.type) + + def visit_binary(self, node, /): + return expr.Binary(node.op, node.left.accept(self), node.right.accept(self), node.type) + + def visit_cast(self, node, /): + return expr.Cast(node.operand.accept(self), node.type, implicit=node.implicit) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index b6333071511a..aa458a318ab8 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -48,6 +48,7 @@ from qiskit.circuit.parameter import Parameter from qiskit.qasm.exceptions import QasmError from qiskit.circuit.exceptions import CircuitError +from . import _classical_resource_map from .classical import expr from .parameterexpression import ParameterExpression, ParameterValueType from .quantumregister import QuantumRegister, Qubit, AncillaRegister, AncillaQubit @@ -959,7 +960,9 @@ def compose( ) edge_map.update(zip(other.clbits, dest.cbit_argument_conversion(clbits))) - variable_mapper = _ComposeVariableMapper(dest, edge_map) + variable_mapper = _classical_resource_map.VariableMapper( + dest.cregs, edge_map, dest.add_register + ) mapped_instrs: list[CircuitInstruction] = [] for instr in other.data: n_qargs: list[Qubit] = [edge_map[qarg] for qarg in instr.qubits] @@ -5196,79 +5199,3 @@ def _bit_argument_conversion_scalar(specifier, bit_sequence, bit_set, type_): else f"Invalid bit index: '{specifier}' of type '{type(specifier)}'" ) raise CircuitError(message) - - -class _ComposeVariableMapper(expr.ExprVisitor[expr.Expr]): - """Stateful helper class that manages the mapping of variables in conditions and expressions to - items in the destination ``circuit``. - - This mutates ``circuit`` by adding registers as required.""" - - __slots__ = ("circuit", "register_map", "bit_map") - - def __init__(self, circuit, bit_map): - self.circuit = circuit - self.register_map = {} - self.bit_map = bit_map - - def _map_register(self, theirs): - """Map the target's registers to suitable equivalents in the destination, adding an - extra one if there's no exact match.""" - if (mapped_theirs := self.register_map.get(theirs.name)) is not None: - return mapped_theirs - mapped_bits = [self.bit_map[bit] for bit in theirs] - for ours in self.circuit.cregs: - if mapped_bits == list(ours): - mapped_theirs = ours - break - else: - mapped_theirs = ClassicalRegister(bits=mapped_bits) - self.circuit.add_register(mapped_theirs) - self.register_map[theirs.name] = mapped_theirs - return mapped_theirs - - def map_condition(self, condition, /): - """Map the given ``condition`` so that it only references variables in the destination - circuit (as given to this class on initialisation).""" - if condition is None: - return None - if isinstance(condition, expr.Expr): - return self.map_expr(condition) - target, value = condition - if isinstance(target, Clbit): - return (self.bit_map[target], value) - return (self._map_register(target), value) - - def map_target(self, target, /): - """Map the runtime variables in a ``target`` of a :class:`.SwitchCaseOp` to the new circuit, - as defined in the ``circuit`` argument of the initialiser of this class.""" - if isinstance(target, Clbit): - return self.bit_map[target] - if isinstance(target, ClassicalRegister): - return self._map_register(target) - return self.map_expr(target) - - def map_expr(self, node: expr.Expr, /) -> expr.Expr: - """Map the variables in an :class:`~.expr.Expr` node to the new circuit.""" - return node.accept(self) - - def visit_var(self, node, /): - if isinstance(node.var, Clbit): - return expr.Var(self.bit_map[node.var], node.type) - if isinstance(node.var, ClassicalRegister): - return expr.Var(self._map_register(node.var), node.type) - # Defensive against the expansion of the variable system; we don't want to silently do the - # wrong thing (which would be `return node` without mapping, right now). - raise CircuitError(f"unhandled variable in 'compose': {node}") # pragma: no cover - - def visit_value(self, node, /): - return expr.Value(node.value, node.type) - - def visit_unary(self, node, /): - return expr.Unary(node.op, node.operand.accept(self), node.type) - - def visit_binary(self, node, /): - return expr.Binary(node.op, node.left.accept(self), node.right.accept(self), node.type) - - def visit_cast(self, node, /): - return expr.Cast(node.operand.accept(self), node.type, implicit=node.implicit) diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 1eba7e81f58d..ea1848a5b807 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -29,7 +29,14 @@ import numpy as np import rustworkx as rx -from qiskit.circuit import ControlFlowOp, ForLoopOp, IfElseOp, WhileLoopOp, SwitchCaseOp +from qiskit.circuit import ( + ControlFlowOp, + ForLoopOp, + IfElseOp, + WhileLoopOp, + SwitchCaseOp, + _classical_resource_map, +) from qiskit.circuit.controlflow import condition_resources, node_resources from qiskit.circuit.quantumregister import QuantumRegister, Qubit from qiskit.circuit.classicalregister import ClassicalRegister, Clbit @@ -627,101 +634,6 @@ def apply_operation_front(self, op, qargs=(), cargs=()): ) return self._multi_graph[node_index] - @staticmethod - def _map_condition(wire_map, condition, target_cregs): - """Use the wire_map dict to change the condition tuple's creg name. - - Args: - wire_map (dict): a map from source wires to destination wires - condition (tuple or None): (ClassicalRegister,int) - target_cregs (list[ClassicalRegister]): List of all cregs in the - target circuit onto which the condition might possibly be mapped. - Returns: - tuple(ClassicalRegister,int): new condition - Raises: - DAGCircuitError: if condition register not in wire_map, or if - wire_map maps condition onto more than one creg, or if the - specified condition is not present in a classical register. - """ - - if condition is None: - new_condition = None - else: - # if there is a condition, map the condition bits to the - # composed cregs based on the wire_map - is_reg = False - if isinstance(condition[0], Clbit): - cond_creg = [condition[0]] - else: - cond_creg = condition[0] - is_reg = True - cond_val = condition[1] - new_cond_val = 0 - new_creg = None - bits_in_condcreg = [bit for bit in wire_map if bit in cond_creg] - for bit in bits_in_condcreg: - if is_reg: - try: - candidate_creg = next( - creg for creg in target_cregs if wire_map[bit] in creg - ) - except StopIteration as ex: - raise DAGCircuitError( - "Did not find creg containing mapped clbit in conditional." - ) from ex - else: - # If cond is on a single Clbit then the candidate_creg is - # the target Clbit to which 'bit' is mapped to. - candidate_creg = wire_map[bit] - if new_creg is None: - new_creg = candidate_creg - elif new_creg != candidate_creg: - # Raise if wire_map maps condition creg on to more than one - # creg in target DAG. - raise DAGCircuitError( - "wire_map maps conditional register onto more than one creg." - ) - - if not is_reg: - # If the cond is on a single Clbit then the new_cond_val is the - # same as the cond_val since the new_creg is also a single Clbit. - new_cond_val = cond_val - elif 2 ** (cond_creg[:].index(bit)) & cond_val: - # If the conditional values of the Clbit 'bit' is 1 then the new_cond_val - # is updated such that the conditional value of the Clbit to which 'bit' - # is mapped to in new_creg is 1. - new_cond_val += 2 ** (new_creg[:].index(wire_map[bit])) - if new_creg is None: - raise DAGCircuitError("Condition registers not found in wire_map.") - new_condition = (new_creg, new_cond_val) - return new_condition - - def _map_classical_resource_with_import(self, resource, wire_map, creg_map): - """Map the classical ``resource`` (a bit or register) in its counterpart in ``self`` using - ``wire_map`` and ``creg_map`` as lookup caches. All single-bit conditions should have a - cache hit in the ``wire_map``, but registers may involve a full linear search the first time - they are encountered. ``creg_map`` is mutated by this function. ``wire_map`` is not; it is - an error if a wire is not in the map. - - This is different to the logic in ``_map_condition`` because it always succeeds; since the - mapping for all wires in the condition is assumed to exist, there can be no fragmented - registers. If there is no matching register (has the same bits in the same order) in - ``self``, a new register alias is added to represent the condition. This does not change - the bits available to ``self``, it just adds a new aliased grouping of them.""" - if isinstance(resource, Clbit): - return wire_map[resource] - if resource.name not in creg_map: - mapped_bits = [wire_map[bit] for bit in resource] - for our_creg in self.cregs.values(): - if mapped_bits == list(our_creg): - new_resource = our_creg - break - else: - new_resource = ClassicalRegister(bits=[wire_map[bit] for bit in resource]) - self.add_creg(new_resource) - creg_map[resource.name] = new_resource - return creg_map[resource.name] - def compose(self, other, qubits=None, clbits=None, front=False, inplace=True): """Compose the ``other`` circuit onto the output of this circuit. @@ -798,6 +710,9 @@ def compose(self, other, qubits=None, clbits=None, front=False, inplace=True): for gate, cals in other.calibrations.items(): dag._calibrations[gate].update(cals) + variable_mapper = _classical_resource_map.VariableMapper( + dag.cregs.values(), edge_map, exc_type=DAGCircuitError + ) for nd in other.topological_nodes(): if isinstance(nd, DAGInNode): # if in edge_map, get new name, else use existing name @@ -816,16 +731,13 @@ def compose(self, other, qubits=None, clbits=None, front=False, inplace=True): # ignore output nodes pass elif isinstance(nd, DAGOpNode): - condition = dag._map_condition( - edge_map, getattr(nd.op, "condition", None), dag.cregs.values() - ) - dag._check_condition(nd.op.name, condition) m_qargs = [edge_map.get(x, x) for x in nd.qargs] m_cargs = [edge_map.get(x, x) for x in nd.cargs] op = nd.op.copy() - if condition and not isinstance(op, Instruction): - raise DAGCircuitError("Cannot add a condition on a generic Operation.") - op.condition = condition + if (condition := getattr(op, "condition", None)) is not None: + 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) else: raise DAGCircuitError("bad node type %s" % type(nd)) @@ -1210,10 +1122,16 @@ def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condit ) reverse_wire_map = {b: a for a, b in wire_map.items()} - creg_map = {} op_condition = getattr(node.op, "condition", None) if propagate_condition and op_condition is not None: in_dag = input_dag.copy_empty_like() + # The remapping of `condition` below is still using the old code that assumes a 2-tuple. + # This is because this remapping code only makes sense in the case of non-control-flow + # operations being replaced. These can only have the 2-tuple conditions, and the + # ability to set a condition at an individual node level will be deprecated and removed + # in favour of the new-style conditional blocks. The extra logic in here to add + # additional wires into the map as necessary would hugely complicate matters if we tried + # to abstract it out into the `VariableMapper` used elsewhere. target, value = op_condition if isinstance(target, Clbit): new_target = reverse_wire_map.get(target, Clbit()) @@ -1227,7 +1145,6 @@ def substitute_node_with_dag(self, node, input_dag, wires=None, propagate_condit # Update to any new dummy bits we just created to the wire maps. wire_map[theirs], reverse_wire_map[ours] = ours, theirs new_target = ClassicalRegister(bits=mapped_bits) - creg_map[new_target.name] = target in_dag.add_creg(new_target) target_cargs = set(new_target) new_condition = (new_target, value) @@ -1315,6 +1232,9 @@ def edge_weight_map(wire): ) self._decrement_op(node.op) + variable_mapper = _classical_resource_map.VariableMapper( + self.cregs.values(), wire_map, self.add_creg + ) # Iterate over nodes of input_circuit and update wires in node objects migrated # from in_dag for old_node_index, new_node_index in node_map.items(): @@ -1322,19 +1242,13 @@ def edge_weight_map(wire): old_node = in_dag._multi_graph[old_node_index] if isinstance(old_node.op, SwitchCaseOp): m_op = SwitchCaseOp( - self._map_classical_resource_with_import( - old_node.op.target, wire_map, creg_map - ), + variable_mapper.map_target(old_node.op.target), old_node.op.cases_specifier(), label=old_node.op.label, ) elif getattr(old_node.op, "condition", None) is not None: - cond_target, cond_value = old_node.op.condition m_op = copy.copy(old_node.op) - m_op.condition = ( - self._map_classical_resource_with_import(cond_target, wire_map, creg_map), - cond_value, - ) + m_op.condition = variable_mapper.map_condition(m_op.condition) else: m_op = old_node.op m_qargs = [wire_map[x] for x in old_node.qargs] diff --git a/test/python/dagcircuit/test_compose.py b/test/python/dagcircuit/test_compose.py index 783c55ac11ad..0a1702d8cbf9 100644 --- a/test/python/dagcircuit/test_compose.py +++ b/test/python/dagcircuit/test_compose.py @@ -14,7 +14,17 @@ import unittest -from qiskit.circuit import QuantumRegister, ClassicalRegister, QuantumCircuit +from qiskit.circuit import ( + QuantumRegister, + ClassicalRegister, + QuantumCircuit, + IfElseOp, + WhileLoopOp, + SwitchCaseOp, + CASE_DEFAULT, +) +from qiskit.circuit.classical import expr, types +from qiskit.dagcircuit import DAGCircuit from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.test import QiskitTestCase from qiskit.pulse import Schedule @@ -423,6 +433,113 @@ def test_compose_condition_multiple_classical(self): self.assertEqual(dag_composed, dag_expected) + def test_compose_expr_condition(self): + """Test that compose correctly maps clbits and registers in expression conditions.""" + inner = QuantumCircuit(1) + inner.x(0) + qr_src = QuantumRegister(1) + a_src = ClassicalRegister(2, "a_src") + b_src = ClassicalRegister(2, "b_src") + source = DAGCircuit() + source.add_qreg(qr_src) + source.add_creg(a_src) + source.add_creg(b_src) + + test_1 = lambda: expr.lift(a_src[0]) + test_2 = lambda: expr.logic_not(b_src[1]) + test_3 = lambda: expr.cast(expr.bit_and(b_src, 2), types.Bool()) + node_1 = source.apply_operation_back(IfElseOp(test_1(), inner.copy(), None), qr_src, []) + node_2 = source.apply_operation_back( + IfElseOp(test_2(), inner.copy(), inner.copy()), qr_src, [] + ) + node_3 = source.apply_operation_back(WhileLoopOp(test_3(), inner.copy()), qr_src, []) + + qr_dest = QuantumRegister(1) + a_dest = ClassicalRegister(2, "a_dest") + b_dest = ClassicalRegister(2, "b_dest") + dest = DAGCircuit() + dest.add_qreg(qr_dest) + dest.add_creg(a_dest) + dest.add_creg(b_dest) + + dest.compose(source) + + # Check that the input conditions weren't mutated. + for in_condition, node in zip((test_1, test_2, test_3), (node_1, node_2, node_3)): + self.assertEqual(in_condition(), node.op.condition) + + expected = QuantumCircuit(qr_dest, a_dest, b_dest) + expected.if_test(expr.lift(a_dest[0]), inner.copy(), [0], []) + expected.if_else(expr.logic_not(b_dest[1]), inner.copy(), inner.copy(), [0], []) + expected.while_loop(expr.cast(expr.bit_and(b_dest, 2), types.Bool()), inner.copy(), [0], []) + self.assertEqual(dest, circuit_to_dag(expected)) + + def test_compose_expr_target(self): + """Test that compose correctly maps clbits and registers in expression targets.""" + inner1 = QuantumCircuit(1) + inner1.x(0) + inner2 = QuantumCircuit(1) + inner2.z(0) + + qr_src = QuantumRegister(1) + a_src = ClassicalRegister(2, "a_src") + b_src = ClassicalRegister(2, "b_src") + source = DAGCircuit() + source.add_qreg(qr_src) + source.add_creg(a_src) + source.add_creg(b_src) + + test_1 = lambda: expr.lift(a_src[0]) + test_2 = lambda: expr.logic_not(b_src[1]) + test_3 = lambda: expr.lift(b_src) + test_4 = lambda: expr.bit_and(b_src, 2) + node_1 = source.apply_operation_back( + SwitchCaseOp(test_1(), [(False, inner1.copy()), (True, inner2.copy())]), qr_src, [] + ) + node_2 = source.apply_operation_back( + SwitchCaseOp(test_2(), [(False, inner1.copy()), (True, inner2.copy())]), qr_src, [] + ) + node_3 = source.apply_operation_back( + SwitchCaseOp(test_3(), [(0, inner1.copy()), (CASE_DEFAULT, inner2.copy())]), qr_src, [] + ) + node_4 = source.apply_operation_back( + SwitchCaseOp(test_4(), [(0, inner1.copy()), (CASE_DEFAULT, inner2.copy())]), qr_src, [] + ) + + qr_dest = QuantumRegister(1) + a_dest = ClassicalRegister(2, "a_dest") + b_dest = ClassicalRegister(2, "b_dest") + dest = DAGCircuit() + dest.add_qreg(qr_dest) + dest.add_creg(a_dest) + dest.add_creg(b_dest) + dest.compose(source) + + # Check that the input expressions weren't mutated. + for in_target, node in zip( + (test_1, test_2, test_3, test_4), (node_1, node_2, node_3, node_4) + ): + self.assertEqual(in_target(), node.op.target) + + expected = QuantumCircuit(qr_dest, a_dest, b_dest) + expected.switch( + expr.lift(a_dest[0]), [(False, inner1.copy()), (True, inner2.copy())], [0], [] + ) + expected.switch( + expr.logic_not(b_dest[1]), [(False, inner1.copy()), (True, inner2.copy())], [0], [] + ) + expected.switch( + expr.lift(b_dest), [(0, inner1.copy()), (CASE_DEFAULT, inner2.copy())], [0], [] + ) + expected.switch( + expr.bit_and(b_dest, 2), + [(0, inner1.copy()), (CASE_DEFAULT, inner2.copy())], + [0], + [], + ) + + self.assertEqual(dest, circuit_to_dag(expected)) + def test_compose_calibrations(self): """Test that compose carries over the calibrations.""" dag_cal = QuantumCircuit(1)