From 805a5b27133b75940550dd19b8ba89d77dae3473 Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Tue, 18 Oct 2022 00:45:58 +0900 Subject: [PATCH] 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