From 805a5b27133b75940550dd19b8ba89d77dae3473 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Tue, 18 Oct 2022 00:45:58 +0900 Subject: [PATCH 01/16] Eliminate use of Schedule in the builder context. Schedule is implicitly converted into ScheduleBlock with AreaBarrier instructions. --- qiskit/pulse/builder.py | 205 ++++++++++++++------ qiskit/pulse/instructions/__init__.py | 2 +- qiskit/pulse/instructions/directives.py | 77 ++++++++ qiskit/pulse/transforms/canonicalization.py | 51 +++-- 4 files changed, 252 insertions(+), 83 deletions(-) diff --git a/qiskit/pulse/builder.py b/qiskit/pulse/builder.py index f0ed02eaa006..7e8d2d1c5d7c 100644 --- a/qiskit/pulse/builder.py +++ b/qiskit/pulse/builder.py @@ -585,8 +585,7 @@ def __init__( if isinstance(block, ScheduleBlock): root_block = block elif isinstance(block, Schedule): - root_block = ScheduleBlock() - root_block.append(instructions.Call(subroutine=block)) + root_block = self._naive_typecast_schedule(block) else: raise exceptions.PulseError( f"Input `block` type {block.__class__.__name__} is " @@ -706,7 +705,7 @@ def _compile_lazy_circuit(self): lazy_circuit = self._lazy_circuit # reset lazy circuit self._lazy_circuit = self._new_circuit() - self.call_subroutine(subroutine=self._compile_circuit(lazy_circuit)) + self.call_subroutine(self._compile_circuit(lazy_circuit)) def _compile_circuit(self, circ) -> Schedule: """Take a QuantumCircuit and output the pulse schedule associated with the circuit.""" @@ -731,27 +730,74 @@ def append_instruction(self, instruction: instructions.Instruction): """ self._context_stack[-1].append(instruction) + def append_reference(self, name: str, *extra_keys: str): + """Add external program as a :class:`~qiskit.pulse.instructions.Reference` instruction. + + Args: + name: Name of subroutine. + extra_keys: Assistance keys to uniquely specify the subroutine. + """ + inst = instructions.Reference(name, *extra_keys) + self.append_instruction(inst) + @_compile_lazy_circuit_before def append_block(self, context_block: ScheduleBlock): """Add a :class:`ScheduleBlock` to the builder's context schedule. Args: context_block: ScheduleBlock to append to the current context block. + + Raises: + PulseError: When non ScheduleBlock object is appended. """ + if not isinstance(context_block, ScheduleBlock): + raise exceptions.PulseError( + f"'{context_block.__class__.__name__}' is not valid data format in the builder. " + "Only 'ScheduleBlock' can be appended to the builder context." + ) + # ignore empty context if len(context_block) > 0: self._context_stack[-1].append(context_block) - def append_reference(self, name: str, *extra_keys: str): - """Add external program as a :class:`~qiskit.pulse.instructions.Reference` instruction. + @functools.singledispatchmethod + def inject_subroutine( + self, + subroutine: Union[Schedule, ScheduleBlock], + ): + """Append a :class:`ScheduleBlock` to the builder's context schedule. + + This operationd doesn't create reference. Subrotuine is directly + injected into current context schedule. Args: - name: Name of subroutine. - extra_keys: Assistance keys to uniquely specify the subroutine. + subroutine: ScheduleBlock to append to the current context block. + + Raises: + PulseError: When subroutine is not Schedule nor ScheduleBlock. """ - inst = instructions.Reference(name, *extra_keys) - self.append_instruction(inst) + raise exceptions.PulseError( + f"Subroutine type {subroutine.__class__.__name__} is " + "not valid data format. Inject Schedule or ScheduleBlock." + ) + + @inject_subroutine.register + def _(self, block: ScheduleBlock): + self._compile_lazy_circuit() + + if len(block) == 0: + return + self._context_stack[-1].append(block) + @inject_subroutine.register + def _(self, schedule: Schedule): + self._compile_lazy_circuit() + + if len(schedule) == 0: + return + self._context_stack[-1].append(self._naive_typecast_schedule(schedule)) + + @functools.singledispatchmethod def call_subroutine( self, subroutine: Union[circuit.QuantumCircuit, Schedule, ScheduleBlock], @@ -778,34 +824,37 @@ def call_subroutine( Raises: PulseError: - - When specified parameter is not contained in the subroutine - When input subroutine is not valid data format. """ - if isinstance(subroutine, circuit.QuantumCircuit): - self._compile_lazy_circuit() - subroutine = self._compile_circuit(subroutine) - - if not isinstance(subroutine, (Schedule, ScheduleBlock)): - raise exceptions.PulseError( - f"Subroutine type {subroutine.__class__.__name__} is " - "not valid data format. Call QuantumCircuit, " - "Schedule, or ScheduleBlock." - ) + raise exceptions.PulseError( + f"Subroutine type {subroutine.__class__.__name__} is " + "not valid data format. Call QuantumCircuit, " + "Schedule, or ScheduleBlock." + ) - if len(subroutine) == 0: + @call_subroutine.register + def _( + self, + target_block: ScheduleBlock, + name: Optional[str] = None, + value_dict: Optional[Dict[ParameterExpression, ParameterValueType]] = None, + **kw_params: ParameterValueType, + ): + if len(target_block) == 0: return # Create local parameter assignment local_assignment = dict() for param_name, value in kw_params.items(): - params = subroutine.get_parameters(param_name) + params = target_block.get_parameters(param_name) if not params: raise exceptions.PulseError( f"Parameter {param_name} is not defined in the target subroutine. " - f'{", ".join(map(str, subroutine.parameters))} can be specified.' + f'{", ".join(map(str, target_block.parameters))} can be specified.' ) for param in params: local_assignment[param] = value + if value_dict: if local_assignment.keys() & value_dict.keys(): warnings.warn( @@ -816,22 +865,54 @@ def call_subroutine( ) local_assignment.update(value_dict) - if isinstance(subroutine, ScheduleBlock): - # If subroutine is schedule block, use reference mechanism. - if local_assignment: - subroutine = subroutine.assign_parameters(local_assignment, inplace=False) - if name is None: - # Add unique string, not to accidentally override existing reference entry. - keys = (subroutine.name, uuid.uuid4().hex) - else: - keys = (name,) - self.append_reference(*keys) - self.get_context().assign_references({keys: subroutine}, inplace=True) + if local_assignment: + target_block = target_block.assign_parameters(local_assignment, inplace=False) + + if name is None: + # Add unique string, not to accidentally override existing reference entry. + keys = (target_block.name, uuid.uuid4().hex) else: - # If subroutine is schedule, use Call instruction. - name = name or subroutine.name - call_instruction = instructions.Call(subroutine, local_assignment, name) - self.append_instruction(call_instruction) + keys = (name,) + + self.append_reference(*keys) + self.get_context().assign_references({keys: target_block}, inplace=True) + + @call_subroutine.register + def _( + self, + target_schedule: Schedule, + name: Optional[str] = None, + value_dict: Optional[Dict[ParameterExpression, ParameterValueType]] = None, + **kw_params: ParameterValueType, + ): + if len(target_schedule) == 0: + return + + self.call_subroutine( + self._naive_typecast_schedule(target_schedule), + name=name, + value_dict=value_dict, + **kw_params, + ) + + @call_subroutine.register + def _( + self, + target_circuit: circuit.QuantumCircuit, + name: Optional[str] = None, + value_dict: Optional[Dict[ParameterExpression, ParameterValueType]] = None, + **kw_params: ParameterValueType, + ): + if len(target_circuit) == 0: + return + + self._compile_lazy_circuit() + self.call_subroutine( + self._compile_circuit(target_circuit), + name=name, + value_dict=value_dict, + **kw_params, + ) @_requires_backend def call_gate(self, gate: circuit.Gate, qubits: Tuple[int, ...], lazy: bool = True): @@ -870,6 +951,21 @@ def _call_gate(self, gate, qargs): self._lazy_circuit.append(gate, qargs=qargs) + @staticmethod + def _naive_typecast_schedule(schedule: Schedule): + # Naively convert into ScheduleBlock + from qiskit.pulse.transforms import inline_subroutines, flatten, pad + + preprocessed_schedule = inline_subroutines(flatten(schedule)) + pad(preprocessed_schedule, inplace=True, pad_with=instructions.AreaBarrier) + + # default to left alignment, namely ASAP scheduling + target_block = ScheduleBlock(name=schedule.name) + for _, inst in preprocessed_schedule.instructions: + target_block.append(inst, inplace=True) + + return target_block + def build( backend=None, @@ -973,21 +1069,9 @@ def append_schedule(schedule: Union[Schedule, ScheduleBlock]): """Call a schedule by appending to the active builder's context block. Args: - schedule: Schedule to append. - - Raises: - PulseError: When input `schedule` is invalid data format. + schedule: Schedule or ScheduleBlock to append. """ - if isinstance(schedule, Schedule): - _active_builder().append_instruction(instructions.Call(subroutine=schedule)) - elif isinstance(schedule, ScheduleBlock): - _active_builder().append_block(schedule) - else: - raise exceptions.PulseError( - f"Input program {schedule.__class__.__name__} is not " - "acceptable program format. Input `Schedule` or " - "`ScheduleBlock`." - ) + _active_builder().inject_subroutine(schedule) def append_instruction(instruction: instructions.Instruction): @@ -1986,16 +2070,8 @@ def call( the parameters having the same name are all updated together. If you want to avoid name collision, use ``value_dict`` with :class:`~.Parameter` objects instead. - - Raises: - exceptions.PulseError: If the input ``target`` type is not supported. """ - if not isinstance(target, (circuit.QuantumCircuit, Schedule, ScheduleBlock)): - raise exceptions.PulseError(f"'{target.__class__.__name__}' is not a valid target object.") - - _active_builder().call_subroutine( - subroutine=target, name=name, value_dict=value_dict, **kw_params - ) + _active_builder().call_subroutine(target, name, value_dict, **kw_params) def reference(name: str, *extra_keys: str): @@ -2237,7 +2313,10 @@ def measure( # note this is not a subroutine. # just a macro to automate combination of stimulus and acquisition. - _active_builder().call_subroutine(measure_sched) + # prepare unique reference name based on qubit and memory slot index. + qubits_repr = "&".join(map(str, qubits)) + mslots_repr = "&".join(map(lambda r: str(r.index), registers)) + _active_builder().call_subroutine(measure_sched, name=f"measure_{qubits_repr}..{mslots_repr}") if len(qubits) == 1: return registers[0] @@ -2283,7 +2362,7 @@ def measure_all() -> List[chans.MemorySlot]: # note this is not a subroutine. # just a macro to automate combination of stimulus and acquisition. - _active_builder().call_subroutine(measure_sched) + _active_builder().call_subroutine(measure_sched, name="measure_all") return registers diff --git a/qiskit/pulse/instructions/__init__.py b/qiskit/pulse/instructions/__init__.py index 142a6e96e857..b79134104738 100644 --- a/qiskit/pulse/instructions/__init__.py +++ b/qiskit/pulse/instructions/__init__.py @@ -57,7 +57,7 @@ """ from .acquire import Acquire from .delay import Delay -from .directives import Directive, RelativeBarrier +from .directives import Directive, RelativeBarrier, AreaBarrier from .call import Call from .instruction import Instruction from .frequency import SetFrequency, ShiftFrequency diff --git a/qiskit/pulse/instructions/directives.py b/qiskit/pulse/instructions/directives.py index d09b69823332..66038373db48 100644 --- a/qiskit/pulse/instructions/directives.py +++ b/qiskit/pulse/instructions/directives.py @@ -55,3 +55,80 @@ def channels(self) -> Tuple[chans.Channel]: def __eq__(self, other): """Verify two barriers are equivalent.""" return isinstance(other, type(self)) and set(self.channels) == set(other.channels) + + +class AreaBarrier(Directive): + """Pulse ``AreaBarrier`` directive. + + This instruction is intended to be used internally within the pulse builder, + to naively convert :class:`.Schedule` into :class:`.ScheduleBlock`. + Becasue :class:`.ScheduleBlock` cannot take absolute instruction interval, + this instruction helps the block represetation with finding instruction starting time. + + Example: + + This schedule plays constant pulse at t0 = 120. + + .. code-block:: python + + schedule = Schedule() + schedule.insert(120, Play(Constant(10, 0.1), DriveChannel(0))) + + This schedule block is expected to be identical to above at a time of execution. + + .. code-block:: python + + block = ScheduleBlock() + block.append(AreaBarrier(120, DriveChannel(0))) + block.append(Play(Constant(10, 0.1), DriveChannel(0))) + + Such conversion may be done by + + .. code-block:: python + + from qiskit.pulse.transforms import block_to_schedule, remove_directives + + schedule = remove_directives(block_to_schedule(block)) + + + .. note:: + + The AreaBarrier instruction behaves almost identically + to :class:`~qiskit.pulse.instructions.Delay` instruction. + However, the AreaBarrier is just a compiler directive and must be removed before execution. + This may be done by :func:`~qiskit.pulse.transforms.remove_directives` transform. + Once these directives are removed, occupied timeslots are released and + user can insert another instruction without timing overlap. + """ + + def __init__( + self, + duration: int, + channel: chans.Channel, + name: Optional[str] = None, + ): + """Create an area barrier directive. + + Args: + duration: Length of time of the occupation in terms of dt. + channel: The channel that will be the occupied. + name: Name of the area barrier for display purposes. + """ + super().__init__(operands=(duration, channel), name=name) + + @property + def channel(self) -> chans.Channel: + """Return the :py:class:`~qiskit.pulse.channels.Channel` that this instruction is + scheduled on. + """ + return self.operands[1] + + @property + def channels(self) -> Tuple[chans.Channel]: + """Returns the channels that this schedule uses.""" + return (self.channel,) + + @property + def duration(self) -> int: + """Duration of this instruction.""" + return self.operands[0] diff --git a/qiskit/pulse/transforms/canonicalization.py b/qiskit/pulse/transforms/canonicalization.py index 4e2409ffa354..7cb7236b5910 100644 --- a/qiskit/pulse/transforms/canonicalization.py +++ b/qiskit/pulse/transforms/canonicalization.py @@ -13,7 +13,7 @@ import warnings from collections import defaultdict -from typing import List, Optional, Iterable, Union +from typing import List, Optional, Iterable, Union, Type import numpy as np @@ -457,6 +457,7 @@ def pad( channels: Optional[Iterable[chans.Channel]] = None, until: Optional[int] = None, inplace: bool = False, + pad_with: Optional[Type[instructions.Instruction]] = None, ) -> Schedule: """Pad the input Schedule with ``Delay``s on all unoccupied timeslots until ``schedule.duration`` or ``until`` if not ``None``. @@ -468,37 +469,49 @@ def pad( of ``schedule`` it will be added. until: Time to pad until. Defaults to ``schedule.duration`` if not provided. inplace: Pad this schedule by mutating rather than returning a new schedule. + pad_with: Pulse ``Instruction`` subclass to be used for padding. + Default to :class:`~qiskit.pulse.instructions.Delay` instruction. Returns: The padded schedule. + + Raises: + PulseError: When non pulse instruction is set to `pad_with`. """ until = until or schedule.duration channels = channels or schedule.channels + if pad_with: + if issubclass(pad_with, instructions.Instruction): + pad_cls = pad_with + else: + raise PulseError( + f"'{pad_with.__class__.__name__}' is not valid pulse instruction to pad with." + ) + else: + pad_cls = instructions.Delay + for channel in channels: if isinstance(channel, ClassicalIOChannel): continue + if channel not in schedule.channels: - schedule |= instructions.Delay(until, channel) + schedule = schedule.insert(0, instructions.Delay(until, channel), inplace=inplace) continue - curr_time = 0 - # Use the copy of timeslots. When a delay is inserted before the current interval, - # current timeslot is pointed twice and the program crashes with the wrong pointer index. - timeslots = schedule.timeslots[channel].copy() - # TODO: Replace with method of getting instructions on a channel - for interval in timeslots: - if curr_time >= until: + prev_time = 0 + timeslots = iter(schedule.timeslots[channel]) + to_pad = [] + while prev_time < until: + try: + t0, t1 = next(timeslots) + except StopIteration: + to_pad.append((prev_time, until - prev_time)) break - if interval[0] != curr_time: - end_time = min(interval[0], until) - schedule = schedule.insert( - curr_time, instructions.Delay(end_time - curr_time, channel), inplace=inplace - ) - curr_time = interval[1] - if curr_time < until: - schedule = schedule.insert( - curr_time, instructions.Delay(until - curr_time, channel), inplace=inplace - ) + if prev_time < t0: + to_pad.append((prev_time, min(t0, until) - prev_time)) + prev_time = t1 + for t0, duration in to_pad: + schedule = schedule.insert(t0, pad_cls(duration, channel), inplace=inplace) return schedule From a02bcb54b2d428a6a33452c53c3f21dac90c2a70 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 19 Oct 2022 19:09:51 +0900 Subject: [PATCH 02/16] Support AreaBarrier instruction in QPY --- qiskit/qpy/type_keys.py | 8 ++++++- test/python/qpy/test_block_load_from_qpy.py | 23 ++++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/qiskit/qpy/type_keys.py b/qiskit/qpy/type_keys.py index 38b22addf8d0..d79aee33a4b2 100644 --- a/qiskit/qpy/type_keys.py +++ b/qiskit/qpy/type_keys.py @@ -44,6 +44,7 @@ SetPhase, ShiftPhase, RelativeBarrier, + AreaBarrier, ) from qiskit.pulse.library import Waveform, SymbolicPulse from qiskit.pulse.schedule import ScheduleBlock @@ -231,10 +232,11 @@ class ScheduleInstruction(TypeKeyBase): SET_PHASE = b"q" SHIFT_PHASE = b"r" BARRIER = b"b" + AREA_BARRIER = b"c" # 's' is reserved by ScheduleBlock, i.e. block can be nested as an element. # Call instructon is not supported by QPY. - # This instruction is excluded from ScheduleBlock instructions with + # This instruction has been excluded from ScheduleBlock instructions with # qiskit-terra/#8005 and new instruction Reference will be added instead. # Call is only applied to Schedule which is not supported by QPY. # Also snapshot is not suppored because of its limited usecase. @@ -257,6 +259,8 @@ def assign(cls, obj): return cls.SHIFT_PHASE if isinstance(obj, RelativeBarrier): return cls.BARRIER + if isinstance(obj, AreaBarrier): + return cls.AREA_BARRIER raise exceptions.QpyError( f"Object type '{type(obj)}' is not supported in {cls.__name__} namespace." @@ -280,6 +284,8 @@ def retrieve(cls, type_key): return ShiftPhase if type_key == cls.BARRIER: return RelativeBarrier + if type_key == cls.AREA_BARRIER: + return AreaBarrier raise exceptions.QpyError( f"A class corresponding to type key '{type_key}' is not found in {cls.__name__} namespace." diff --git a/test/python/qpy/test_block_load_from_qpy.py b/test/python/qpy/test_block_load_from_qpy.py index ef814aecce61..0e9f6af7635a 100644 --- a/test/python/qpy/test_block_load_from_qpy.py +++ b/test/python/qpy/test_block_load_from_qpy.py @@ -17,7 +17,7 @@ import numpy as np -from qiskit.pulse import builder +from qiskit.pulse import builder, Schedule from qiskit.pulse.library import ( SymbolicPulse, Gaussian, @@ -34,6 +34,7 @@ MemorySlot, RegisterSlot, ) +from qiskit.pulse.instructions import Play, AreaBarrier from qiskit.circuit import Parameter, QuantumCircuit, Gate from qiskit.test import QiskitTestCase from qiskit.qpy import dump, load @@ -151,6 +152,12 @@ def test_barrier(self): builder.barrier(DriveChannel(0), DriveChannel(1), ControlChannel(2)) self.assert_roundtrip_equal(test_sched) + def test_area_barrier(self): + """Test area barrier.""" + with builder.build() as test_sched: + builder.append_instruction(AreaBarrier(10, DriveChannel(0))) + self.assert_roundtrip_equal(test_sched) + def test_measure(self): """Test measurement.""" with builder.build() as test_sched: @@ -187,6 +194,20 @@ def test_nested_blocks(self): builder.delay(200, DriveChannel(1)) self.assert_roundtrip_equal(test_sched) + def test_called_schedule(self): + """Test referenced pulse Schedule object. + + Referened object is naively converted into ScheduleBlock with AreaBarrier instructions. + Thus referenced Schedule is still QPY compatibile. + """ + refsched = Schedule() + refsched.insert(20, Play(Constant(100, 0.1), DriveChannel(0))) + refsched.insert(50, Play(Constant(100, 0.1), DriveChannel(1))) + + with builder.build() as test_sched: + builder.call(refsched, name="test_ref") + self.assert_roundtrip_equal(test_sched) + def test_bell_schedule(self): """Test complex schedule to create a Bell state.""" with builder.build() as test_sched: From f2bc6076213f0dd55a66ef3a1e891fea5322de8f Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 19 Oct 2022 19:10:17 +0900 Subject: [PATCH 03/16] Update RZX calibration builder to generate ScheduleBlock --- .../passes/calibration/rzx_builder.py | 156 ++++++++---------- .../test_calibrationbuilder.py | 16 +- 2 files changed, 76 insertions(+), 96 deletions(-) rename test/python/{pulse => transpiler}/test_calibrationbuilder.py (92%) diff --git a/qiskit/transpiler/passes/calibration/rzx_builder.py b/qiskit/transpiler/passes/calibration/rzx_builder.py index 6c4a6e4f2efd..62bd63197316 100644 --- a/qiskit/transpiler/passes/calibration/rzx_builder.py +++ b/qiskit/transpiler/passes/calibration/rzx_builder.py @@ -12,18 +12,17 @@ """RZX calibration builders.""" +import enum import math import warnings from typing import List, Tuple, Union -import enum import numpy as np from qiskit.circuit import Instruction as CircuitInst from qiskit.circuit.library.standard_gates import RZXGate from qiskit.exceptions import QiskitError from qiskit.pulse import ( Play, - Delay, Schedule, ScheduleBlock, ControlChannel, @@ -31,8 +30,9 @@ GaussianSquare, Waveform, ) +from qiskit.pulse import builder from qiskit.pulse.filters import filter_instructions -from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap, CalibrationPublisher +from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap from .base_builder import CalibrationBuilder from .exceptions import CalibrationNotAvailable @@ -104,8 +104,10 @@ def supported(self, node_op: CircuitInst, qubits: List) -> bool: return isinstance(node_op, RZXGate) and self._inst_map.has("cx", qubits) @staticmethod - def rescale_cr_inst(instruction: Play, theta: float, sample_mult: int = 16) -> Play: - """ + @builder.macro + def rescale_cr_inst(instruction: Play, theta: float, sample_mult: int = 16) -> int: + """A builder macro to play stretched pulse. + Args: instruction: The instruction from which to create a new shortened or lengthened pulse. theta: desired angle, pi/2 is assumed to be the angle that the pulse in the given @@ -113,8 +115,7 @@ def rescale_cr_inst(instruction: Play, theta: float, sample_mult: int = 16) -> P sample_mult: All pulses must be a multiple of sample_mult. Returns: - qiskit.pulse.Play: The play instruction with the stretched compressed - GaussianSquare pulse. + Duration of stretched pulse. Raises: QiskitError: if rotation angle is not assigned. @@ -133,24 +134,29 @@ def rescale_cr_inst(instruction: Play, theta: float, sample_mult: int = 16) -> P # The error function is used because the Gaussian may have chopped tails. gaussian_area = abs(amp) * sigma * np.sqrt(2 * np.pi) * math.erf(n_sigmas) area = gaussian_area + abs(amp) * width - target_area = abs(theta) / (np.pi / 2.0) * area - sign = np.sign(theta) if target_area > gaussian_area: width = (target_area - gaussian_area) / abs(amp) duration = round((width + n_sigmas * sigma) / sample_mult) * sample_mult - return Play( - GaussianSquare(amp=sign * amp, width=width, sigma=sigma, duration=duration), - channel=instruction.channel, + stretched_pulse = GaussianSquare( + duration=duration, + amp=np.sign(theta) * amp, + width=width, + sigma=sigma, ) else: - amp_scale = sign * target_area / gaussian_area + amp_scale = np.sign(theta) * target_area / gaussian_area duration = round(n_sigmas * sigma / sample_mult) * sample_mult - return Play( - GaussianSquare(amp=amp * amp_scale, width=0, sigma=sigma, duration=duration), - channel=instruction.channel, + stretched_pulse = GaussianSquare( + duration=duration, + amp=amp * amp_scale, + width=0, + sigma=sigma, ) + builder.play(stretched_pulse, instruction.channel) + + return duration def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, ScheduleBlock]: """Builds the calibration schedule for the RZXGate(theta) with echos. @@ -169,11 +175,8 @@ def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, """ theta = node_op.params[0] - rzx_theta = Schedule(name="rzx(%.3f)" % theta) - rzx_theta.metadata["publisher"] = CalibrationPublisher.QISKIT - if np.isclose(theta, 0.0): - return rzx_theta + return ScheduleBlock(name="rzx(0.000)") cx_sched = self._inst_map.get("cx", qubits=qubits) cal_type, cr_tones, comp_tones = _check_calibration_type(cx_sched) @@ -194,59 +197,44 @@ def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, "Native CR direction cannot be determined." ) + xgate = self._inst_map.get("x", qubits[0]) + with builder.build( + default_alignment="sequential", name="rzx(%.3f)" % theta + ) as rzx_theta_native: + for cr_tone, comp_tone in zip(cr_tones, comp_tones): + with builder.align_left(): + self.rescale_cr_inst(cr_tone, theta) + self.rescale_cr_inst(comp_tone, theta) + builder.call(xgate) + # Determine native direction, assuming only single drive channel per qubit. # This guarantees channel and qubit index equality. - is_native = comp_tones[0].channel.index == qubits[1] - - stretched_cr_tones = list(map(lambda p: self.rescale_cr_inst(p, theta), cr_tones)) - stretched_comp_tones = list(map(lambda p: self.rescale_cr_inst(p, theta), comp_tones)) - - if is_native: - xgate = self._inst_map.get("x", qubits[0]) - - for cr, comp in zip(stretched_cr_tones, stretched_comp_tones): - current_dur = rzx_theta.duration - rzx_theta.insert(current_dur, cr, inplace=True) - rzx_theta.insert(current_dur, comp, inplace=True) - rzx_theta.append(xgate, inplace=True) - - else: - # Add hadamard gate to flip - xgate = self._inst_map.get("x", qubits[1]) - szc = self._inst_map.get("rz", qubits[1], np.pi / 2) - sxc = self._inst_map.get("sx", qubits[1]) - szt = self._inst_map.get("rz", qubits[0], np.pi / 2) - sxt = self._inst_map.get("sx", qubits[0]) - - # Hadamard to control - rzx_theta.insert(0, szc, inplace=True) - rzx_theta.insert(0, sxc, inplace=True) - rzx_theta.insert(sxc.duration, szc, inplace=True) - - # Hadamard to target - rzx_theta.insert(0, szt, inplace=True) - rzx_theta.insert(0, sxt, inplace=True) - rzx_theta.insert(sxt.duration, szt, inplace=True) - - for cr, comp in zip(stretched_cr_tones, stretched_comp_tones): - current_dur = rzx_theta.duration - rzx_theta.insert(current_dur, cr, inplace=True) - rzx_theta.insert(current_dur, comp, inplace=True) - rzx_theta.append(xgate, inplace=True) - - current_dur = rzx_theta.duration - - # Hadamard to control - rzx_theta.insert(current_dur, szc, inplace=True) - rzx_theta.insert(current_dur, sxc, inplace=True) - rzx_theta.insert(current_dur + sxc.duration, szc, inplace=True) - - # Hadamard to target - rzx_theta.insert(current_dur, szt, inplace=True) - rzx_theta.insert(current_dur, sxt, inplace=True) - rzx_theta.insert(current_dur + sxt.duration, szt, inplace=True) - - return rzx_theta + if comp_tones[0].channel.index == qubits[1]: + return rzx_theta_native + + # Add hadamard gate to flip + xgate = self._inst_map.get("x", qubits[1]) + szc = self._inst_map.get("rz", qubits[1], np.pi / 2) + sxc = self._inst_map.get("sx", qubits[1]) + szt = self._inst_map.get("rz", qubits[0], np.pi / 2) + sxt = self._inst_map.get("sx", qubits[0]) + with builder.build(default_alignment="sequential", name="hadamard") as hadamard: + # Control qubit + builder.call(szc, name="rzc") + builder.call(sxc, name="sxc") + builder.call(szc, name="szc") + # Target qubit + builder.call(szt, name="rzt") + builder.call(sxt, name="sxt") + builder.call(szt, name="szt") + + with builder.build( + default_alignment="sequential", name="rzx(%.3f)" % theta + ) as rzx_theta_flip: + builder.call(hadamard, name="hadamard") + builder.call(rzx_theta_native, name="rzx_theta_native") + builder.call(hadamard, name="hadamard") + return rzx_theta_flip class RZXCalibrationBuilderNoEcho(RZXCalibrationBuilder): @@ -281,11 +269,8 @@ def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, """ theta = node_op.params[0] - rzx_theta = Schedule(name="rzx(%.3f)" % theta) - rzx_theta.metadata["publisher"] = CalibrationPublisher.QISKIT - if np.isclose(theta, 0.0): - return rzx_theta + return ScheduleBlock(name="rzx(0.000)") cx_sched = self._inst_map.get("cx", qubits=qubits) cal_type, cr_tones, comp_tones = _check_calibration_type(cx_sched) @@ -308,21 +293,12 @@ def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, # Determine native direction, assuming only single drive channel per qubit. # This guarantees channel and qubit index equality. - is_native = comp_tones[0].channel.index == qubits[1] - - stretched_cr_tone = self.rescale_cr_inst(cr_tones[0], 2 * theta) - stretched_comp_tone = self.rescale_cr_inst(comp_tones[0], 2 * theta) - - if is_native: - # Placeholder to make pulse gate work - delay = Delay(stretched_cr_tone.duration, DriveChannel(qubits[0])) - - # This doesn't remove unwanted instruction such as ZI - # These terms are eliminated along with other gates around the pulse gate. - rzx_theta = rzx_theta.insert(0, stretched_cr_tone, inplace=True) - rzx_theta = rzx_theta.insert(0, stretched_comp_tone, inplace=True) - rzx_theta = rzx_theta.insert(0, delay, inplace=True) - + if comp_tones[0].channel.index == qubits[1]: + with builder.build(default_alignment="left", name="rzx(%.3f)" % theta) as rzx_theta: + stretched_dur = self.rescale_cr_inst(cr_tones[0], 2 * theta) + self.rescale_cr_inst(comp_tones[0], 2 * theta) + # Placeholder to make pulse gate work + builder.delay(stretched_dur, DriveChannel(qubits[0])) return rzx_theta raise QiskitError("RZXCalibrationBuilderNoEcho only supports hardware-native RZX gates.") diff --git a/test/python/pulse/test_calibrationbuilder.py b/test/python/transpiler/test_calibrationbuilder.py similarity index 92% rename from test/python/pulse/test_calibrationbuilder.py rename to test/python/transpiler/test_calibrationbuilder.py index ba108ca9f64b..599e0a52869c 100644 --- a/test/python/pulse/test_calibrationbuilder.py +++ b/test/python/transpiler/test_calibrationbuilder.py @@ -18,6 +18,7 @@ from ddt import data, ddt from qiskit import circuit, schedule +from qiskit.pulse import builder from qiskit.pulse import ( ControlChannel, Delay, @@ -62,13 +63,17 @@ def test_rzx_calibration_builder_duration(self, theta: float): amp = 1.0 pulse = GaussianSquare(duration=duration, amp=amp, sigma=sigma, width=width) instruction = Play(pulse, ControlChannel(1)) - scaled = RZXCalibrationBuilder.rescale_cr_inst(instruction, theta, sample_mult=sample_mult) + with builder.build(): + # this is builder macro to play stretched pulse. need builder context. + test_duration = RZXCalibrationBuilder.rescale_cr_inst( + instruction=instruction, theta=theta, sample_mult=sample_mult + ) gaussian_area = abs(amp) * sigma * np.sqrt(2 * np.pi) * erf(n_sigmas) area = gaussian_area + abs(amp) * width target_area = abs(theta) / (np.pi / 2.0) * area width = (target_area - gaussian_area) / abs(amp) expected_duration = round((width + n_sigmas * sigma) / sample_mult) * sample_mult - self.assertEqual(scaled.duration, expected_duration) + self.assertEqual(test_duration, expected_duration) def test_pass_alive_with_dcx_ish(self): """Test if the pass is not terminated by error with direct CX input.""" @@ -165,10 +170,9 @@ def test_pulse_amp_typecasted(self): fake_theta = circuit.Parameter("theta") assigned_theta = fake_theta.assign(fake_theta, 0.01) - scaled = RZXCalibrationBuilderNoEcho.rescale_cr_inst( - instruction=fake_play, theta=assigned_theta - ) - scaled_pulse = scaled.pulse + with builder.build() as test_sched: + RZXCalibrationBuilderNoEcho.rescale_cr_inst(instruction=fake_play, theta=assigned_theta) + scaled_pulse = test_sched.blocks[0].blocks[0].pulse self.assertIsInstance(scaled_pulse.amp, complex) From 17d2a461d3e00f8e56d0ee27776a4df2e4eaeb80 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 19 Oct 2022 19:30:23 +0900 Subject: [PATCH 04/16] Add release note --- ...lder-and-rzx-builder-033ac8ad8ad2a192.yaml | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml diff --git a/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml b/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml new file mode 100644 index 000000000000..fbab18e7953f --- /dev/null +++ b/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml @@ -0,0 +1,26 @@ +--- +features: + - | + New pulse directive :class:`~qiskit.pulse.instructions.AreaBarrier` has been added. + This instruction is QPY compatible. This directive behaves almost identically to + delay instruction, but will be removed before execution. + This directive is intended to be used internally within the pulse builder + and helps :class:`.ScheduleBlock` with representing instructions with + absolute time intervals. This allows the pulse builder to convert + :class:`Schedule` into :class:`ScheduleBlock`, rather than wrapping with call instruction. + - | + QPY dump and load now support :class:`~qiskit.pulse.instructions.AreaBarrier` instruction. +upgrade: + - | + The behavior of the pulse builder when a :class:`.Schedule` is called + has been upgraded. Called schedules are internally converted into + :class:`.ScheduleBlock` representation and now reference mechanism is + always applied rather than appending the schedules wrapped by + the :class:`~qiskit.pulse.instructions.Call` instruction. + This is an upgrade of internal representation and thus no API changes. + This change guarantees the generated schedule blocks are always QPY compatible. + - | + :class:`~qiskit.transpiler.passes.RZXCalibrationBuilder` + and :class:`~qiskit.transpiler.passes.RZXCalibrationBuilderNoEcho` transpiler pass + have been upgraded to generate :class:`.ScheduleBlock`. + This change guarantees the transpiled circuits are always QPY compatible. From 4d0ddf5c70448c4138b7d27d615ebe161e8695d8 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 19 Oct 2022 19:32:59 +0900 Subject: [PATCH 05/16] Support python3.7 --- qiskit/pulse/builder.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/qiskit/pulse/builder.py b/qiskit/pulse/builder.py index 7e8d2d1c5d7c..80fda5a498ae 100644 --- a/qiskit/pulse/builder.py +++ b/qiskit/pulse/builder.py @@ -481,6 +481,12 @@ from qiskit.pulse.schedule import Schedule, ScheduleBlock from qiskit.pulse.transforms.alignments import AlignmentKind +if sys.version_info >= (3, 8): + from functools import singledispatchmethod # pylint: disable=no-name-in-module +else: + from singledispatchmethod import singledispatchmethod + + #: contextvars.ContextVar[BuilderContext]: active builder BUILDER_CONTEXTVAR = contextvars.ContextVar("backend") @@ -760,7 +766,7 @@ def append_block(self, context_block: ScheduleBlock): if len(context_block) > 0: self._context_stack[-1].append(context_block) - @functools.singledispatchmethod + @singledispatchmethod def inject_subroutine( self, subroutine: Union[Schedule, ScheduleBlock], @@ -797,7 +803,7 @@ def _(self, schedule: Schedule): return self._context_stack[-1].append(self._naive_typecast_schedule(schedule)) - @functools.singledispatchmethod + @singledispatchmethod def call_subroutine( self, subroutine: Union[circuit.QuantumCircuit, Schedule, ScheduleBlock], From 6a90761dc6396dc30999c40c9c49f7acee648d84 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 19 Oct 2022 20:32:45 +0900 Subject: [PATCH 06/16] Fix hadamard schedule in the RZX builder. This should not use sequential context. --- qiskit/transpiler/passes/calibration/rzx_builder.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qiskit/transpiler/passes/calibration/rzx_builder.py b/qiskit/transpiler/passes/calibration/rzx_builder.py index 62bd63197316..7c9228cb7dcf 100644 --- a/qiskit/transpiler/passes/calibration/rzx_builder.py +++ b/qiskit/transpiler/passes/calibration/rzx_builder.py @@ -218,13 +218,13 @@ def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, sxc = self._inst_map.get("sx", qubits[1]) szt = self._inst_map.get("rz", qubits[0], np.pi / 2) sxt = self._inst_map.get("sx", qubits[0]) - with builder.build(default_alignment="sequential", name="hadamard") as hadamard: + with builder.build(name="hadamard") as hadamard: # Control qubit - builder.call(szc, name="rzc") + builder.call(szc, name="szc") builder.call(sxc, name="sxc") builder.call(szc, name="szc") # Target qubit - builder.call(szt, name="rzt") + builder.call(szt, name="szt") builder.call(sxt, name="sxt") builder.call(szt, name="szt") From 0ca8efbc78481b5edd7456ec02b026a96785981a Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 20 Oct 2022 02:14:54 +0900 Subject: [PATCH 07/16] Review comments Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit/pulse/builder.py | 37 +++++-------------- .../passes/calibration/rzx_builder.py | 27 ++++++++------ ...lder-and-rzx-builder-033ac8ad8ad2a192.yaml | 2 +- 3 files changed, 26 insertions(+), 40 deletions(-) diff --git a/qiskit/pulse/builder.py b/qiskit/pulse/builder.py index 80fda5a498ae..630a0ca96c39 100644 --- a/qiskit/pulse/builder.py +++ b/qiskit/pulse/builder.py @@ -444,6 +444,7 @@ import contextvars import functools import itertools +import sys import uuid import warnings from contextlib import contextmanager @@ -766,15 +767,12 @@ def append_block(self, context_block: ScheduleBlock): if len(context_block) > 0: self._context_stack[-1].append(context_block) - @singledispatchmethod - def inject_subroutine( - self, - subroutine: Union[Schedule, ScheduleBlock], - ): + @_compile_lazy_circuit_before + def append_subroutine(self, subroutine: Union[Schedule, ScheduleBlock]): """Append a :class:`ScheduleBlock` to the builder's context schedule. - This operationd doesn't create reference. Subrotuine is directly - injected into current context schedule. + This operation doesn't create a reference. Subroutine is directly + appended to current context schedule. Args: subroutine: ScheduleBlock to append to the current context block. @@ -782,26 +780,11 @@ def inject_subroutine( Raises: PulseError: When subroutine is not Schedule nor ScheduleBlock. """ - raise exceptions.PulseError( - f"Subroutine type {subroutine.__class__.__name__} is " - "not valid data format. Inject Schedule or ScheduleBlock." - ) - - @inject_subroutine.register - def _(self, block: ScheduleBlock): - self._compile_lazy_circuit() - - if len(block) == 0: - return - self._context_stack[-1].append(block) - - @inject_subroutine.register - def _(self, schedule: Schedule): - self._compile_lazy_circuit() - - if len(schedule) == 0: + if len(subroutine) == 0: return - self._context_stack[-1].append(self._naive_typecast_schedule(schedule)) + if isinstance(subroutine, Schedule): + subroutine = self._naive_typecast_schedule(subroutine) + self._context_stack[-1].append(subroutine) @singledispatchmethod def call_subroutine( @@ -1077,7 +1060,7 @@ def append_schedule(schedule: Union[Schedule, ScheduleBlock]): Args: schedule: Schedule or ScheduleBlock to append. """ - _active_builder().inject_subroutine(schedule) + _active_builder().append_subroutine(schedule) def append_instruction(instruction: instructions.Instruction): diff --git a/qiskit/transpiler/passes/calibration/rzx_builder.py b/qiskit/transpiler/passes/calibration/rzx_builder.py index 7c9228cb7dcf..814fcca4b2f7 100644 --- a/qiskit/transpiler/passes/calibration/rzx_builder.py +++ b/qiskit/transpiler/passes/calibration/rzx_builder.py @@ -197,22 +197,21 @@ def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, "Native CR direction cannot be determined." ) - xgate = self._inst_map.get("x", qubits[0]) - with builder.build( - default_alignment="sequential", name="rzx(%.3f)" % theta - ) as rzx_theta_native: - for cr_tone, comp_tone in zip(cr_tones, comp_tones): - with builder.align_left(): - self.rescale_cr_inst(cr_tone, theta) - self.rescale_cr_inst(comp_tone, theta) - builder.call(xgate) - # Determine native direction, assuming only single drive channel per qubit. # This guarantees channel and qubit index equality. if comp_tones[0].channel.index == qubits[1]: + xgate = self._inst_map.get("x", qubits[0]) + with builder.build( + default_alignment="sequential", name="rzx(%.3f)" % theta + ) as rzx_theta_native: + for cr_tone, comp_tone in zip(cr_tones, comp_tones): + with builder.align_left(): + self.rescale_cr_inst(cr_tone, theta) + self.rescale_cr_inst(comp_tone, theta) + builder.call(xgate) return rzx_theta_native - # Add hadamard gate to flip + # The direction is not native. Add Hadamard gates to flip the direction. xgate = self._inst_map.get("x", qubits[1]) szc = self._inst_map.get("rz", qubits[1], np.pi / 2) sxc = self._inst_map.get("sx", qubits[1]) @@ -232,7 +231,11 @@ def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, default_alignment="sequential", name="rzx(%.3f)" % theta ) as rzx_theta_flip: builder.call(hadamard, name="hadamard") - builder.call(rzx_theta_native, name="rzx_theta_native") + for cr_tone, comp_tone in zip(cr_tones, comp_tones): + with builder.align_left(): + self.rescale_cr_inst(cr_tone, theta) + self.rescale_cr_inst(comp_tone, theta) + builder.call(xgate) builder.call(hadamard, name="hadamard") return rzx_theta_flip diff --git a/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml b/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml index fbab18e7953f..caf8da9fcbb8 100644 --- a/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml +++ b/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml @@ -5,7 +5,7 @@ features: This instruction is QPY compatible. This directive behaves almost identically to delay instruction, but will be removed before execution. This directive is intended to be used internally within the pulse builder - and helps :class:`.ScheduleBlock` with representing instructions with + and helps :class:`.ScheduleBlock` represent instructions with absolute time intervals. This allows the pulse builder to convert :class:`Schedule` into :class:`ScheduleBlock`, rather than wrapping with call instruction. - | From de2f7d5a86647d4dcb25827b37890a8410776bee Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Thu, 20 Oct 2022 02:40:58 +0900 Subject: [PATCH 08/16] Unified append_block and append_subroutine --- qiskit/pulse/builder.py | 42 ++++++++++++++--------------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/qiskit/pulse/builder.py b/qiskit/pulse/builder.py index 630a0ca96c39..677d688dba20 100644 --- a/qiskit/pulse/builder.py +++ b/qiskit/pulse/builder.py @@ -656,7 +656,7 @@ def get_context(self) -> ScheduleBlock: """Get current context. Notes: - New instruction can be added by `.append_block` or `.append_instruction` method. + New instruction can be added by `.append_subroutine` or `.append_instruction` method. Use above methods rather than directly accessing to the current context. """ return self._context_stack[-1] @@ -698,7 +698,7 @@ def compile(self) -> ScheduleBlock: while len(self._context_stack) > 1: current = self.pop_context() - self.append_block(current) + self.append_subroutine(current) return self._context_stack[0] @@ -747,26 +747,6 @@ def append_reference(self, name: str, *extra_keys: str): inst = instructions.Reference(name, *extra_keys) self.append_instruction(inst) - @_compile_lazy_circuit_before - def append_block(self, context_block: ScheduleBlock): - """Add a :class:`ScheduleBlock` to the builder's context schedule. - - Args: - context_block: ScheduleBlock to append to the current context block. - - Raises: - PulseError: When non ScheduleBlock object is appended. - """ - if not isinstance(context_block, ScheduleBlock): - raise exceptions.PulseError( - f"'{context_block.__class__.__name__}' is not valid data format in the builder. " - "Only 'ScheduleBlock' can be appended to the builder context." - ) - - # ignore empty context - if len(context_block) > 0: - self._context_stack[-1].append(context_block) - @_compile_lazy_circuit_before def append_subroutine(self, subroutine: Union[Schedule, ScheduleBlock]): """Append a :class:`ScheduleBlock` to the builder's context schedule. @@ -780,6 +760,12 @@ def append_subroutine(self, subroutine: Union[Schedule, ScheduleBlock]): Raises: PulseError: When subroutine is not Schedule nor ScheduleBlock. """ + if not isinstance(subroutine, (ScheduleBlock, Schedule)): + raise exceptions.PulseError( + f"'{subroutine.__class__.__name__}' is not valid data format in the builder. " + "'Schedule' and 'ScheduleBlock' can be appended to the builder context." + ) + if len(subroutine) == 0: return if isinstance(subroutine, Schedule): @@ -1254,7 +1240,7 @@ def align_left() -> ContextManager[None]: yield finally: current = builder.pop_context() - builder.append_block(current) + builder.append_subroutine(current) @contextmanager @@ -1292,7 +1278,7 @@ def align_right() -> AlignmentKind: yield finally: current = builder.pop_context() - builder.append_block(current) + builder.append_subroutine(current) @contextmanager @@ -1330,7 +1316,7 @@ def align_sequential() -> AlignmentKind: yield finally: current = builder.pop_context() - builder.append_block(current) + builder.append_subroutine(current) @contextmanager @@ -1381,7 +1367,7 @@ def align_equispaced(duration: Union[int, ParameterExpression]) -> AlignmentKind yield finally: current = builder.pop_context() - builder.append_block(current) + builder.append_subroutine(current) @contextmanager @@ -1442,7 +1428,7 @@ def udd10_pos(j): yield finally: current = builder.pop_context() - builder.append_block(current) + builder.append_subroutine(current) @contextmanager @@ -1468,7 +1454,7 @@ def general_transforms(alignment_context: AlignmentKind) -> ContextManager[None] yield finally: current = builder.pop_context() - builder.append_block(current) + builder.append_subroutine(current) @contextmanager From 1f1db8de02baed5abced3c850dc262efce9492f6 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Mon, 24 Oct 2022 11:38:54 +0900 Subject: [PATCH 09/16] Update instruction name AreaBarrier -> TimeBlockade --- qiskit/pulse/builder.py | 2 +- qiskit/pulse/instructions/__init__.py | 2 +- qiskit/pulse/instructions/directives.py | 27 +++++++++++++------ qiskit/qpy/type_keys.py | 12 ++++----- ...lder-and-rzx-builder-033ac8ad8ad2a192.yaml | 4 +-- test/python/qpy/test_block_load_from_qpy.py | 8 +++--- 6 files changed, 33 insertions(+), 22 deletions(-) diff --git a/qiskit/pulse/builder.py b/qiskit/pulse/builder.py index 677d688dba20..4656c442dc64 100644 --- a/qiskit/pulse/builder.py +++ b/qiskit/pulse/builder.py @@ -932,7 +932,7 @@ def _naive_typecast_schedule(schedule: Schedule): from qiskit.pulse.transforms import inline_subroutines, flatten, pad preprocessed_schedule = inline_subroutines(flatten(schedule)) - pad(preprocessed_schedule, inplace=True, pad_with=instructions.AreaBarrier) + pad(preprocessed_schedule, inplace=True, pad_with=instructions.TimeBlockade) # default to left alignment, namely ASAP scheduling target_block = ScheduleBlock(name=schedule.name) diff --git a/qiskit/pulse/instructions/__init__.py b/qiskit/pulse/instructions/__init__.py index b79134104738..9fab29a27c87 100644 --- a/qiskit/pulse/instructions/__init__.py +++ b/qiskit/pulse/instructions/__init__.py @@ -57,7 +57,7 @@ """ from .acquire import Acquire from .delay import Delay -from .directives import Directive, RelativeBarrier, AreaBarrier +from .directives import Directive, RelativeBarrier, TimeBlockade from .call import Call from .instruction import Instruction from .frequency import SetFrequency, ShiftFrequency diff --git a/qiskit/pulse/instructions/directives.py b/qiskit/pulse/instructions/directives.py index 66038373db48..187e4f73700d 100644 --- a/qiskit/pulse/instructions/directives.py +++ b/qiskit/pulse/instructions/directives.py @@ -11,7 +11,7 @@ # that they have been altered from the originals. """Directives are hints to the pulse compiler for how to process its input programs.""" - +import numpy as np from abc import ABC from typing import Optional, Tuple @@ -57,13 +57,13 @@ def __eq__(self, other): return isinstance(other, type(self)) and set(self.channels) == set(other.channels) -class AreaBarrier(Directive): - """Pulse ``AreaBarrier`` directive. +class TimeBlockade(Directive): + """Pulse ``TimeBlockade`` directive. This instruction is intended to be used internally within the pulse builder, to naively convert :class:`.Schedule` into :class:`.ScheduleBlock`. - Becasue :class:`.ScheduleBlock` cannot take absolute instruction interval, - this instruction helps the block represetation with finding instruction starting time. + Because :class:`.ScheduleBlock` cannot take absolute instruction time interval, + this instruction helps the block representation with finding instruction starting time. Example: @@ -79,7 +79,7 @@ class AreaBarrier(Directive): .. code-block:: python block = ScheduleBlock() - block.append(AreaBarrier(120, DriveChannel(0))) + block.append(TimeBlockade(120, DriveChannel(0))) block.append(Play(Constant(10, 0.1), DriveChannel(0))) Such conversion may be done by @@ -93,9 +93,9 @@ class AreaBarrier(Directive): .. note:: - The AreaBarrier instruction behaves almost identically + The TimeBlockade instruction behaves almost identically to :class:`~qiskit.pulse.instructions.Delay` instruction. - However, the AreaBarrier is just a compiler directive and must be removed before execution. + However, the TimeBlockade is just a compiler directive and must be removed before execution. This may be done by :func:`~qiskit.pulse.transforms.remove_directives` transform. Once these directives are removed, occupied timeslots are released and user can insert another instruction without timing overlap. @@ -116,6 +116,17 @@ def __init__( """ super().__init__(operands=(duration, channel), name=name) + def _validate(self): + """Called after initialization to validate instruction data. + + Raises: + PulseError: If the input ``duration`` is not integer value. + """ + if not isinstance(self.duration, int): + raise TypeError( + "TimeBlockade duration cannot be parameterized. Specify an integer duration value." + ) + @property def channel(self) -> chans.Channel: """Return the :py:class:`~qiskit.pulse.channels.Channel` that this instruction is diff --git a/qiskit/qpy/type_keys.py b/qiskit/qpy/type_keys.py index d79aee33a4b2..4355f8071615 100644 --- a/qiskit/qpy/type_keys.py +++ b/qiskit/qpy/type_keys.py @@ -44,7 +44,7 @@ SetPhase, ShiftPhase, RelativeBarrier, - AreaBarrier, + TimeBlockade, ) from qiskit.pulse.library import Waveform, SymbolicPulse from qiskit.pulse.schedule import ScheduleBlock @@ -232,7 +232,7 @@ class ScheduleInstruction(TypeKeyBase): SET_PHASE = b"q" SHIFT_PHASE = b"r" BARRIER = b"b" - AREA_BARRIER = b"c" + TimeBlockade = b"t" # 's' is reserved by ScheduleBlock, i.e. block can be nested as an element. # Call instructon is not supported by QPY. @@ -259,8 +259,8 @@ def assign(cls, obj): return cls.SHIFT_PHASE if isinstance(obj, RelativeBarrier): return cls.BARRIER - if isinstance(obj, AreaBarrier): - return cls.AREA_BARRIER + if isinstance(obj, TimeBlockade): + return cls.TimeBlockade raise exceptions.QpyError( f"Object type '{type(obj)}' is not supported in {cls.__name__} namespace." @@ -284,8 +284,8 @@ def retrieve(cls, type_key): return ShiftPhase if type_key == cls.BARRIER: return RelativeBarrier - if type_key == cls.AREA_BARRIER: - return AreaBarrier + if type_key == cls.TimeBlockade: + return TimeBlockade raise exceptions.QpyError( f"A class corresponding to type key '{type_key}' is not found in {cls.__name__} namespace." diff --git a/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml b/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml index caf8da9fcbb8..1d67f7aa058f 100644 --- a/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml +++ b/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml @@ -1,7 +1,7 @@ --- features: - | - New pulse directive :class:`~qiskit.pulse.instructions.AreaBarrier` has been added. + New pulse directive :class:`~qiskit.pulse.instructions.TimeBlockade` has been added. This instruction is QPY compatible. This directive behaves almost identically to delay instruction, but will be removed before execution. This directive is intended to be used internally within the pulse builder @@ -9,7 +9,7 @@ features: absolute time intervals. This allows the pulse builder to convert :class:`Schedule` into :class:`ScheduleBlock`, rather than wrapping with call instruction. - | - QPY dump and load now support :class:`~qiskit.pulse.instructions.AreaBarrier` instruction. + QPY dump and load now support :class:`~qiskit.pulse.instructions.TimeBlockade` instruction. upgrade: - | The behavior of the pulse builder when a :class:`.Schedule` is called diff --git a/test/python/qpy/test_block_load_from_qpy.py b/test/python/qpy/test_block_load_from_qpy.py index 0e9f6af7635a..e1ea9de79d8f 100644 --- a/test/python/qpy/test_block_load_from_qpy.py +++ b/test/python/qpy/test_block_load_from_qpy.py @@ -34,7 +34,7 @@ MemorySlot, RegisterSlot, ) -from qiskit.pulse.instructions import Play, AreaBarrier +from qiskit.pulse.instructions import Play, TimeBlockade from qiskit.circuit import Parameter, QuantumCircuit, Gate from qiskit.test import QiskitTestCase from qiskit.qpy import dump, load @@ -155,7 +155,7 @@ def test_barrier(self): def test_area_barrier(self): """Test area barrier.""" with builder.build() as test_sched: - builder.append_instruction(AreaBarrier(10, DriveChannel(0))) + builder.append_instruction(TimeBlockade(10, DriveChannel(0))) self.assert_roundtrip_equal(test_sched) def test_measure(self): @@ -197,8 +197,8 @@ def test_nested_blocks(self): def test_called_schedule(self): """Test referenced pulse Schedule object. - Referened object is naively converted into ScheduleBlock with AreaBarrier instructions. - Thus referenced Schedule is still QPY compatibile. + Referenced object is naively converted into ScheduleBlock with TimeBlockade instructions. + Thus referenced Schedule is still QPY compatible. """ refsched = Schedule() refsched.insert(20, Play(Constant(100, 0.1), DriveChannel(0))) From 7ab2710f05605a1d70bbd3d5405243092d0340c1 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Mon, 24 Oct 2022 11:51:39 +0900 Subject: [PATCH 10/16] Documentation updates Co-authored-by: Daniel J. Egger <38065505+eggerdj@users.noreply.github.com> --- qiskit/pulse/instructions/directives.py | 6 +++--- ...rade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml | 6 +++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/qiskit/pulse/instructions/directives.py b/qiskit/pulse/instructions/directives.py index 187e4f73700d..1b24c412aee2 100644 --- a/qiskit/pulse/instructions/directives.py +++ b/qiskit/pulse/instructions/directives.py @@ -61,9 +61,9 @@ class TimeBlockade(Directive): """Pulse ``TimeBlockade`` directive. This instruction is intended to be used internally within the pulse builder, - to naively convert :class:`.Schedule` into :class:`.ScheduleBlock`. - Because :class:`.ScheduleBlock` cannot take absolute instruction time interval, - this instruction helps the block representation with finding instruction starting time. + to convert :class:`.Schedule` into :class:`.ScheduleBlock`. + Because :class:`.ScheduleBlock` cannot take an absolute instruction time interval, + this directive helps the block representation to find the starting time of an instruction. Example: diff --git a/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml b/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml index 1d67f7aa058f..cc24e88f7a2e 100644 --- a/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml +++ b/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml @@ -17,7 +17,11 @@ upgrade: :class:`.ScheduleBlock` representation and now reference mechanism is always applied rather than appending the schedules wrapped by the :class:`~qiskit.pulse.instructions.Call` instruction. - This is an upgrade of internal representation and thus no API changes. + Note that the converted block doesn't necessary recover the original alignment context. + This is simply an ASAP aligned sequence of pulse instructions with absolute time intervals. + This is an upgrade of internal representation of called pulse programs and thus no API changes. + However the :class:`~qiskit.pulse.instructions.Call` instruction and :class:`.Schedule` + no longer appear in the builder's pulse program. This change guarantees the generated schedule blocks are always QPY compatible. - | :class:`~qiskit.transpiler.passes.RZXCalibrationBuilder` From 15887c8074f53a232e00e7e1fcb508727f7cd6bb Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Mon, 24 Oct 2022 11:59:59 +0900 Subject: [PATCH 11/16] Remove old name --- qiskit/pulse/instructions/directives.py | 4 ++-- test/python/qpy/test_block_load_from_qpy.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit/pulse/instructions/directives.py b/qiskit/pulse/instructions/directives.py index 1b24c412aee2..0a9be342a75b 100644 --- a/qiskit/pulse/instructions/directives.py +++ b/qiskit/pulse/instructions/directives.py @@ -107,12 +107,12 @@ def __init__( channel: chans.Channel, name: Optional[str] = None, ): - """Create an area barrier directive. + """Create a time blockade directive. Args: duration: Length of time of the occupation in terms of dt. channel: The channel that will be the occupied. - name: Name of the area barrier for display purposes. + name: Name of the time blockade for display purposes. """ super().__init__(operands=(duration, channel), name=name) diff --git a/test/python/qpy/test_block_load_from_qpy.py b/test/python/qpy/test_block_load_from_qpy.py index e1ea9de79d8f..ea5dac578cc2 100644 --- a/test/python/qpy/test_block_load_from_qpy.py +++ b/test/python/qpy/test_block_load_from_qpy.py @@ -152,8 +152,8 @@ def test_barrier(self): builder.barrier(DriveChannel(0), DriveChannel(1), ControlChannel(2)) self.assert_roundtrip_equal(test_sched) - def test_area_barrier(self): - """Test area barrier.""" + def test_time_blockade(self): + """Test time blockade.""" with builder.build() as test_sched: builder.append_instruction(TimeBlockade(10, DriveChannel(0))) self.assert_roundtrip_equal(test_sched) From 37f4cbd1798db25f755c3051dd9c0189471a3fd6 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Tue, 25 Oct 2022 18:34:24 +0900 Subject: [PATCH 12/16] lint fix --- qiskit/pulse/instructions/directives.py | 4 ++-- qiskit/qpy/type_keys.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qiskit/pulse/instructions/directives.py b/qiskit/pulse/instructions/directives.py index 0a9be342a75b..4ca5656ad3a7 100644 --- a/qiskit/pulse/instructions/directives.py +++ b/qiskit/pulse/instructions/directives.py @@ -11,12 +11,12 @@ # that they have been altered from the originals. """Directives are hints to the pulse compiler for how to process its input programs.""" -import numpy as np from abc import ABC from typing import Optional, Tuple from qiskit.pulse import channels as chans from qiskit.pulse.instructions import instruction +from qiskit.pulse.exceptions import PulseError class Directive(instruction.Instruction, ABC): @@ -123,7 +123,7 @@ def _validate(self): PulseError: If the input ``duration`` is not integer value. """ if not isinstance(self.duration, int): - raise TypeError( + raise PulseError( "TimeBlockade duration cannot be parameterized. Specify an integer duration value." ) diff --git a/qiskit/qpy/type_keys.py b/qiskit/qpy/type_keys.py index 4355f8071615..53093b96dbb7 100644 --- a/qiskit/qpy/type_keys.py +++ b/qiskit/qpy/type_keys.py @@ -232,7 +232,7 @@ class ScheduleInstruction(TypeKeyBase): SET_PHASE = b"q" SHIFT_PHASE = b"r" BARRIER = b"b" - TimeBlockade = b"t" + TIME_BLOCKADE = b"t" # 's' is reserved by ScheduleBlock, i.e. block can be nested as an element. # Call instructon is not supported by QPY. @@ -260,7 +260,7 @@ def assign(cls, obj): if isinstance(obj, RelativeBarrier): return cls.BARRIER if isinstance(obj, TimeBlockade): - return cls.TimeBlockade + return cls.TIME_BLOCKADE raise exceptions.QpyError( f"Object type '{type(obj)}' is not supported in {cls.__name__} namespace." @@ -284,7 +284,7 @@ def retrieve(cls, type_key): return ShiftPhase if type_key == cls.BARRIER: return RelativeBarrier - if type_key == cls.TimeBlockade: + if type_key == cls.TIME_BLOCKADE: return TimeBlockade raise exceptions.QpyError( From 1504aaf3a5178b58667c7a029a26ffbd09d4185d Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Fri, 28 Oct 2022 16:27:37 +0900 Subject: [PATCH 13/16] add release note for bugfix --- ...pgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml b/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml index cc24e88f7a2e..9f1917448afe 100644 --- a/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml +++ b/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml @@ -28,3 +28,7 @@ upgrade: and :class:`~qiskit.transpiler.passes.RZXCalibrationBuilderNoEcho` transpiler pass have been upgraded to generate :class:`.ScheduleBlock`. This change guarantees the transpiled circuits are always QPY compatible. +fixes: + - | + The ECR pulse sequence misalignment of :class:`~qiskit.transpiler.passes.RZXCalibrationBuilder` + pass has been fixed. See Qiskit/qiskit-terra/#9013 for details. From 1756e35a9d7ed57101a934ccf0eb6148ba57a31c Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Mon, 14 Nov 2022 14:20:59 +0900 Subject: [PATCH 14/16] review comments - render class docs of the directive instructions - update qpy format documentation - add separate reno for time blockade --- qiskit/pulse/instructions/__init__.py | 2 ++ qiskit/qpy/__init__.py | 1 + ...add-timeblockade-instruction-9469a5e9e0218adc.yaml | 10 ++++++++++ ...ulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml | 11 ----------- 4 files changed, 13 insertions(+), 11 deletions(-) create mode 100644 releasenotes/notes/add-timeblockade-instruction-9469a5e9e0218adc.yaml diff --git a/qiskit/pulse/instructions/__init__.py b/qiskit/pulse/instructions/__init__.py index 9fab29a27c87..3e0de5ab6f29 100644 --- a/qiskit/pulse/instructions/__init__.py +++ b/qiskit/pulse/instructions/__init__.py @@ -45,11 +45,13 @@ Reference Delay Play + RelativeBarrier SetFrequency ShiftFrequency SetPhase ShiftPhase Snapshot + TimeBlockade These are all instances of the same base class: diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 9df5806e5f4e..f4279a9b73dc 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -207,6 +207,7 @@ - ``q``: :class:`~qiskit.pulse.instructions.SetPhase` instruction - ``r``: :class:`~qiskit.pulse.instructions.ShiftPhase` instruction - ``b``: :class:`~qiskit.pulse.instructions.RelativeBarrier` instruction +- ``t``: :class:`~qiskit.pulse.instructions.TimeBlockade` instruction The operands of these instances can be serialized through the standard QPY value serialization mechanism, however there are special object types that only appear in the schedule operands. diff --git a/releasenotes/notes/add-timeblockade-instruction-9469a5e9e0218adc.yaml b/releasenotes/notes/add-timeblockade-instruction-9469a5e9e0218adc.yaml new file mode 100644 index 000000000000..ff458906a46e --- /dev/null +++ b/releasenotes/notes/add-timeblockade-instruction-9469a5e9e0218adc.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + New pulse directive :class:`~qiskit.pulse.instructions.TimeBlockade` has been added. + This instruction is QPY compatible. This directive behaves almost identically to + the delay instruction, but will be removed before execution. + This directive is intended to be used internally within the pulse builder + and helps :class:`.ScheduleBlock` represent instructions with + absolute time intervals. This allows the pulse builder to convert + :class:`Schedule` into :class:`ScheduleBlock`, rather than wrapping with call instruction. diff --git a/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml b/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml index 9f1917448afe..7b44cad9a0c0 100644 --- a/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml +++ b/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml @@ -1,15 +1,4 @@ --- -features: - - | - New pulse directive :class:`~qiskit.pulse.instructions.TimeBlockade` has been added. - This instruction is QPY compatible. This directive behaves almost identically to - delay instruction, but will be removed before execution. - This directive is intended to be used internally within the pulse builder - and helps :class:`.ScheduleBlock` represent instructions with - absolute time intervals. This allows the pulse builder to convert - :class:`Schedule` into :class:`ScheduleBlock`, rather than wrapping with call instruction. - - | - QPY dump and load now support :class:`~qiskit.pulse.instructions.TimeBlockade` instruction. upgrade: - | The behavior of the pulse builder when a :class:`.Schedule` is called From b17595e08f1de66ee39bcb240493385d2039270d Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Tue, 22 Nov 2022 12:51:45 +0900 Subject: [PATCH 15/16] more detailed upgrade notes --- ...de-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml b/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml index 7b44cad9a0c0..8023d54b383f 100644 --- a/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml +++ b/releasenotes/notes/upgrade-pulse-builder-and-rzx-builder-033ac8ad8ad2a192.yaml @@ -12,11 +12,19 @@ upgrade: However the :class:`~qiskit.pulse.instructions.Call` instruction and :class:`.Schedule` no longer appear in the builder's pulse program. This change guarantees the generated schedule blocks are always QPY compatible. + If you are filtering the output schedule instructions by :class:`~qiskit.pulse.instructions.Call`, + you can access to the :attr:`.ScheduleBlock.references` instead to retrieve the called program. - | :class:`~qiskit.transpiler.passes.RZXCalibrationBuilder` and :class:`~qiskit.transpiler.passes.RZXCalibrationBuilderNoEcho` transpiler pass have been upgraded to generate :class:`.ScheduleBlock`. This change guarantees the transpiled circuits are always QPY compatible. + If you are directly using :meth:`~qiskit.transpiler.passes.RZXCalibrationBuilder.rescale_cr_inst`, + method from another program or a pass subclass to rescale cross resonance pulse of the device, + now this method is turned into a pulse builder macro, and you need to use this method + within the pulse builder context to adopts to new release. + The method call injects a play instruction to the context pulse program, + instead of returning a :class:`.Play` instruction with the stretched pulse. fixes: - | The ECR pulse sequence misalignment of :class:`~qiskit.transpiler.passes.RZXCalibrationBuilder` From 8562d98393b5c8a55b49f3d601f04314f261c17c Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 23 Nov 2022 02:18:59 +0900 Subject: [PATCH 16/16] add new unittests. rescale_cr_inst method is updated so that it becomes more robust to the rounding error. --- .../passes/calibration/rzx_builder.py | 58 ++-- test/python/qpy/test_circuit_load_from_qpy.py | 69 ++++ .../transpiler/test_calibrationbuilder.py | 302 +++++++++++++----- 3 files changed, 316 insertions(+), 113 deletions(-) create mode 100644 test/python/qpy/test_circuit_load_from_qpy.py diff --git a/qiskit/transpiler/passes/calibration/rzx_builder.py b/qiskit/transpiler/passes/calibration/rzx_builder.py index a02a37ad1e09..8b1312a37d0c 100644 --- a/qiskit/transpiler/passes/calibration/rzx_builder.py +++ b/qiskit/transpiler/passes/calibration/rzx_builder.py @@ -13,8 +13,8 @@ """RZX calibration builders.""" import enum -import math import warnings +from math import pi, erf # pylint: disable=no-name-in-module from typing import List, Tuple, Union import numpy as np @@ -126,37 +126,37 @@ def rescale_cr_inst(instruction: Play, theta: float, sample_mult: int = 16) -> i raise QiskitError("Target rotation angle is not assigned.") from ex # This method is called for instructions which are guaranteed to play GaussianSquare pulse - amp = instruction.pulse.amp - width = instruction.pulse.width - sigma = instruction.pulse.sigma - n_sigmas = (instruction.pulse.duration - width) / sigma + params = instruction.pulse.parameters.copy() + risefall_sigma_ratio = (params["duration"] - params["width"]) / params["sigma"] # The error function is used because the Gaussian may have chopped tails. - gaussian_area = abs(amp) * sigma * np.sqrt(2 * np.pi) * math.erf(n_sigmas) - area = gaussian_area + abs(amp) * width - target_area = abs(theta) / (np.pi / 2.0) * area - - if target_area > gaussian_area: - width = (target_area - gaussian_area) / abs(amp) - duration = round((width + n_sigmas * sigma) / sample_mult) * sample_mult - stretched_pulse = GaussianSquare( - duration=duration, - amp=np.sign(theta) * amp, - width=width, - sigma=sigma, - ) + # Area is normalized by amplitude. + # This makes widths robust to the rounding error. + risefall_area = params["sigma"] * np.sqrt(2 * pi) * erf(risefall_sigma_ratio) + full_area = params["width"] + risefall_area + + # Get estimate of target area. Assume this is pi/2 controlled rotation. + cal_angle = pi / 2 + target_area = abs(theta) / cal_angle * full_area + new_width = target_area - risefall_area + + if new_width >= 0: + width = new_width + params["amp"] *= np.sign(theta) else: - amp_scale = np.sign(theta) * target_area / gaussian_area - duration = round(n_sigmas * sigma / sample_mult) * sample_mult - stretched_pulse = GaussianSquare( - duration=duration, - amp=amp * amp_scale, - width=0, - sigma=sigma, - ) + width = 0 + params["amp"] *= np.sign(theta) * target_area / risefall_area + + round_duration = ( + round((width + risefall_sigma_ratio * params["sigma"]) / sample_mult) * sample_mult + ) + params["duration"] = round_duration + params["width"] = width + + stretched_pulse = GaussianSquare(**params) builder.play(stretched_pulse, instruction.channel) - return duration + return round_duration def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, ScheduleBlock]: """Builds the calibration schedule for the RZXGate(theta) with echos. @@ -219,9 +219,9 @@ def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, # The direction is not native. Add Hadamard gates to flip the direction. xgate = self._inst_map.get("x", qubits[1]) - szc = self._inst_map.get("rz", qubits[1], np.pi / 2) + szc = self._inst_map.get("rz", qubits[1], pi / 2) sxc = self._inst_map.get("sx", qubits[1]) - szt = self._inst_map.get("rz", qubits[0], np.pi / 2) + szt = self._inst_map.get("rz", qubits[0], pi / 2) sxt = self._inst_map.get("sx", qubits[0]) with builder.build(name="hadamard") as hadamard: # Control qubit diff --git a/test/python/qpy/test_circuit_load_from_qpy.py b/test/python/qpy/test_circuit_load_from_qpy.py new file mode 100644 index 000000000000..4174e59c792d --- /dev/null +++ b/test/python/qpy/test_circuit_load_from_qpy.py @@ -0,0 +1,69 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Test cases for the schedule block qpy loading and saving.""" + +import io + +from ddt import ddt, data + +from qiskit.circuit import QuantumCircuit +from qiskit.providers.fake_provider import FakeHanoi +from qiskit.qpy import dump, load +from qiskit.test import QiskitTestCase +from qiskit.transpiler import PassManager +from qiskit.transpiler import passes + + +class QpyCircuitTestCase(QiskitTestCase): + """QPY schedule testing platform.""" + + def assert_roundtrip_equal(self, circuit): + """QPY roundtrip equal test.""" + qpy_file = io.BytesIO() + dump(circuit, qpy_file) + qpy_file.seek(0) + new_circuit = load(qpy_file)[0] + + self.assertEqual(circuit, new_circuit) + + +@ddt +class TestCalibrationPasses(QpyCircuitTestCase): + """QPY round-trip test case of transpiled circuits with pulse level optimization.""" + + def setUp(self): + super().setUp() + # This backend provides CX(0,1) with native ECR direction. + self.inst_map = FakeHanoi().defaults().instruction_schedule_map + + @data(0.1, 0.7, 1.5) + def test_rzx_calibration(self, angle): + """RZX builder calibration pass with echo.""" + pass_ = passes.RZXCalibrationBuilder(self.inst_map) + pass_manager = PassManager(pass_) + test_qc = QuantumCircuit(2) + test_qc.rzx(angle, 0, 1) + rzx_qc = pass_manager.run(test_qc) + + self.assert_roundtrip_equal(rzx_qc) + + @data(0.1, 0.7, 1.5) + def test_rzx_calibration_echo(self, angle): + """RZX builder calibration pass without echo.""" + pass_ = passes.RZXCalibrationBuilderNoEcho(self.inst_map) + pass_manager = PassManager(pass_) + test_qc = QuantumCircuit(2) + test_qc.rzx(angle, 0, 1) + rzx_qc = pass_manager.run(test_qc) + + self.assert_roundtrip_equal(rzx_qc) diff --git a/test/python/transpiler/test_calibrationbuilder.py b/test/python/transpiler/test_calibrationbuilder.py index 599e0a52869c..f70c2b942851 100644 --- a/test/python/transpiler/test_calibrationbuilder.py +++ b/test/python/transpiler/test_calibrationbuilder.py @@ -12,26 +12,26 @@ """Test the RZXCalibrationBuilderNoEcho.""" -from math import erf, pi +from math import pi, erf # pylint: disable=no-name-in-module import numpy as np from ddt import data, ddt from qiskit import circuit, schedule -from qiskit.pulse import builder +from qiskit.circuit.library.standard_gates import SXGate, RZGate +from qiskit.providers.fake_provider import FakeHanoi from qiskit.pulse import ( ControlChannel, - Delay, DriveChannel, GaussianSquare, Waveform, Play, - ShiftPhase, InstructionScheduleMap, Schedule, ) +from qiskit.pulse import builder +from qiskit.pulse.transforms import target_qobj_transform from qiskit.test import QiskitTestCase -from qiskit.providers.fake_provider import FakeAthens from qiskit.transpiler import PassManager from qiskit.transpiler.passes.calibration.builders import ( RZXCalibrationBuilder, @@ -42,38 +42,194 @@ class TestCalibrationBuilder(QiskitTestCase): """Test the Calibration Builder.""" + # CR parameters + __risefall = 4 + __angle = np.pi / 2 + __granularity = 16 + def setUp(self): super().setUp() - self.backend = FakeAthens() + # This backend provides CX(0,1) with native ECR direction. + self.backend = FakeHanoi() self.inst_map = self.backend.defaults().instruction_schedule_map + # Get positive CR tone + def _get_cr(time_inst, name): + return isinstance(time_inst[1], Play) and time_inst[1].pulse.name.startswith(name) + + cx_sched = self.inst_map.get("cx", (0, 1)) + + # CR tone + self.u0p_play = cx_sched.filter(lambda elm: _get_cr(elm, "CR90p_u")).instructions[0][1] + self.u0m_play = cx_sched.filter(lambda elm: _get_cr(elm, "CR90m_u")).instructions[0][1] + + # Rotary tone + self.d1p_play = cx_sched.filter(lambda elm: _get_cr(elm, "CR90p_d")).instructions[0][1] + self.d1m_play = cx_sched.filter(lambda elm: _get_cr(elm, "CR90m_d")).instructions[0][1] + + def compute_stretch_duration(self, play_gaussian_square_pulse, theta): + """Compute duration of stretched Gaussian Square pulse.""" + pulse = play_gaussian_square_pulse.pulse + sigma = pulse.sigma + width = self.compute_stretch_width(play_gaussian_square_pulse, theta) + + duration = width + sigma * self.__risefall + return round(duration / self.__granularity) * self.__granularity + + def compute_stretch_width(self, play_gaussian_square_pulse, theta): + """Compute width of stretched Gaussian Square pulse.""" + pulse = play_gaussian_square_pulse.pulse + sigma = pulse.sigma + width = pulse.width + + risefall_area = sigma * np.sqrt(2 * np.pi) * erf(self.__risefall) + full_area = risefall_area + width + + target_area = abs(theta) / self.__angle * full_area + return max(0, target_area - risefall_area) + @ddt class TestRZXCalibrationBuilder(TestCalibrationBuilder): """Test RZXCalibrationBuilder.""" - @data(np.pi / 4, np.pi / 2, np.pi) - def test_rzx_calibration_builder_duration(self, theta: float): - """Test that pulse durations are computed correctly.""" - width = 512.00000001 - sigma = 64 - n_sigmas = 4 - duration = width + n_sigmas * sigma - sample_mult = 16 - amp = 1.0 - pulse = GaussianSquare(duration=duration, amp=amp, sigma=sigma, width=width) - instruction = Play(pulse, ControlChannel(1)) - with builder.build(): - # this is builder macro to play stretched pulse. need builder context. - test_duration = RZXCalibrationBuilder.rescale_cr_inst( - instruction=instruction, theta=theta, sample_mult=sample_mult - ) - gaussian_area = abs(amp) * sigma * np.sqrt(2 * np.pi) * erf(n_sigmas) - area = gaussian_area + abs(amp) * width - target_area = abs(theta) / (np.pi / 2.0) * area - width = (target_area - gaussian_area) / abs(amp) - expected_duration = round((width + n_sigmas * sigma) / sample_mult) * sample_mult - self.assertEqual(test_duration, expected_duration) + @data(-np.pi / 4, 0.1, np.pi / 4, np.pi / 2, np.pi) + def test_rzx_calibration_cr_pulse_stretch(self, theta: float): + """Test that cross resonance pulse durations are computed correctly.""" + with builder.build() as test_sched: + RZXCalibrationBuilder.rescale_cr_inst(self.u0p_play, theta) + + self.assertEqual(test_sched.duration, self.compute_stretch_duration(self.u0p_play, theta)) + + @data(-np.pi / 4, 0.1, np.pi / 4, np.pi / 2, np.pi) + def test_rzx_calibration_rotary_pulse_stretch(self, theta: float): + """Test that rotary pulse durations are computed correctly.""" + with builder.build() as test_sched: + RZXCalibrationBuilder.rescale_cr_inst(self.d1p_play, theta) + + self.assertEqual(test_sched.duration, self.compute_stretch_duration(self.d1p_play, theta)) + + def test_native_cr(self): + """Test that correct pulse sequence is generated for native CR pair.""" + # Sufficiently large angle to avoid minimum duration, i.e. amplitude rescaling + theta = np.pi / 4 + + qc = circuit.QuantumCircuit(2) + qc.rzx(theta, 0, 1) + + _pass = RZXCalibrationBuilder(self.inst_map) + test_qc = PassManager(_pass).run(qc) + + duration = self.compute_stretch_duration(self.u0p_play, theta) + with builder.build( + self.backend, + default_alignment="sequential", + default_transpiler_settings={"optimization_level": 0}, + ) as ref_sched: + with builder.align_left(): + # Positive CRs + u0p_params = self.u0p_play.pulse.parameters + u0p_params["duration"] = duration + u0p_params["width"] = self.compute_stretch_width(self.u0p_play, theta) + builder.play( + GaussianSquare(**u0p_params), + ControlChannel(0), + ) + d1p_params = self.d1p_play.pulse.parameters + d1p_params["duration"] = duration + d1p_params["width"] = self.compute_stretch_width(self.d1p_play, theta) + builder.play( + GaussianSquare(**d1p_params), + DriveChannel(1), + ) + builder.x(0) + with builder.align_left(): + # Negative CRs + u0m_params = self.u0m_play.pulse.parameters + u0m_params["duration"] = duration + u0m_params["width"] = self.compute_stretch_width(self.u0m_play, theta) + builder.play( + GaussianSquare(**u0m_params), + ControlChannel(0), + ) + d1m_params = self.d1m_play.pulse.parameters + d1m_params["duration"] = duration + d1m_params["width"] = self.compute_stretch_width(self.d1m_play, theta) + builder.play( + GaussianSquare(**d1m_params), + DriveChannel(1), + ) + builder.x(0) + + self.assertEqual(schedule(test_qc, self.backend), target_qobj_transform(ref_sched)) + + def test_non_native_cr(self): + """Test that correct pulse sequence is generated for non-native CR pair.""" + # Sufficiently large angle to avoid minimum duration, i.e. amplitude rescaling + theta = np.pi / 4 + + qc = circuit.QuantumCircuit(2) + qc.rzx(theta, 1, 0) + + _pass = RZXCalibrationBuilder(self.inst_map) + test_qc = PassManager(_pass).run(qc) + + duration = self.compute_stretch_duration(self.u0p_play, theta) + with builder.build( + self.backend, + default_alignment="sequential", + default_transpiler_settings={"optimization_level": 0}, + ) as ref_sched: + # Hadamard gates + builder.call_gate(RZGate(np.pi / 2), qubits=(0,)) + builder.call_gate(SXGate(), qubits=(0,)) + builder.call_gate(RZGate(np.pi / 2), qubits=(0,)) + builder.call_gate(RZGate(np.pi / 2), qubits=(1,)) + builder.call_gate(SXGate(), qubits=(1,)) + builder.call_gate(RZGate(np.pi / 2), qubits=(1,)) + with builder.align_left(): + # Positive CRs + u0p_params = self.u0p_play.pulse.parameters + u0p_params["duration"] = duration + u0p_params["width"] = self.compute_stretch_width(self.u0p_play, theta) + builder.play( + GaussianSquare(**u0p_params), + ControlChannel(0), + ) + d1p_params = self.d1p_play.pulse.parameters + d1p_params["duration"] = duration + d1p_params["width"] = self.compute_stretch_width(self.d1p_play, theta) + builder.play( + GaussianSquare(**d1p_params), + DriveChannel(1), + ) + builder.x(0) + with builder.align_left(): + # Negative CRs + u0m_params = self.u0m_play.pulse.parameters + u0m_params["duration"] = duration + u0m_params["width"] = self.compute_stretch_width(self.u0m_play, theta) + builder.play( + GaussianSquare(**u0m_params), + ControlChannel(0), + ) + d1m_params = self.d1m_play.pulse.parameters + d1m_params["duration"] = duration + d1m_params["width"] = self.compute_stretch_width(self.d1m_play, theta) + builder.play( + GaussianSquare(**d1m_params), + DriveChannel(1), + ) + builder.x(0) + # Hadamard gates + builder.call_gate(RZGate(np.pi / 2), qubits=(0,)) + builder.call_gate(SXGate(), qubits=(0,)) + builder.call_gate(RZGate(np.pi / 2), qubits=(0,)) + builder.call_gate(RZGate(np.pi / 2), qubits=(1,)) + builder.call_gate(SXGate(), qubits=(1,)) + builder.call_gate(RZGate(np.pi / 2), qubits=(1,)) + + self.assertEqual(schedule(test_qc, self.backend), target_qobj_transform(ref_sched)) def test_pass_alive_with_dcx_ish(self): """Test if the pass is not terminated by error with direct CX input.""" @@ -103,63 +259,41 @@ def test_pass_alive_with_dcx_ish(self): class TestRZXCalibrationBuilderNoEcho(TestCalibrationBuilder): """Test RZXCalibrationBuilderNoEcho.""" - def test_rzx_calibration_builder(self): - """Test whether RZXCalibrationBuilderNoEcho scales pulses correctly.""" - - # Define a circuit with one RZX gate and an angle theta. - theta = pi / 3 - rzx_qc = circuit.QuantumCircuit(2) - rzx_qc.rzx(theta / 2, 1, 0) - - # Verify that there are no calibrations for this circuit yet. - self.assertEqual(rzx_qc.calibrations, {}) + def test_native_cr(self): + """Test that correct pulse sequence is generated for native CR pair. + + .. notes:: + No echo builder only supports native direction. + """ + # Sufficiently large angle to avoid minimum duration, i.e. amplitude rescaling + theta = np.pi / 4 + + qc = circuit.QuantumCircuit(2) + qc.rzx(theta, 0, 1) + + _pass = RZXCalibrationBuilderNoEcho(self.inst_map) + test_qc = PassManager(_pass).run(qc) + + duration = self.compute_stretch_duration(self.u0p_play, 2.0 * theta) + with builder.build() as ref_sched: + # Positive CRs + u0p_params = self.u0p_play.pulse.parameters + u0p_params["duration"] = duration + u0p_params["width"] = self.compute_stretch_width(self.u0p_play, 2.0 * theta) + builder.play( + GaussianSquare(**u0p_params), + ControlChannel(0), + ) + d1p_params = self.d1p_play.pulse.parameters + d1p_params["duration"] = duration + d1p_params["width"] = self.compute_stretch_width(self.d1p_play, 2.0 * theta) + builder.play( + GaussianSquare(**d1p_params), + DriveChannel(1), + ) + builder.delay(duration, DriveChannel(0)) - # apply the RZXCalibrationBuilderNoEcho. - pass_ = RZXCalibrationBuilderNoEcho( - instruction_schedule_map=self.backend.defaults().instruction_schedule_map - ) - cal_qc = PassManager(pass_).run(rzx_qc) - rzx_qc_duration = schedule(cal_qc, self.backend).duration - - # Check that the calibrations contain the correct instructions - # and pulses on the correct channels. - rzx_qc_instructions = cal_qc.calibrations["rzx"][((1, 0), (theta / 2,))].instructions - self.assertEqual(rzx_qc_instructions[0][1].channel, DriveChannel(0)) - self.assertTrue(isinstance(rzx_qc_instructions[0][1], Play)) - self.assertEqual(rzx_qc_instructions[0][1].pulse.pulse_type, "GaussianSquare") - self.assertEqual(rzx_qc_instructions[1][1].channel, DriveChannel(1)) - self.assertTrue(isinstance(rzx_qc_instructions[1][1], Delay)) - self.assertEqual(rzx_qc_instructions[2][1].channel, ControlChannel(1)) - self.assertTrue(isinstance(rzx_qc_instructions[2][1], Play)) - self.assertEqual(rzx_qc_instructions[2][1].pulse.pulse_type, "GaussianSquare") - - # Calculate the duration of one scaled Gaussian square pulse from the CX gate. - cx_sched = self.inst_map.get("cx", qubits=(1, 0)) - - crs = [] - for time, inst in cx_sched.instructions: - - # Identify the CR pulses. - if isinstance(inst, Play) and not isinstance(inst, ShiftPhase): - if isinstance(inst.channel, ControlChannel): - crs.append((time, inst)) - - pulse_ = crs[0][1].pulse - amp = pulse_.amp - width = pulse_.width - sigma = pulse_.sigma - n_sigmas = (pulse_.duration - width) / sigma - sample_mult = 16 - - gaussian_area = abs(amp) * sigma * np.sqrt(2 * np.pi) * erf(n_sigmas) - area = gaussian_area + abs(amp) * width - target_area = abs(theta) / (np.pi / 2.0) * area - width = (target_area - gaussian_area) / abs(amp) - duration = round((width + n_sigmas * sigma) / sample_mult) * sample_mult - - # Check whether the durations of the RZX pulse and - # the scaled CR pulse from the CX gate match. - self.assertEqual(rzx_qc_duration, duration) + self.assertEqual(schedule(test_qc, self.backend), target_qobj_transform(ref_sched)) def test_pulse_amp_typecasted(self): """Test if scaled pulse amplitude is complex type."""