diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index b5f1b444158c..23ca86f675d0 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -57,6 +57,7 @@ def transpile( instruction_durations: Optional[InstructionDurationsType] = None, dt: Optional[float] = None, approximation_degree: Optional[float] = None, + alignment: Optional[int] = None, seed_transpiler: Optional[int] = None, optimization_level: Optional[int] = None, pass_manager: Optional[PassManager] = None, @@ -147,6 +148,21 @@ def transpile( If ``None`` (default), ``backend.configuration().dt`` is used. approximation_degree (float): heuristic dial used for circuit approximation (1.0=no approximation, 0.0=maximal approximation) + alignment: An optional control hardware restriction on instruction time allocation. + The location of instructions is adjusted to be at quantized time that is + multiple of this value, or data point length of pulse gate instruction + should be multiple of this value though it can define the pulse envelope with dt step. + By providing this argument, such instruction alignment and gate length validation + are triggered. This information will be provided by the backend configuration. + If the backend doesn't have any restriction on the alignment, + then ``alignment`` is None and no adjustment will be performed. + + .. note:: + + Note that currently this instruction time optimization is performed only for + ``measure`` instructions. Here we assume a hardware whose + measurements should start at a time which is a multiple of the alignment value. + seed_transpiler: Sets random seed for the stochastic parts of the transpiler optimization_level: How much optimization to perform on the circuits. Higher levels generate more optimized circuits, @@ -263,6 +279,7 @@ def callback_func(**kwargs): optimization_level, callback, output_name, + alignment, ) _check_circuits_coupling_map(circuits, transpile_args, backend) @@ -443,6 +460,7 @@ def _parse_transpile_args( optimization_level, callback, output_name, + alignment, ) -> List[Dict]: """Resolve the various types of args allowed to the transpile() function through duck typing, overriding args, etc. Refer to the transpile() docstring for details on @@ -480,6 +498,7 @@ def _parse_transpile_args( callback = _parse_callback(callback, num_circuits) durations = _parse_instruction_durations(backend, instruction_durations, dt, circuits) scheduling_method = _parse_scheduling_method(scheduling_method, num_circuits) + alignment = _parse_alignment(backend, alignment, num_circuits) if scheduling_method and any(d is None for d in durations): raise TranspilerError( "Transpiling a circuit with a scheduling method" @@ -498,6 +517,7 @@ def _parse_transpile_args( scheduling_method, durations, approximation_degree, + alignment, seed_transpiler, optimization_level, output_name, @@ -517,13 +537,14 @@ def _parse_transpile_args( scheduling_method=args[7], instruction_durations=args[8], approximation_degree=args[9], - seed_transpiler=args[10], + alignment=args[10], + seed_transpiler=args[11], ), - "optimization_level": args[11], - "output_name": args[12], - "callback": args[13], - "backend_num_qubits": args[14], - "faulty_qubits_map": args[15], + "optimization_level": args[12], + "output_name": args[13], + "callback": args[14], + "backend_num_qubits": args[15], + "faulty_qubits_map": args[16], } list_transpile_args.append(transpile_args) @@ -833,3 +854,16 @@ def _parse_output_name(output_name, circuits): ) else: return [circuit.name for circuit in circuits] + + +def _parse_alignment(backend, alignment, num_circuits): + if backend is None and alignment is None: + alignment = 1 + else: + if alignment is None: + alignment = getattr(backend.configuration(), "alignment", 1) + + if not isinstance(alignment, int) or alignment == 0: + raise TranspilerError(f"Alignment should be nonzero integer value. Not {alignment}.") + + return [alignment] * num_circuits diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index c21e6cb418e7..a1f03b250043 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -85,6 +85,8 @@ ALAPSchedule ASAPSchedule RZXCalibrationBuilder + AlignMeasures + ValidatePulseGates Circuit Analysis ================ @@ -187,6 +189,8 @@ from .scheduling import ASAPSchedule from .scheduling import RZXCalibrationBuilder from .scheduling import TimeUnitConversion +from .scheduling import AlignMeasures +from .scheduling import ValidatePulseGates # additional utility passes from .utils import CheckMap diff --git a/qiskit/transpiler/passes/scheduling/__init__.py b/qiskit/transpiler/passes/scheduling/__init__.py index f91371eb2cc1..b5c479ba69ad 100644 --- a/qiskit/transpiler/passes/scheduling/__init__.py +++ b/qiskit/transpiler/passes/scheduling/__init__.py @@ -17,3 +17,4 @@ from .time_unit_conversion import TimeUnitConversion from .calibration_creators import CalibrationCreator, RZXCalibrationBuilder from .rzx_templates import rzx_templates +from .instruction_alignment import AlignMeasures, ValidatePulseGates diff --git a/qiskit/transpiler/passes/scheduling/instruction_alignment.py b/qiskit/transpiler/passes/scheduling/instruction_alignment.py new file mode 100644 index 000000000000..6b383a01858d --- /dev/null +++ b/qiskit/transpiler/passes/scheduling/instruction_alignment.py @@ -0,0 +1,166 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +"""Align measurement instructions.""" + +from collections import defaultdict +from typing import List + +from qiskit.circuit.delay import Delay +from qiskit.circuit.measure import Measure +from qiskit.dagcircuit import DAGCircuit +from qiskit.transpiler.basepasses import TransformationPass, AnalysisPass +from qiskit.transpiler.exceptions import TranspilerError + + +class AlignMeasures(TransformationPass): + """Measurement alignment. + + This is control electronics aware optimization pass. + """ + + def __init__(self, alignment: int = 1): + super().__init__() + self.alignment = alignment + + def run(self, dag: DAGCircuit): + """Run the measurement alignment pass on `dag`. + + Args: + dag (DAGCircuit): DAG to be checked. + + Returns: + DAGCircuit: DAG with consistent timing and op nodes annotated with duration. + + Raises: + TranspilerError: If circuit is not scheduled. + """ + time_unit = self.property_set["time_unit"] + + require_validation = True + + if all(delay_node.op.duration % self.alignment == 0 for delay_node in dag.op_nodes(Delay)): + # delay is the only instruction that can move other instructions + # to the position which is not multiple of alignment. + # if all delays are multiple of alignment then we can avoid validation. + require_validation = False + + if len(dag.op_nodes(Measure)) == 0: + # if no measurement is involved we don't need to run validation. + # since this pass assumes backend execution, this is really rare case. + require_validation = False + + if self.alignment == 1: + # we can place measure at arbitrary time of dt. + require_validation = False + + if not require_validation: + # return input as-is to avoid unnecessary scheduling. + # because following procedure regenerate new DAGCircuit, + # we should avoid continuing if not necessary from performance viewpoint. + return dag + + # if circuit is not yet scheduled, schedule with ALAP method + if dag.duration is None: + raise TranspilerError( + f"This circuit {dag.name} may involve a delay instruction violating the " + "pulse controller alignment. To adjust instructions to " + "right timing, you should call one of scheduling passes first. " + "This is usually done by calling transpiler with scheduling_method='alap'." + ) + + # the following lines are basically copied from ASAPSchedule pass + # + # * some validations for non-scheduled nodes are dropped, since we assume scheduled input + # * pad_with_delay is called only with non-delay node to avoid consecutive delay + new_dag = dag._copy_circuit_metadata() + + qubit_time_available = defaultdict(int) + qubit_stop_times = defaultdict(int) + + def pad_with_delays(qubits: List[int], until, unit) -> None: + """Pad idle time-slots in ``qubits`` with delays in ``unit`` until ``until``.""" + for q in qubits: + if qubit_stop_times[q] < until: + idle_duration = until - qubit_stop_times[q] + new_dag.apply_operation_back(Delay(idle_duration, unit), [q]) + + for node in dag.topological_op_nodes(): + start_time = max(qubit_time_available[q] for q in node.qargs) + + if isinstance(node.op, Measure): + if start_time % self.alignment != 0: + start_time = ((start_time // self.alignment) + 1) * self.alignment + + if not isinstance(node.op, Delay): + pad_with_delays(node.qargs, until=start_time, unit=time_unit) + new_dag.apply_operation_back(node.op, node.qargs, node.cargs) + + stop_time = start_time + node.op.duration + # update time table + for q in node.qargs: + qubit_time_available[q] = stop_time + qubit_stop_times[q] = stop_time + else: + stop_time = start_time + node.op.duration + for q in node.qargs: + qubit_time_available[q] = stop_time + + working_qubits = qubit_time_available.keys() + circuit_duration = max(qubit_time_available[q] for q in working_qubits) + pad_with_delays(new_dag.qubits, until=circuit_duration, unit=time_unit) + + new_dag.name = dag.name + new_dag.metadata = dag.metadata + + # set circuit duration and unit to indicate it is scheduled + new_dag.duration = circuit_duration + new_dag.unit = time_unit + + return new_dag + + +class ValidatePulseGates(AnalysisPass): + """Check custom gate length. + + This is control electronics aware validation pass. + """ + + def __init__(self, alignment: int = 1): + super().__init__() + self.alignment = alignment + + def run(self, dag: DAGCircuit): + """Run the measurement alignment pass on `dag`. + + Args: + dag (DAGCircuit): DAG to be checked. + + Returns: + DAGCircuit: DAG with consistent timing and op nodes annotated with duration. + + Raises: + TranspilerError: When pulse gate violate pulse controller alignment. + """ + if self.alignment == 1: + # we can define arbitrary length pulse with dt resolution + return + + for gate, insts in dag.calibrations.items(): + for qubit_param_pair, schedule in insts.items(): + if schedule.duration % self.alignment != 0: + raise TranspilerError( + f"Pulse gate duration is not multiple of {self.alignment}. " + "This pulse cannot be played on the specified backend. " + f"Please modify the duration of the custom gate schedule {schedule.name} " + f"which is associated with the gate {gate} of qubit {qubit_param_pair[0]}." + ) diff --git a/qiskit/transpiler/passmanager_config.py b/qiskit/transpiler/passmanager_config.py index e91ed5ed623d..fc298d57cd55 100644 --- a/qiskit/transpiler/passmanager_config.py +++ b/qiskit/transpiler/passmanager_config.py @@ -29,6 +29,7 @@ def __init__( backend_properties=None, approximation_degree=None, seed_transpiler=None, + alignment=None, ): """Initialize a PassManagerConfig object @@ -54,6 +55,7 @@ def __init__( (1.0=no approximation, 0.0=maximal approximation) seed_transpiler (int): Sets random seed for the stochastic parts of the transpiler. + alignment (int): Hardware instruction alignment restriction. """ self.initial_layout = initial_layout self.basis_gates = basis_gates @@ -66,3 +68,4 @@ def __init__( self.backend_properties = backend_properties self.approximation_degree = approximation_degree self.seed_transpiler = seed_transpiler + self.alignment = alignment diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index e34cb4cd92a2..01e80a9216b3 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -44,6 +44,8 @@ from qiskit.transpiler.passes import TimeUnitConversion from qiskit.transpiler.passes import ALAPSchedule from qiskit.transpiler.passes import ASAPSchedule +from qiskit.transpiler.passes import AlignMeasures +from qiskit.transpiler.passes import ValidatePulseGates from qiskit.transpiler.passes import Error from qiskit.transpiler import TranspilerError @@ -83,6 +85,7 @@ def level_0_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: seed_transpiler = pass_manager_config.seed_transpiler backend_properties = pass_manager_config.backend_properties approximation_degree = pass_manager_config.approximation_degree + alignment = pass_manager_config.alignment # 1. Choose an initial layout if not set by user (default: trivial layout) _given_layout = SetLayout(initial_layout) @@ -169,6 +172,9 @@ def _direction_condition(property_set): else: raise TranspilerError("Invalid scheduling method %s." % scheduling_method) + # 8. Call measure alignment. Should come after scheduling. + _alignments = [ValidatePulseGates(alignment=alignment), AlignMeasures(alignment=alignment)] + # Build pass manager pm0 = PassManager() if coupling_map or initial_layout: @@ -184,4 +190,6 @@ def _direction_condition(property_set): pm0.append(_direction, condition=_direction_condition) pm0.append(_unroll) pm0.append(_scheduling) + if alignment != 1: + pm0.append(_alignments) return pm0 diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index deab87f30467..468e49fe78ac 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -50,6 +50,8 @@ from qiskit.transpiler.passes import TimeUnitConversion from qiskit.transpiler.passes import ALAPSchedule from qiskit.transpiler.passes import ASAPSchedule +from qiskit.transpiler.passes import AlignMeasures +from qiskit.transpiler.passes import ValidatePulseGates from qiskit.transpiler.passes import Error from qiskit.transpiler import TranspilerError @@ -91,6 +93,7 @@ def level_1_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: seed_transpiler = pass_manager_config.seed_transpiler backend_properties = pass_manager_config.backend_properties approximation_degree = pass_manager_config.approximation_degree + alignment = pass_manager_config.alignment # 1. Use trivial layout if no layout given _given_layout = SetLayout(initial_layout) @@ -200,6 +203,9 @@ def _opt_control(property_set): else: raise TranspilerError("Invalid scheduling method %s." % scheduling_method) + # 11. Call measure alignment. Should come after scheduling. + _alignments = [ValidatePulseGates(alignment=alignment), AlignMeasures(alignment=alignment)] + # Build pass manager pm1 = PassManager() if coupling_map or initial_layout: @@ -217,5 +223,7 @@ def _opt_control(property_set): pm1.append(_reset) pm1.append(_depth_check + _opt + _unroll, do_while=_opt_control) pm1.append(_scheduling) + if alignment != 1: + pm1.append(_alignments) return pm1 diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index d26222b3cc61..42a7f103a033 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -52,6 +52,8 @@ from qiskit.transpiler.passes import TimeUnitConversion from qiskit.transpiler.passes import ALAPSchedule from qiskit.transpiler.passes import ASAPSchedule +from qiskit.transpiler.passes import AlignMeasures +from qiskit.transpiler.passes import ValidatePulseGates from qiskit.transpiler.passes import Error from qiskit.transpiler import TranspilerError @@ -95,6 +97,7 @@ def level_2_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: seed_transpiler = pass_manager_config.seed_transpiler backend_properties = pass_manager_config.backend_properties approximation_degree = pass_manager_config.approximation_degree + alignment = pass_manager_config.alignment # 1. Search for a perfect layout, or choose a dense layout, if no layout given _given_layout = SetLayout(initial_layout) @@ -237,6 +240,9 @@ def _opt_control(property_set): else: raise TranspilerError("Invalid scheduling method %s." % scheduling_method) + # 10. Call measure alignment. Should come after scheduling. + _alignments = [ValidatePulseGates(alignment=alignment), AlignMeasures(alignment=alignment)] + # Build pass manager pm2 = PassManager() if coupling_map or initial_layout: @@ -255,5 +261,6 @@ def _opt_control(property_set): pm2.append(_reset) pm2.append(_depth_check + _opt + _unroll, do_while=_opt_control) pm2.append(_scheduling) - + if alignment != 1: + pm2.append(_alignments) return pm2 diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index 257bc4babe88..d06896c90a46 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -55,6 +55,8 @@ from qiskit.transpiler.passes import TimeUnitConversion from qiskit.transpiler.passes import ALAPSchedule from qiskit.transpiler.passes import ASAPSchedule +from qiskit.transpiler.passes import AlignMeasures +from qiskit.transpiler.passes import ValidatePulseGates from qiskit.transpiler.passes import Error from qiskit.transpiler import TranspilerError @@ -98,6 +100,7 @@ def level_3_pass_manager(pass_manager_config: PassManagerConfig) -> PassManager: seed_transpiler = pass_manager_config.seed_transpiler backend_properties = pass_manager_config.backend_properties approximation_degree = pass_manager_config.approximation_degree + alignment = pass_manager_config.alignment # 1. Unroll to 1q or 2q gates _unroll3q = Unroll3qOrMore() @@ -245,6 +248,9 @@ def _opt_control(property_set): else: raise TranspilerError("Invalid scheduling method %s." % scheduling_method) + # 10. Call measure alignment. Should come after scheduling. + _alignments = [ValidatePulseGates(alignment=alignment), AlignMeasures(alignment=alignment)] + # Build pass manager pm3 = PassManager() pm3.append(_unroll3q) @@ -264,5 +270,7 @@ def _opt_control(property_set): pm3.append(_reset) pm3.append(_depth_check + _opt + _unroll, do_while=_opt_control) pm3.append(_scheduling) + if alignment != 1: + pm3.append(_alignments) return pm3 diff --git a/releasenotes/notes/add-alignment-management-passes-650b8172e1426a73.yaml b/releasenotes/notes/add-alignment-management-passes-650b8172e1426a73.yaml new file mode 100644 index 000000000000..bd7e18555a9c --- /dev/null +++ b/releasenotes/notes/add-alignment-management-passes-650b8172e1426a73.yaml @@ -0,0 +1,15 @@ +--- +features: + - | + Added new passes `AlignMeasures` and `ValidatePulseGates` that manage the alignment + restriction on time allocation of instructions. These passes are triggered when + finite alignment factor > 1 is specified to the transpiler argument. + The alignment may indicate the chunk size of the hardware waveform memory, + and all instruction should have data point length of multiple of it. + Thus these passes are hardware (pulse controller) aware optimization and validation routine. + This value may be provided by the backend configuration field, or you can + set arbitrary alignment value directly to the transpiler. + If backend doesn't provide alignment information, then this value defaults to 1 and + execution of these passes are just skipped. + Backends assigned to IBM Quantum Services will shortly provide this new information to + provide better optimization of circuits before execution. diff --git a/test/python/transpiler/test_instruction_alignments.py b/test/python/transpiler/test_instruction_alignments.py new file mode 100644 index 000000000000..fb49ef6429fc --- /dev/null +++ b/test/python/transpiler/test_instruction_alignments.py @@ -0,0 +1,314 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +"""Testing instruction alignment pass.""" + +from qiskit import QuantumCircuit, pulse +from qiskit.test import QiskitTestCase +from qiskit.transpiler import InstructionDurations +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.passes import ( + AlignMeasures, + ValidatePulseGates, + ALAPSchedule, + TimeUnitConversion, +) + + +class TestAlignMeasures(QiskitTestCase): + """A test for measurement alignment pass.""" + + def setUp(self): + super().setUp() + instruction_durations = InstructionDurations() + instruction_durations.update( + [ + ("rz", (0,), 0), + ("rz", (1,), 0), + ("x", (0,), 160), + ("x", (1,), 160), + ("sx", (0,), 160), + ("sx", (1,), 160), + ("cx", (0, 1), 800), + ("cx", (1, 0), 800), + ("measure", (0,), 1600), + ("measure", (1,), 1600), + ] + ) + self.time_conversion_pass = TimeUnitConversion(inst_durations=instruction_durations) + self.scheduling_pass = ALAPSchedule(durations=instruction_durations) + self.align_measure_pass = AlignMeasures(alignment=16) + + def test_t1_experiment_type(self): + """Test T1 experiment type circuit. + + (input) + + ┌───┐┌────────────────┐┌─┐ + q_0: ┤ X ├┤ Delay(100[dt]) ├┤M├ + └───┘└────────────────┘└╥┘ + c: 1/════════════════════════╩═ + 0 + + (aligned) + + ┌───┐┌────────────────┐┌─┐ + q_0: ┤ X ├┤ Delay(112[dt]) ├┤M├ + └───┘└────────────────┘└╥┘ + c: 1/════════════════════════╩═ + 0 + + This type of experiment slightly changes delay duration of interest. + However the quantization error should be less than alignment * dt. + """ + circuit = QuantumCircuit(1, 1) + circuit.x(0) + circuit.delay(100, 0, unit="dt") + circuit.measure(0, 0) + + timed_circuit = self.time_conversion_pass(circuit) + scheduled_circuit = self.scheduling_pass(timed_circuit, property_set={"time_unit": "dt"}) + aligned_circuit = self.align_measure_pass( + scheduled_circuit, property_set={"time_unit": "dt"} + ) + + ref_circuit = QuantumCircuit(1, 1) + ref_circuit.x(0) + ref_circuit.delay(112, 0, unit="dt") + ref_circuit.measure(0, 0) + + self.assertEqual(aligned_circuit, ref_circuit) + + def test_hanh_echo_experiment_type(self): + """Test Hahn echo experiment type circuit. + + (input) + + ┌────┐┌────────────────┐┌───┐┌────────────────┐┌────┐┌─┐ + q_0: ┤ √X ├┤ Delay(100[dt]) ├┤ X ├┤ Delay(100[dt]) ├┤ √X ├┤M├ + └────┘└────────────────┘└───┘└────────────────┘└────┘└╥┘ + c: 1/══════════════════════════════════════════════════════╩═ + 0 + + (output) + + ┌────┐┌────────────────┐┌───┐┌────────────────┐┌────┐┌──────────────┐┌─┐ + q_0: ┤ √X ├┤ Delay(100[dt]) ├┤ X ├┤ Delay(100[dt]) ├┤ √X ├┤ Delay(8[dt]) ├┤M├ + └────┘└────────────────┘└───┘└────────────────┘└────┘└──────────────┘└╥┘ + c: 1/══════════════════════════════════════════════════════════════════════╩═ + 0 + + This type of experiment doesn't change duration of interest (two in the middle). + However induces slight delay less than alignment * dt before measurement. + This might induce extra amplitude damping error. + """ + circuit = QuantumCircuit(1, 1) + circuit.sx(0) + circuit.delay(100, 0, unit="dt") + circuit.x(0) + circuit.delay(100, 0, unit="dt") + circuit.sx(0) + circuit.measure(0, 0) + + timed_circuit = self.time_conversion_pass(circuit) + scheduled_circuit = self.scheduling_pass(timed_circuit, property_set={"time_unit": "dt"}) + aligned_circuit = self.align_measure_pass( + scheduled_circuit, property_set={"time_unit": "dt"} + ) + + ref_circuit = QuantumCircuit(1, 1) + ref_circuit.sx(0) + ref_circuit.delay(100, 0, unit="dt") + ref_circuit.x(0) + ref_circuit.delay(100, 0, unit="dt") + ref_circuit.sx(0) + ref_circuit.delay(8, 0, unit="dt") + ref_circuit.measure(0, 0) + + self.assertEqual(aligned_circuit, ref_circuit) + + def test_mid_circuit_measure(self): + """Test circuit with mid circuit measurement. + + (input) + + ┌───┐┌────────────────┐┌─┐┌───────────────┐┌───┐┌────────────────┐┌─┐ + q_0: ┤ X ├┤ Delay(100[dt]) ├┤M├┤ Delay(10[dt]) ├┤ X ├┤ Delay(120[dt]) ├┤M├ + └───┘└────────────────┘└╥┘└───────────────┘└───┘└────────────────┘└╥┘ + c: 2/════════════════════════╩══════════════════════════════════════════╩═ + 0 1 + + (output) + + ┌───┐┌────────────────┐┌─┐┌───────────────┐┌───┐┌────────────────┐┌─┐ + q_0: ┤ X ├┤ Delay(112[dt]) ├┤M├┤ Delay(10[dt]) ├┤ X ├┤ Delay(134[dt]) ├┤M├ + └───┘└────────────────┘└╥┘└───────────────┘└───┘└────────────────┘└╥┘ + c: 2/════════════════════════╩══════════════════════════════════════════╩═ + 0 1 + + Extra delay is always added to the existing delay right before the measurement. + Delay after measurement is unchanged. + """ + circuit = QuantumCircuit(1, 2) + circuit.x(0) + circuit.delay(100, 0, unit="dt") + circuit.measure(0, 0) + circuit.delay(10, 0, unit="dt") + circuit.x(0) + circuit.delay(120, 0, unit="dt") + circuit.measure(0, 1) + + timed_circuit = self.time_conversion_pass(circuit) + scheduled_circuit = self.scheduling_pass(timed_circuit, property_set={"time_unit": "dt"}) + aligned_circuit = self.align_measure_pass( + scheduled_circuit, property_set={"time_unit": "dt"} + ) + + ref_circuit = QuantumCircuit(1, 2) + ref_circuit.x(0) + ref_circuit.delay(112, 0, unit="dt") + ref_circuit.measure(0, 0) + ref_circuit.delay(10, 0, unit="dt") + ref_circuit.x(0) + ref_circuit.delay(134, 0, unit="dt") + ref_circuit.measure(0, 1) + + self.assertEqual(aligned_circuit, ref_circuit) + + def test_mid_circuit_multiq_gates(self): + """Test circuit with mid circuit measurement and multi qubit gates. + + (input) + + ┌───┐┌────────────────┐┌─┐ ┌─┐ + q_0: ┤ X ├┤ Delay(100[dt]) ├┤M├──■───────■──┤M├ + └───┘└────────────────┘└╥┘┌─┴─┐┌─┐┌─┴─┐└╥┘ + q_1: ────────────────────────╫─┤ X ├┤M├┤ X ├─╫─ + ║ └───┘└╥┘└───┘ ║ + c: 2/════════════════════════╩═══════╩═══════╩═ + 0 1 0 + + (output) + + ┌───┐ ┌────────────────┐┌─┐ ┌─────────────────┐ ┌─┐» + q_0: ───────┤ X ├───────┤ Delay(112[dt]) ├┤M├──■──┤ Delay(1600[dt]) ├──■──┤M├» + ┌──────┴───┴──────┐└────────────────┘└╥┘┌─┴─┐└───────┬─┬───────┘┌─┴─┐└╥┘» + q_1: ┤ Delay(1872[dt]) ├───────────────────╫─┤ X ├────────┤M├────────┤ X ├─╫─» + └─────────────────┘ ║ └───┘ └╥┘ └───┘ ║ » + c: 2/══════════════════════════════════════╩═══════════════╩═══════════════╩═» + 0 1 0 » + « + «q_0: ─────────────────── + « ┌─────────────────┐ + «q_1: ┤ Delay(1600[dt]) ├ + « └─────────────────┘ + «c: 2/═══════════════════ + « + + Delay for the other channel paired by multi-qubit instruction is also scheduled. + Delay (1872dt) = X (160dt) + Delay (100dt + extra 12dt) + Measure (1600dt). + """ + circuit = QuantumCircuit(2, 2) + circuit.x(0) + circuit.delay(100, 0, unit="dt") + circuit.measure(0, 0) + circuit.cx(0, 1) + circuit.measure(1, 1) + circuit.cx(0, 1) + circuit.measure(0, 0) + + timed_circuit = self.time_conversion_pass(circuit) + scheduled_circuit = self.scheduling_pass(timed_circuit, property_set={"time_unit": "dt"}) + aligned_circuit = self.align_measure_pass( + scheduled_circuit, property_set={"time_unit": "dt"} + ) + + ref_circuit = QuantumCircuit(2, 2) + ref_circuit.x(0) + ref_circuit.delay(112, 0, unit="dt") + ref_circuit.measure(0, 0) + ref_circuit.delay(160 + 112 + 1600, 1, unit="dt") + ref_circuit.cx(0, 1) + ref_circuit.delay(1600, 0, unit="dt") + ref_circuit.measure(1, 1) + ref_circuit.cx(0, 1) + ref_circuit.delay(1600, 1, unit="dt") + ref_circuit.measure(0, 0) + + self.assertEqual(aligned_circuit, ref_circuit) + + def test_alignment_is_not_processed(self): + """Test avoid pass processing if delay is aligned.""" + circuit = QuantumCircuit(2, 2) + circuit.x(0) + circuit.delay(160, 0, unit="dt") + circuit.measure(0, 0) + circuit.cx(0, 1) + circuit.measure(1, 1) + circuit.cx(0, 1) + circuit.measure(0, 0) + + # pre scheduling is not necessary because alignment is skipped + # this is to minimize breaking changes to existing code. + transpiled = self.align_measure_pass(circuit, property_set={"time_unit": "dt"}) + + self.assertEqual(transpiled, circuit) + + +class TestPulseGateValidation(QiskitTestCase): + """A test for pulse gate validation pass.""" + + def setUp(self): + super().setUp() + self.pulse_gate_validation_pass = ValidatePulseGates(alignment=16) + + def test_invalid_pulse_duration(self): + """Kill pass manager if invalid pulse gate is found.""" + + # this is invalid duration pulse + # this will cause backend error since this doesn't fit with waveform memory chunk. + custom_gate = pulse.Schedule(name="custom_x_gate") + custom_gate.insert( + 0, pulse.Play(pulse.Constant(100, 0.1), pulse.DriveChannel(0)), inplace=True + ) + + circuit = QuantumCircuit(1) + circuit.x(0) + circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) + + with self.assertRaises(TranspilerError): + self.pulse_gate_validation_pass(circuit) + + def test_valid_pulse_duration(self): + """No error raises if valid calibration is provided.""" + + # this is valid duration pulse + custom_gate = pulse.Schedule(name="custom_x_gate") + custom_gate.insert( + 0, pulse.Play(pulse.Constant(160, 0.1), pulse.DriveChannel(0)), inplace=True + ) + + circuit = QuantumCircuit(1) + circuit.x(0) + circuit.add_calibration("x", qubits=(0,), schedule=custom_gate) + + # just not raise an error + self.pulse_gate_validation_pass(circuit) + + def test_no_calibration(self): + """No error raises if no calibration is addedd.""" + + circuit = QuantumCircuit(1) + circuit.x(0) + + # just not raise an error + self.pulse_gate_validation_pass(circuit)