diff --git a/qiskit/assembler/assemble_schedules.py b/qiskit/assembler/assemble_schedules.py index cb9f935dcca7..5474b3223407 100644 --- a/qiskit/assembler/assemble_schedules.py +++ b/qiskit/assembler/assemble_schedules.py @@ -26,7 +26,8 @@ def assemble_schedules( - schedules: List[Union['schedule.ScheduleComponent', + schedules: List[Union['schedule.ScheduleBlock', + 'schedule.ScheduleComponent', Tuple[int, 'schedule.ScheduleComponent']]], qobj_id: int, qobj_header: qobj.QobjHeader, @@ -97,15 +98,7 @@ def _assemble_experiments( instruction_converter = instruction_converter(qobj.PulseQobjInstruction, **run_config.to_dict()) - formatted_schedules = [] - for sched in schedules: - if isinstance(sched, pulse.Schedule): - sched = transforms.inline_subroutines(sched) - sched = transforms.flatten(sched) - formatted_schedules.append(sched) - else: - formatted_schedules.append(pulse.Schedule(sched)) - + formatted_schedules = [transforms.target_qobj_transform(sched) for sched in schedules] compressed_schedules = transforms.compress_pulses(formatted_schedules) user_pulselib = {} @@ -158,7 +151,7 @@ def _assemble_experiments( def _assemble_instructions( - sched: pulse.Schedule, + sched: Union[pulse.Schedule, pulse.ScheduleBlock], instruction_converter: converters.InstructionToQobjConverter, run_config: RunConfig, user_pulselib: Dict[str, List[complex]] @@ -180,6 +173,8 @@ def _assemble_instructions( A list of converted instructions, the user pulse library dictionary (from pulse name to pulse samples), and the maximum number of readout memory slots used by this Schedule. """ + sched = transforms.target_qobj_transform(sched) + max_memory_slot = 0 qobj_instructions = [] diff --git a/qiskit/compiler/assembler.py b/qiskit/compiler/assembler.py index 1a4d88d5398f..202b9e72acb0 100644 --- a/qiskit/compiler/assembler.py +++ b/qiskit/compiler/assembler.py @@ -11,24 +11,25 @@ # that they have been altered from the originals. """Assemble function for converting a list of circuits into a qobj""" -import uuid import copy import logging +import uuid import warnings from time import time from typing import Union, List, Dict, Optional + +from qiskit.assembler import assemble_circuits, assemble_schedules +from qiskit.assembler.run_config import RunConfig from qiskit.circuit import QuantumCircuit, Qubit, Parameter from qiskit.exceptions import QiskitError +from qiskit.providers import BaseBackend +from qiskit.providers.backend import Backend from qiskit.pulse import LoConfig, Instruction -from qiskit.assembler.run_config import RunConfig -from qiskit.assembler import assemble_circuits, assemble_schedules +from qiskit.pulse import Schedule, ScheduleBlock +from qiskit.pulse.channels import PulseChannel from qiskit.qobj import QobjHeader, Qobj from qiskit.qobj.utils import MeasLevel, MeasReturnType from qiskit.validation.jsonschema import SchemaValidationError -from qiskit.providers import BaseBackend -from qiskit.providers.backend import Backend -from qiskit.pulse.channels import PulseChannel -from qiskit.pulse import Schedule logger = logging.getLogger(__name__) @@ -39,7 +40,9 @@ def _log_assembly_time(start_time, end_time): # TODO: parallelize over the experiments (serialize each separately, then add global header/config) -def assemble(experiments: Union[QuantumCircuit, List[QuantumCircuit], Schedule, List[Schedule]], +def assemble(experiments: Union[QuantumCircuit, List[QuantumCircuit], + Schedule, List[Schedule], + ScheduleBlock, Union[ScheduleBlock]], backend: Optional[Union[Backend, BaseBackend]] = None, qobj_id: Optional[str] = None, qobj_header: Optional[Union[QobjHeader, Dict]] = None, @@ -154,7 +157,7 @@ def assemble(experiments: Union[QuantumCircuit, List[QuantumCircuit], Schedule, return assemble_circuits(circuits=bound_experiments, qobj_id=qobj_id, qobj_header=qobj_header, run_config=run_config) - elif all(isinstance(exp, (Schedule, Instruction)) for exp in experiments): + elif all(isinstance(exp, (ScheduleBlock, Schedule, Instruction)) for exp in experiments): run_config = _parse_pulse_args(backend, qubit_lo_freq, meas_lo_freq, qubit_lo_range, meas_lo_range, schedule_los, meas_level, meas_return, diff --git a/qiskit/pulse/builder.py b/qiskit/pulse/builder.py index 6f2a2bdee43c..60e0fb27ef83 100644 --- a/qiskit/pulse/builder.py +++ b/qiskit/pulse/builder.py @@ -227,9 +227,11 @@ macros, library, transforms, + utils ) from qiskit.pulse.instructions import directives -from qiskit.pulse.schedule import Schedule +from qiskit.pulse.schedule import Schedule, ScheduleBlock +from qiskit.pulse.transforms.alignments import AlignmentKind #: contextvars.ContextVar[BuilderContext]: active builder BUILDER_CONTEXTVAR = contextvars.ContextVar("backend") @@ -267,11 +269,17 @@ def wrapper(self, *args, **kwargs): class _PulseBuilder(): """Builder context class.""" + __alignment_kinds__ = { + 'left': transforms.AlignLeft(), + 'right': transforms.AlignRight(), + 'sequential': transforms.AlignSequential() + } + def __init__(self, backend=None, - schedule: Optional[Schedule] = None, + block: Optional[ScheduleBlock] = None, name: Optional[str] = None, - default_alignment: Union[str, Callable] = 'left', + default_alignment: Union[str, AlignmentKind] = 'left', default_transpiler_settings: Mapping = None, default_circuit_scheduler_settings: Mapping = None): """Initialize the builder context. @@ -286,16 +294,17 @@ def __init__(self, Args: backend (Union[Backend, BaseBackend]): Input backend to use in builder. If not set certain functionality will be unavailable. - schedule: Initital schedule to build on. If not supplied - a schedule will be created. - name: Name of pulse program to be built. Only used if `schedule` - is not ``None``. - default_alignment: Default scheduling alignment policy for the - builder. One of 'left', 'right', 'sequential', or an alignment - contextmanager. + block: Initital ``ScheduleBlock`` to build on. + name: Name of pulse program to be built. + default_alignment: Default scheduling alignment for builder. + One of ``left``, ``right``, ``sequential`` or an instance of + :class:`~qiskit.pulse.transforms.alignments.AlignmentKind` subclass. default_transpiler_settings: Default settings for the transpiler. default_circuit_scheduler_settings: Default settings for the circuit to pulse scheduler. + + Raises: + PulseError: When invalid ``default_alignment`` or `block` is specified. """ #: BaseBackend: Backend instance for context builder. self._backend = backend @@ -303,28 +312,43 @@ def __init__(self, #: Union[None, ContextVar]: Token for this ``_PulseBuilder``'s ``ContextVar``. self._backend_ctx_token = None - #: pulse.Schedule: Active schedule of BuilderContext. - self._context_schedule = None - #: QuantumCircuit: Lazily constructed quantum circuit self._lazy_circuit = None - # ContextManager: Default alignment context. - self._default_alignment_context = _align(default_alignment) - - self._transpiler_settings = default_transpiler_settings or {} - - if default_circuit_scheduler_settings is None: - default_circuit_scheduler_settings = {} - self._circuit_scheduler_settings = \ - default_circuit_scheduler_settings or {} - - # pulse.Schedule: Root program context-schedule - self._schedule = schedule or Schedule(name=name) - - self.set_context_schedule(Schedule()) - - def __enter__(self) -> Schedule: + #: Dict[str, Any]: Transpiler setting dictionary. + self._transpiler_settings = default_transpiler_settings or dict() + + #: Dict[str, Any]: Scheduler setting dictionary. + self._circuit_scheduler_settings = default_circuit_scheduler_settings or dict() + + #: List[ScheduleBlock]: Stack of context. + self._context_stack = [] + + #: str: Name of the output program + self._name = name + + # Add root block if provided. Schedule will be built on top of this. + if block is not None: + if isinstance(block, ScheduleBlock): + root_block = block + elif isinstance(block, Schedule): + root_block = ScheduleBlock() + root_block.append(instructions.Call(subroutine=block)) + else: + raise exceptions.PulseError(f'Input `block` type {block.__class__.__name__} is ' + 'not a valid format. Specify a pulse program.') + self._context_stack.append(root_block) + + # Set default alignment context + alignment = _PulseBuilder.__alignment_kinds__.get(default_alignment, default_alignment) + if not isinstance(alignment, AlignmentKind): + raise exceptions.PulseError(f'Given `default_alignment` {repr(default_alignment)} is ' + 'not a valid transformation. Set one of ' + f'{", ".join(_PulseBuilder.__alignment_kinds__.keys())}, ' + 'or set an instance of `AlignmentKind` subclass.') + self.push_context(alignment) + + def __enter__(self) -> ScheduleBlock: """Enter this builder context and yield either the supplied schedule or the schedule created for the user. @@ -332,13 +356,14 @@ def __enter__(self) -> Schedule: The schedule that the builder will build on. """ self._backend_ctx_token = BUILDER_CONTEXTVAR.set(self) - self._default_alignment_context.__enter__() - return self._schedule + output = self._context_stack[0] + output._name = self._name or output.name + + return output @_compile_lazy_circuit_before def __exit__(self, exc_type, exc_val, exc_tb): """Exit the builder context and compile the built pulse program.""" - self._default_alignment_context.__exit__(exc_type, exc_val, exc_tb) self.compile() BUILDER_CONTEXTVAR.reset(self._backend_ctx_token) @@ -351,10 +376,27 @@ def backend(self): """ return self._backend - @property - def context_schedule(self) -> Schedule: - """Return the current context schedule.""" - return self._context_schedule + @_compile_lazy_circuit_before + def push_context(self, alignment: AlignmentKind): + """Push new context to the stack.""" + self._context_stack.append(ScheduleBlock(alignment_context=alignment)) + + @_compile_lazy_circuit_before + def pop_context(self) -> ScheduleBlock: + """Pop the last context from the stack.""" + if len(self._context_stack) == 1: + raise exceptions.PulseError('The root context cannot be popped out.') + + return self._context_stack.pop() + + def get_context(self) -> ScheduleBlock: + """Get current context. + + Notes: + New instruction can be added by `.append_block` or `.append_instruction` method. + Use above methods rather than directly accessing to the current context. + """ + return self._context_stack[-1] @property @_requires_backend @@ -385,37 +427,17 @@ def circuit_scheduler_settings(self, settings: Mapping): self._circuit_scheduler_settings = settings @_compile_lazy_circuit_before - def compile(self) -> Schedule: + def compile(self) -> ScheduleBlock: """Compile and output the built pulse program.""" # Not much happens because we currently compile as we build. # This should be offloaded to a true compilation module # once we define a more sophisticated IR. - program = self._schedule.append(self.context_schedule, inplace=True) - self.set_context_schedule(Schedule()) - return program - @_compile_lazy_circuit_before - def set_context_schedule(self, context_schedule: Schedule): - """Set the current context's schedule for the builder.""" - self._context_schedule = context_schedule + while len(self._context_stack) > 1: + current = self.pop_context() + self.append_block(current) - @_compile_lazy_circuit_before - def append_schedule(self, context_schedule: Schedule): - """Add a :class:`Schedule` to the builder's context schedule. - - Args: - context_schedule: Schedule to append to the current context schedule. - """ - self.context_schedule.append(context_schedule, inplace=True) - - @_compile_lazy_circuit_before - def append_instruction(self, instruction: instructions.Instruction): - """Add an instruction to the builder's context schedule. - - Args: - instruction: Instruction to append. - """ - self.context_schedule.append(instruction, inplace=True) + return self._context_stack[0] def _compile_lazy_circuit(self): """Call a context QuantumCircuit (lazy circuit) and append the output pulse schedule @@ -426,8 +448,8 @@ def _compile_lazy_circuit(self): if self._lazy_circuit: lazy_circuit = self._lazy_circuit # reset lazy circuit - self._lazy_circuit = self.new_circuit() - self.call_schedule(self._compile_circuit(lazy_circuit)) + self._lazy_circuit = self._new_circuit() + self.call_subroutine(subroutine=self._compile_circuit(lazy_circuit)) def _compile_circuit(self, circ) -> Schedule: """Take a QuantumCircuit and output the pulse schedule associated with the circuit.""" @@ -441,16 +463,32 @@ def _compile_circuit(self, circ) -> Schedule: **self.circuit_scheduler_settings) return sched - def call_schedule(self, schedule: Schedule): - """Call a schedule and append to the builder's context schedule. + def _new_circuit(self): + """Create a new circuit for lazy circuit scheduling.""" + return circuit.QuantumCircuit(self.num_qubits) + + @_compile_lazy_circuit_before + def append_instruction(self, instruction: instructions.Instruction): + """Add an instruction to the builder's context schedule. + + Args: + instruction: Instruction to append. + """ + self._context_stack[-1].append(instruction) + + @_compile_lazy_circuit_before + def append_block(self, context_block: ScheduleBlock): + """Add a :class:`ScheduleBlock` to the builder's context schedule. - The schedule is just appended to the context schedule as-is. - Use :meth:`~call_subroutine` method to define the schedule as a subroutine. + Args: + context_block: ScheduleBlock to append to the current context block. """ - self.append_schedule(schedule) + # ignore empty context + if len(context_block) > 0: + self._context_stack[-1].append(context_block) def call_subroutine(self, - subroutine: Union[circuit.QuantumCircuit, Schedule], + subroutine: Union[circuit.QuantumCircuit, Schedule, ScheduleBlock], name: Optional[str] = None, value_dict: Optional[Dict[ParameterExpression, ParameterValueType]] = None, **kw_params: ParameterValueType): @@ -479,9 +517,6 @@ def call_subroutine(self, subroutine = self._compile_circuit(subroutine) if len(subroutine.instructions) > 0: - if value_dict is None: - value_dict = dict() - param_value_map = dict() for param_name, assigned_value in kw_params.items(): param_objs = subroutine.get_parameters(param_name) @@ -493,60 +528,13 @@ def call_subroutine(self, f'Parameter {param_name} is not defined in the target subroutine. ' f'{", ".join(map(str, subroutine.parameters))} can be specified.') - param_value_map.update(value_dict) + if value_dict: + param_value_map.update(value_dict) + call_def = instructions.Call(subroutine, param_value_map, name) self.append_instruction(call_def) - def new_circuit(self): - """Create a new circuit for lazy circuit scheduling.""" - return circuit.QuantumCircuit(self.num_qubits) - - @_requires_backend - def call_circuit(self, - circ: circuit.QuantumCircuit, - lazy: bool = False): - """Deprecated. - - Call a circuit in the pulse program. - The circuit is assumed to be defined on physical qubits. - - If ``lazy == True`` this circuit will extend a lazily constructed - quantum circuit. When an operation occurs that breaks the underlying - circuit scheduling assumptions such as adding a pulse instruction or - changing the alignment context the circuit will be - transpiled and scheduled into pulses with the current active settings. - - Args: - circ: Circuit to call. - lazy: If false the circuit will be transpiled and pulse scheduled - immediately. Otherwise, it will extend the active lazy circuit - as defined above. - """ - warnings.warn('Calling ``call_circuit`` is being deprecated. ' - 'Use ``call_subroutine`` method instead. ' - 'New method stores the circuit as a ``Call`` instruction ' - 'after scheduling.', DeprecationWarning) - - if lazy: - self._call_circuit(circ) - else: - self._compile_lazy_circuit() - self._call_circuit(circ) - self._compile_lazy_circuit() - - def _call_circuit(self, circ): - # TODO deprecate this with call circuits - if self._lazy_circuit is None: - self._lazy_circuit = self.new_circuit() - - for creg in circ.cregs: - try: - self._lazy_circuit.add_register(creg) - except circuit.exceptions.CircuitError: - pass - self._lazy_circuit.compose(circ, inplace=True) - @_requires_backend def call_gate(self, gate: circuit.Gate, @@ -583,18 +571,18 @@ def call_gate(self, def _call_gate(self, gate, qargs): if self._lazy_circuit is None: - self._lazy_circuit = self.new_circuit() + self._lazy_circuit = self._new_circuit() self._lazy_circuit.append(gate, qargs=qargs) def build(backend=None, - schedule: Optional[Schedule] = None, + schedule: Optional[ScheduleBlock] = None, name: Optional[str] = None, - default_alignment: str = 'left', + default_alignment: Optional[Union[str, AlignmentKind]] = 'left', default_transpiler_settings: Optional[Dict[str, Any]] = None, default_circuit_scheduler_settings: Optional[Dict[str, Any]] = None - ) -> ContextManager[Schedule]: + ) -> ContextManager[ScheduleBlock]: """Create a context manager for launching the imperative pulse builder DSL. To enter a building context and starting building a pulse program: @@ -623,13 +611,10 @@ def build(backend=None, Args: backend (Union[Backend, BaseBackend]): A Qiskit backend. If not supplied certain builder functionality will be unavailable. - schedule: a *mutable* pulse Schedule in which your pulse program will - be built. - name: Name of pulse program to be built. Only used if `schedule` - is not ``None``. + schedule: A pulse ``ScheduleBlock`` in which your pulse program will be built. + name: Name of pulse program to be built. default_alignment: Default scheduling alignment for builder. - One of ``left``, ``right``, ``sequential`` or an alignment - contextmanager. + One of ``left``, ``right``, ``sequential`` or an alignment context. default_transpiler_settings: Default settings for the transpiler. default_circuit_scheduler_settings: Default settings for the circuit to pulse scheduler. @@ -639,7 +624,7 @@ def build(backend=None, """ return _PulseBuilder( backend=backend, - schedule=schedule, + block=schedule, name=name, default_alignment=default_alignment, default_transpiler_settings=default_transpiler_settings, @@ -647,6 +632,7 @@ def build(backend=None, # Builder Utilities + def _active_builder() -> _PulseBuilder: """Get the active builder in the active context. @@ -684,9 +670,23 @@ def active_backend(): return builder -def append_schedule(schedule: Schedule): - """Call a schedule by appending to the active builder's context schedule.""" - _active_builder().append_schedule(schedule) +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. + """ + 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`.') def append_instruction(instruction: instructions.Instruction): @@ -820,8 +820,7 @@ def active_transpiler_settings() -> Dict[str, Any]: return dict(_active_builder().transpiler_settings) -# pylint: disable=invalid-name -def active_circuit_scheduler_settings() -> Dict[str, Any]: +def active_circuit_scheduler_settings() -> Dict[str, Any]: # pylint: disable=invalid-name """Return the current active builder context's circuit scheduler settings. Examples: @@ -845,57 +844,8 @@ def active_circuit_scheduler_settings() -> Dict[str, Any]: # Contexts -def _transform_context(transform: Callable[[Schedule], Schedule], - **transform_kwargs: Any - ) -> Callable[..., ContextManager[None]]: - """A transform context generator, decorator. - - Decorator accepts a transformation function, and then decorates a new - ContextManager function. - When the context is entered it creates a new schedule, sets it as the - builder's context schedule and then yields. - - Finally it will reset the initial builder's context schedule after exiting - the context and apply the decorated transform function to the - context Schedule. The output transformed schedule will then be - appended to the initial builder's context schedule. This effectively builds a stack - of builder's context schedules that is automatically collapsed upon exiting the context. - - Args: - transform: Transform to decorate as context. - transform_kwargs: Additional override keyword arguments for the - decorated transform. - - Returns: - A function that generates a new transformation ``ContextManager``. - """ - def wrap(function): - @functools.wraps(function) - @contextmanager - def wrapped_transform(*args, **kwargs): - builder = _active_builder() - context_schedule = builder.context_schedule - transform_schedule = Schedule() - builder.set_context_schedule(transform_schedule) - try: - yield - finally: - builder._compile_lazy_circuit() - transformed_schedule = transform( - transform_schedule, - *args, - **kwargs, - **transform_kwargs, - ) - builder.set_context_schedule(context_schedule) - builder.append_schedule(transformed_schedule) - return wrapped_transform - - return wrap - - -@_transform_context(transforms.align_left) +@contextmanager def align_left() -> ContextManager[None]: """Left alignment pulse scheduling context. @@ -917,13 +867,24 @@ def align_left() -> ContextManager[None]: pulse.play(pulse.Constant(100, 1.0), d0) # this pulse will start at t=0 pulse.play(pulse.Constant(20, 1.0), d1) + pulse_prog = pulse.transforms.block_to_schedule(pulse_prog) assert pulse_prog.ch_start_time(d0) == pulse_prog.ch_start_time(d1) + + Yields: + None """ + builder = _active_builder() + builder.push_context(transforms.AlignLeft()) + try: + yield + finally: + current = builder.pop_context() + builder.append_block(current) -@_transform_context(transforms.align_right) -def align_right() -> ContextManager[None]: +@contextmanager +def align_right() -> AlignmentKind: """Right alignment pulse scheduling context. Pulse instructions within this context are scheduled as late as possible @@ -944,13 +905,24 @@ def align_right() -> ContextManager[None]: pulse.play(pulse.Constant(100, 1.0), d0) # this pulse will start at t=80 pulse.play(pulse.Constant(20, 1.0), d1) + pulse_prog = pulse.transforms.block_to_schedule(pulse_prog) assert pulse_prog.ch_stop_time(d0) == pulse_prog.ch_stop_time(d1) + + Yields: + None """ + builder = _active_builder() + builder.push_context(transforms.AlignRight()) + try: + yield + finally: + current = builder.pop_context() + builder.append_block(current) -@_transform_context(transforms.align_sequential) -def align_sequential() -> ContextManager[None]: +@contextmanager +def align_sequential() -> AlignmentKind: """Sequential alignment pulse scheduling context. Pulse instructions within this context are scheduled sequentially in time @@ -971,14 +943,25 @@ def align_sequential() -> ContextManager[None]: pulse.play(pulse.Constant(100, 1.0), d0) # this pulse will also start at t=100 pulse.play(pulse.Constant(20, 1.0), d1) + pulse_prog = pulse.transforms.block_to_schedule(pulse_prog) assert pulse_prog.ch_stop_time(d0) == pulse_prog.ch_start_time(d1) + + Yields: + None """ + builder = _active_builder() + builder.push_context(transforms.AlignSequential()) + try: + yield + finally: + current = builder.pop_context() + builder.append_block(current) -# pylint: disable=unused-argument -@_transform_context(transforms.align_equispaced) -def align_equispaced(duration: int) -> ContextManager[None]: +@contextmanager +def align_equispaced(duration: Union[int, ParameterExpression] + ) -> AlignmentKind: """Equispaced alignment pulse scheduling context. Pulse instructions within this context are scheduled with the same interval spacing such that @@ -1011,17 +994,27 @@ def align_equispaced(duration: int) -> ContextManager[None]: Args: duration: Duration of this context. This should be larger than the schedule duration. + Yields: + None + Notes: The scheduling is performed for sub-schedules within the context rather than channel-wise. If you want to apply the equispaced context for each channel, you should use the context independently for channels. """ + builder = _active_builder() + builder.push_context(transforms.AlignEquispaced(duration=duration)) + try: + yield + finally: + current = builder.pop_context() + builder.append_block(current) -# pylint: disable=unused-argument -@_transform_context(transforms.align_func) -def align_func(duration: int, - func: Callable[[int], float]) -> ContextManager[None]: +@contextmanager +def align_func(duration: Union[int, ParameterExpression], + func: Callable[[int], float] + ) -> AlignmentKind: """Callback defined alignment pulse scheduling context. Pulse instructions within this context are scheduled at the location specified by @@ -1062,113 +1055,99 @@ def udd10_pos(j): The returned value should be defined within [0, 1]. The pulse index starts from 1. + Yields: + None + Notes: The scheduling is performed for sub-schedules within the context rather than channel-wise. If you want to apply the numerical context for each channel, you need to apply the context independently to channels. """ + builder = _active_builder() + builder.push_context(transforms.AlignFunc(duration=duration, func=func)) + try: + yield + finally: + current = builder.pop_context() + builder.append_block(current) -def _align(alignment: str = 'left') -> ContextManager[None]: - """General alignment context. Used by the :class:`_Builder` to choose the - default alignment policy. +@contextmanager +def general_transforms(alignment_context: AlignmentKind) -> ContextManager[None]: + """Arbitrary alignment transformation defined by a subclass instance of + :class:`~qiskit.pulse.transforms.alignments.AlignmentKind`. Args: - alignment: Alignment scheduling policy to follow. - One of "left", "right" or "sequential". + alignment_context: Alignment context instance that defines schedule transformation. - Returns: - An alignment context that will schedule the instructions it contains - according to the selected alignment policy upon exiting the context. + Yields: + None Raises: - exceptions.PulseError: If an unsupported alignment context is selected. + PulseError: When input ``alignment_context`` is not ``AlignmentKind`` subclasses. """ - if alignment == 'left': - return align_left() - elif alignment == 'right': - return align_right() - elif alignment == 'sequential': - return align_sequential() - else: - raise exceptions.PulseError('Alignment "{}" is not ' - 'supported.'.format(alignment)) + if not isinstance(alignment_context, AlignmentKind): + raise exceptions.PulseError('Input alignment context is not `AlignmentKind` subclass.') + + builder = _active_builder() + builder.push_context(alignment_context) + try: + yield + finally: + current = builder.pop_context() + builder.append_block(current) +@utils.deprecated_functionality @contextmanager def inline() -> ContextManager[None]: - """Inline all instructions within this context into the parent context, + """Deprecated. Inline all instructions within this context into the parent context, inheriting the scheduling policy of the parent context. - Examples: - - .. jupyter-execute:: - - from qiskit import pulse - - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - d2 = pulse.DriveChannel(2) - - with pulse.build() as pulse_prog: - # will be ignored due to internal grouping - with pulse.align_left(): - pulse.play(pulse.Constant(10, 1.0), d0) - with pulse.inline(): - with pulse.align_right(): - # this pulse will start at t=0 - pulse.play(pulse.Constant(100, 1.0), d1) - # this pulse will also start at t=0 - pulse.play(pulse.Constant(20, 1.0), d2) - - assert (pulse_prog.ch_start_time(d0) == - pulse_prog.ch_start_time(d1) == - pulse_prog.ch_start_time(d2)) - .. warning:: This will cause all scheduling directives within this context to be ignored. """ + def _flatten(block): + for inst in block.blocks: + if isinstance(inst, ScheduleBlock): + yield from _flatten(inst) + else: + yield inst + builder = _active_builder() - context_schedule = builder.context_schedule - transform_schedule = Schedule() - builder.set_context_schedule(transform_schedule) + + # set a placeholder + builder.push_context(transforms.AlignLeft()) try: yield finally: - builder._compile_lazy_circuit() - builder.set_context_schedule(context_schedule) - for _, instruction in transform_schedule.instructions: - append_instruction(instruction) + placeholder = builder.pop_context() + for inst in _flatten(placeholder): + builder.append_instruction(inst) -@_transform_context(transforms.pad, inplace=True) +@contextmanager def pad(*chs: chans.Channel) -> ContextManager[None]: # pylint: disable=unused-argument - """Pad all available timeslots with delays upon exiting context. + """Deprecated. Pad all available timeslots with delays upon exiting context. Args: chs: Channels to pad with delays. Defaults to all channels in context if none are supplied. - Examples: - - .. jupyter-execute:: - - from qiskit import pulse - - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - - with pulse.build() as pulse_prog: - with pulse.pad(): - with pulse.align_right(): - # this pulse will start at t=0 - pulse.play(pulse.Constant(100, 1.0), d0) - # this pulse will start at t=80 - # a delay will be inserted from t=0 to t=80 - pulse.play(pulse.Constant(20, 1.0), d1) - assert pulse_prog.ch_start_time(d0) == pulse_prog.ch_start_time(d1) - assert pulse_prog.ch_stop_time(d0) == pulse_prog.ch_stop_time(d1) + Yields: + None """ + warnings.warn('Context-wise padding is being deprecated. Requested padding is being ignored. ' + 'Now the pulse builder generate a program in `ScheduleBlock` representation. ' + 'The padding with delay as a blocker is no longer necessary for this program. ' + 'However, if you still want delays, you can convert the output program ' + 'into `Schedule` representation by calling ' + '`qiskit.pulse.transforms.target_qobj_transform`. Then, you can apply ' + '`qiskit.pulse.transforms.pad` to the converted schedule. ', DeprecationWarning) + try: + yield + finally: + pass @contextmanager @@ -1307,7 +1286,11 @@ def frequency_offset(frequency: float, None """ builder = _active_builder() - t0 = builder.context_schedule.duration + # TODO: Need proper implementation of compensation. t0 may depend on the parent context. + # For example, the instruction position within the equispaced context depends on + # the current total number of instructions, thus adding more instruction after + # offset context may change the t0 when the parent context is transformed. + t0 = builder.get_context().duration for channel in channels: shift_frequency(frequency, channel) @@ -1315,7 +1298,7 @@ def frequency_offset(frequency: float, yield finally: if compensate_phase: - duration = builder.context_schedule.duration - t0 + duration = builder.get_context().duration - t0 dt = active_backend().configuration().dt accumulated_phase = 2 * np.pi * ((duration * dt * frequency) % 1) for channel in channels: @@ -1788,9 +1771,9 @@ def call(target: Union[circuit.QuantumCircuit, Schedule], Raises: exceptions.PulseError: If the input ``target`` type is not supported. """ - if not isinstance(target, (circuit.QuantumCircuit, Schedule)): + if not isinstance(target, (circuit.QuantumCircuit, Schedule, ScheduleBlock)): raise exceptions.PulseError( - 'Target of type "{}" is not supported.'.format(type(target))) + f'Target of type "{target.__class__.__name__}" is not supported.') _active_builder().call_subroutine(target, name, value_dict, **kw_params) @@ -1832,7 +1815,9 @@ def barrier(*channels_or_qubits: Union[chans.Channel, int], name: Optional[str] pulse.play(pulse.Constant(10, 1.0), d0) pulse.play(pulse.Constant(10, 1.0), d1) - barrier_pulse_prog = transforms.remove_directives(barrier_pulse_prog) + barrier_pulse_prog = transforms.target_qobj_transform(barrier_pulse_prog) + aligned_pulse_prog = transforms.target_qobj_transform(aligned_pulse_prog) + assert barrier_pulse_prog == aligned_pulse_prog The barrier allows the pulse compiler to take care of more advanced @@ -1919,7 +1904,7 @@ def wrapper(*args, **kwargs): with build(backend=_builder.backend, name=func_name) as built: output = func(*args, **kwargs) - _builder.call_schedule(built) + _builder.call_subroutine(built) return output return wrapper @@ -2005,7 +1990,7 @@ def measure(qubits: Union[List[int], int], # note this is not a subroutine. # just a macro to automate combination of stimulus and acquisition. - _active_builder().call_schedule(measure_sched) + _active_builder().call_subroutine(measure_sched) if len(qubits) == 1: return registers[0] @@ -2050,7 +2035,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_schedule(measure_sched) + _active_builder().call_subroutine(measure_sched) return registers @@ -2082,7 +2067,7 @@ def delay_qubits(duration: int, """ qubit_chans = set(itertools.chain.from_iterable( qubit_channels(qubit) for qubit in qubits)) - with align_left(): + with align_left(): # pylint: disable=not-context-manager for chan in qubit_chans: delay(duration, chan) @@ -2144,7 +2129,7 @@ def call_gate(gate: circuit.Gate, qubits: Tuple[int, ...], lazy: bool = True): _active_builder().call_gate(gate, qubits, lazy=lazy) -def cx(control: int, target: int): +def cx(control: int, target: int): # pylint: disable=invalid-name """Call a :class:`~qiskit.circuit.library.standard_gates.CXGate` on the input physical qubits. @@ -2169,7 +2154,7 @@ def cx(control: int, target: int): call_gate(gates.CXGate(), (control, target)) -def u1(theta: float, qubit: int): +def u1(theta: float, qubit: int): # pylint: disable=invalid-name """Call a :class:`~qiskit.circuit.library.standard_gates.U1Gate` on the input physical qubit. @@ -2196,7 +2181,7 @@ def u1(theta: float, qubit: int): call_gate(gates.U1Gate(theta), qubit) -def u2(phi: float, lam: float, qubit: int): +def u2(phi: float, lam: float, qubit: int): # pylint: disable=invalid-name """Call a :class:`~qiskit.circuit.library.standard_gates.U2Gate` on the input physical qubit. @@ -2223,7 +2208,7 @@ def u2(phi: float, lam: float, qubit: int): call_gate(gates.U2Gate(phi, lam), qubit) -def u3(theta: float, phi: float, lam: float, qubit: int): +def u3(theta: float, phi: float, lam: float, qubit: int): # pylint: disable=invalid-name """Call a :class:`~qiskit.circuit.library.standard_gates.U3Gate` on the input physical qubit. diff --git a/qiskit/pulse/instruction_schedule_map.py b/qiskit/pulse/instruction_schedule_map.py index ac818072bf2d..56b4447ff241 100644 --- a/qiskit/pulse/instruction_schedule_map.py +++ b/qiskit/pulse/instruction_schedule_map.py @@ -36,10 +36,10 @@ from qiskit.circuit import ParameterExpression from qiskit.circuit.instruction import Instruction from qiskit.pulse.exceptions import PulseError -from qiskit.pulse.schedule import Schedule, ParameterizedSchedule +from qiskit.pulse.schedule import Schedule, ScheduleBlock, ParameterizedSchedule ScheduleArgumentsTuple = NamedTuple('ScheduleArgumentsTuple', - [('schedule', Union[Callable, Schedule]), + [('schedule', Union[Callable, Schedule, ScheduleBlock]), ('arguments', Tuple[str])]) ScheduleArgumentsTuple.__doc__ = 'Set of schedule generator and associated argument names.' ScheduleArgumentsTuple.schedule.__doc__ = 'Schedule generator function or Schedule.' @@ -159,9 +159,10 @@ def get(self, instruction: Union[str, Instruction], qubits: Union[int, Iterable[int]], *params: Union[int, float, complex, ParameterExpression], - **kwparams: Union[int, float, complex, ParameterExpression]) -> Schedule: - """Return the defined :py:class:`~qiskit.pulse.Schedule` for the given instruction on - the given qubits. + **kwparams: Union[int, float, complex, ParameterExpression] + ) -> Union[Schedule, ScheduleBlock]: + """Return the defined :py:class:`~qiskit.pulse.Schedule` or + :py:class:`~qiskit.pulse.ScheduleBlock` for the given instruction on the given qubits. If all keys are not specified this method returns schedule with unbound parameters. @@ -211,7 +212,7 @@ def get(self, def add(self, instruction: Union[str, Instruction], qubits: Union[int, Iterable[int]], - schedule: Union[Schedule, Callable[..., Schedule]], + schedule: Union[Schedule, ScheduleBlock, Callable[..., Union[Schedule, ScheduleBlock]]], arguments: Optional[List[str]] = None) -> None: """Add a new known instruction for the given qubits and its mapping to a pulse schedule. @@ -250,8 +251,8 @@ def sched_callback(**kwargs): return # validation of input data - if not (isinstance(schedule, Schedule) or callable(schedule)): - raise PulseError('Supplied schedule must be either a Schedule, or a ' + if not (isinstance(schedule, (Schedule, ScheduleBlock)) or callable(schedule)): + raise PulseError('Supplied schedule must be either a Schedule, ScheduleBlock or a ' 'callable that outputs a schedule.') # initialize parameter list @@ -297,8 +298,9 @@ def pop(self, instruction: Union[str, Instruction], qubits: Union[int, Iterable[int]], *params: Union[int, float, complex, ParameterExpression], - **kwparams: Union[int, float, complex, ParameterExpression]) -> Schedule: - """Remove and return the defined ``Schedule`` for the given instruction on the given + **kwparams: Union[int, float, complex, ParameterExpression] + ) -> Union[Schedule, ScheduleBlock]: + """Remove and return the defined schedule for the given instruction on the given qubits. Args: diff --git a/qiskit/pulse/instructions/call.py b/qiskit/pulse/instructions/call.py index b1f3749b4d52..c993b8b48cbd 100644 --- a/qiskit/pulse/instructions/call.py +++ b/qiskit/pulse/instructions/call.py @@ -55,16 +55,17 @@ def __init__(self, subroutine, # initialize parameter template # TODO remove self._parameter_table - self._arguments = dict() if subroutine.is_parameterized(): - for param in subroutine.parameters: - self._arguments[param] = value_dict.get(param, param) + self._arguments = {par: value_dict.get(par, par) for par in subroutine.parameters} + assigned_subroutine = subroutine.assign_parameters( + value_dict=self.arguments, + inplace=False + ) + else: + self._arguments = dict() + assigned_subroutine = subroutine # create cache data of parameter-assigned subroutine - assigned_subroutine = subroutine.assign_parameters( - value_dict=self.arguments, - inplace=False - ) self._assigned_cache = tuple((self._get_arg_hash(), assigned_subroutine)) super().__init__(operands=(subroutine, ), name=name or f"{self.prefix}_{subroutine.name}") diff --git a/qiskit/pulse/parameter_manager.py b/qiskit/pulse/parameter_manager.py index 3983e41e248d..6b6435a4191b 100644 --- a/qiskit/pulse/parameter_manager.py +++ b/qiskit/pulse/parameter_manager.py @@ -134,7 +134,7 @@ def visit_ScheduleBlock(self, node: ScheduleBlock): .. note:: ``ScheduleBlock`` can have parameters in blocks and its alignment. """ # accessing to protected member - node._blocks = [self.visit(block) for block in node.instructions] + node._blocks = [self.visit(block) for block in node.blocks] node._alignment_context = self.visit_AlignmentKind(node.alignment_context) self._update_parameter_manager(node) diff --git a/qiskit/pulse/schedule.py b/qiskit/pulse/schedule.py index e2d67fefc845..748bc9bf3556 100644 --- a/qiskit/pulse/schedule.py +++ b/qiskit/pulse/schedule.py @@ -31,7 +31,7 @@ from qiskit.circuit.parameter import Parameter from qiskit.circuit.parameterexpression import ParameterExpression, ParameterValueType from qiskit.pulse.channels import Channel -from qiskit.pulse.exceptions import PulseError, UnassignedDurationError +from qiskit.pulse.exceptions import PulseError from qiskit.pulse.instructions import Instruction from qiskit.pulse.utils import instruction_duration_validation, deprecated_functionality from qiskit.utils.multiprocessing import is_main_process @@ -780,11 +780,7 @@ def _require_schedule_conversion(function: Callable) -> Callable: @functools.wraps(function) def wrapper(self, *args, **kwargs): from qiskit.pulse.transforms import block_to_schedule - if self.is_schedulable(): - return function(block_to_schedule(self), *args, **kwargs) - raise UnassignedDurationError('This method requires all durations to be assigned with ' - 'some integer value. Please check `.parameters` to find ' - 'unassigned parameter objects.') + return function(block_to_schedule(self), *args, **kwargs) return wrapper @@ -933,7 +929,7 @@ def is_schedulable(self) -> bool: return False # check duration assignment - for block in self.instructions: + for block in self.blocks: if isinstance(block, ScheduleBlock): if not block.is_schedulable(): return False @@ -973,13 +969,19 @@ def stop_time(self) -> int: def channels(self) -> Tuple[Channel]: """Returns channels that this schedule clock uses.""" chans = set() - for block in self.instructions: + for block in self.blocks: for chan in block.channels: chans.add(chan) return tuple(chans) @property - def instructions(self) -> Tuple[BlockComponent]: + @_require_schedule_conversion + def instructions(self) -> Tuple[Tuple[int, Instruction]]: + """Get the time-ordered instructions from self.""" + return self.instructions + + @property + def blocks(self) -> Tuple[BlockComponent]: """Get the time-ordered instructions from self.""" return tuple(self._blocks) @@ -1166,7 +1168,7 @@ def exclude(self, *filter_funcs: List[Callable], defined by the :py:class:`~qiskit.pulse.instructions.Call` instruction. Returns: - ``Schedule`` consisting of instructions that are not matche with filtering condition. + ``Schedule`` consisting of instructions that are not match with filtering condition. Raises: PulseError: When this method is called. This method will be supported soon. @@ -1198,7 +1200,7 @@ def replace(self, new_blocks = [] new_parameters = ParameterManager() - for block in self.instructions: + for block in self.blocks: if block == old: new_blocks.append(new) new_parameters.update_parameter_table(new) @@ -1260,7 +1262,7 @@ def get_parameters(self, def __len__(self) -> int: """Return number of instructions in the schedule.""" - return len(self.instructions) + return len(self.blocks) def __eq__(self, other: 'ScheduleBlock') -> bool: """Test if two ScheduleBlocks are equal. @@ -1306,12 +1308,12 @@ def __eq__(self, other: 'ScheduleBlock') -> bool: def __repr__(self) -> str: name = format(self._name) if self._name else "" - instructions = ", ".join([repr(instr) for instr in self.instructions[:50]]) - if len(self.instructions) > 25: - instructions += ", ..." + blocks = ", ".join([repr(instr) for instr in self.blocks[:50]]) + if len(self.blocks) > 25: + blocks += ", ..." return '{}({}, name="{}", transform={})'.format( self.__class__.__name__, - instructions, + blocks, name, repr(self.alignment_context) ) diff --git a/qiskit/pulse/transforms/__init__.py b/qiskit/pulse/transforms/__init__.py index 1189bc31953a..369514f3622b 100644 --- a/qiskit/pulse/transforms/__init__.py +++ b/qiskit/pulse/transforms/__init__.py @@ -31,7 +31,6 @@ AlignLeft AlignRight AlignSequential - pad Canonicalization @@ -49,6 +48,7 @@ compress_pulses flatten inline_subroutines + pad remove_directives remove_trivial_barriers @@ -64,6 +64,17 @@ block_to_dag + +Composite transform +=================== + +A sequence of transformations to generate a target code. + +.. autosummary:: + :toctree: ../stubs/ + + target_qobj_transform + """ from qiskit.pulse.transforms.alignments import ( @@ -76,8 +87,11 @@ align_func, align_left, align_right, - align_sequential, - pad + align_sequential +) + +from qiskit.pulse.transforms.base_transforms import ( + target_qobj_transform ) from qiskit.pulse.transforms.canonicalization import ( @@ -87,6 +101,7 @@ compress_pulses, flatten, inline_subroutines, + pad, remove_directives, remove_trivial_barriers, ) diff --git a/qiskit/pulse/transforms/alignments.py b/qiskit/pulse/transforms/alignments.py index b9787df1e857..f34682b43de3 100644 --- a/qiskit/pulse/transforms/alignments.py +++ b/qiskit/pulse/transforms/alignments.py @@ -12,15 +12,14 @@ """A collection of passes to reallocate the timeslots of instructions according to context.""" import abc -from typing import Optional, Iterable, Callable, Dict, Any, Union +from typing import Callable, Dict, Any, Union import numpy as np from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.pulse import channels as chans, instructions from qiskit.pulse.exceptions import PulseError from qiskit.pulse.schedule import Schedule, ScheduleComponent -from qiskit.pulse.utils import instruction_duration_validation +from qiskit.pulse.utils import instruction_duration_validation, deprecated_functionality class AlignmentKind(abc.ABC): @@ -84,6 +83,7 @@ def align(self, schedule: Schedule) -> Schedule: aligned = Schedule() for _, child in schedule._children: self._push_left_append(aligned, child) + return aligned @staticmethod @@ -115,6 +115,7 @@ def _push_left_append(this: Schedule, other: ScheduleComponent) -> Schedule: other_only_insert_time = other.ch_start_time(*(other_channels - this_channels)) # Choose whichever is greatest. insert_time = max(shared_insert_time, other_only_insert_time) + return this.insert(insert_time, other, inplace=True) @@ -140,6 +141,7 @@ def align(self, schedule: Schedule) -> Schedule: aligned = Schedule() for _, child in reversed(schedule._children): aligned = self._push_right_prepend(aligned, child) + return aligned @staticmethod @@ -200,6 +202,7 @@ def align(self, schedule: Schedule) -> Schedule: aligned = Schedule() for _, child in schedule._children: aligned.insert(aligned.duration, child, inplace=True) + return aligned @@ -211,7 +214,8 @@ class AlignEquispaced(AlignmentKind): """ is_sequential = True - def __init__(self, duration: Union[int, ParameterExpression]): + def __init__(self, + duration: Union[int, ParameterExpression]): """Create new equispaced context. Args: @@ -224,6 +228,11 @@ def __init__(self, duration: Union[int, ParameterExpression]): self._context_params = (duration, ) + @property + def duration(self): + """Return context duration.""" + return self._context_params[0] + def align(self, schedule: Schedule) -> Schedule: """Reallocate instructions according to the policy. @@ -236,14 +245,13 @@ def align(self, schedule: Schedule) -> Schedule: Returns: Schedule with reallocated instructions. """ - duration = self._context_params[0] - instruction_duration_validation(duration) + instruction_duration_validation(self.duration) total_duration = sum([child.duration for _, child in schedule._children]) - if duration < total_duration: + if self.duration < total_duration: return schedule - total_delay = duration - total_duration + total_delay = self.duration - total_duration if len(schedule._children) > 1: # Calculate the interval in between sub-schedules. @@ -264,12 +272,12 @@ def align(self, schedule: Schedule) -> Schedule: aligned.insert(_t0, child, inplace=True) _t0 = int(aligned.stop_time + interval) - return pad(aligned, aligned.channels, until=duration, inplace=True) + return aligned def to_dict(self) -> Dict[str, Any]: """Returns dictionary to represent this alignment.""" return {'alignment': self.__class__.__name__, - 'duration': self._context_params[0]} + 'duration': self.duration} class AlignFunc(AlignmentKind): @@ -307,6 +315,11 @@ def __init__(self, duration: Union[int, ParameterExpression], func: Callable): self._context_params = (duration, ) self._func = func + @property + def duration(self): + """Return context duration.""" + return self._context_params[0] + def align(self, schedule: Schedule) -> Schedule: """Reallocate instructions according to the policy. @@ -319,21 +332,20 @@ def align(self, schedule: Schedule) -> Schedule: Returns: Schedule with reallocated instructions. """ - duration = self._context_params[0] - instruction_duration_validation(duration) + instruction_duration_validation(self.duration) - if duration < schedule.duration: + if self.duration < schedule.duration: return schedule aligned = Schedule() for ind, (_, child) in enumerate(schedule._children): - _t_center = duration * self._func(ind + 1) + _t_center = self.duration * self._func(ind + 1) _t0 = int(_t_center - 0.5 * child.duration) - if _t0 < 0 or _t0 > duration: + if _t0 < 0 or _t0 > self.duration: PulseError('Invalid schedule position t=%d is specified at index=%d' % (_t0, ind)) aligned.insert(_t0, child, inplace=True) - return pad(aligned, aligned.channels, until=duration, inplace=True) + return aligned def to_dict(self) -> Dict[str, Any]: """Returns dictionary to represent this alignment. @@ -345,57 +357,7 @@ def to_dict(self) -> Dict[str, Any]: 'func': self._func.__name__} -def pad(schedule: Schedule, - channels: Optional[Iterable[chans.Channel]] = None, - until: Optional[int] = None, - inplace: bool = False - ) -> Schedule: - """Pad the input Schedule with ``Delay``s on all unoccupied timeslots until - ``schedule.duration`` or ``until`` if not ``None``. - - Args: - schedule: Schedule to pad. - channels: Channels to pad. Defaults to all channels in - ``schedule`` if not provided. If the supplied channel is not a member - 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. - - Returns: - The padded schedule. - """ - until = until or schedule.duration - channels = channels or schedule.channels - - for channel in channels: - if channel not in schedule.channels: - schedule |= instructions.Delay(until, channel) - 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: - 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) - - return schedule - - +@deprecated_functionality def align_left(schedule: Schedule) -> Schedule: """Align a list of pulse instructions on the left. @@ -410,6 +372,7 @@ def align_left(schedule: Schedule) -> Schedule: return context.align(schedule) +@deprecated_functionality def align_right(schedule: Schedule) -> Schedule: """Align a list of pulse instructions on the right. @@ -424,6 +387,7 @@ def align_right(schedule: Schedule) -> Schedule: return context.align(schedule) +@deprecated_functionality def align_sequential(schedule: Schedule) -> Schedule: """Schedule all top-level nodes in parallel. @@ -438,6 +402,7 @@ def align_sequential(schedule: Schedule) -> Schedule: return context.align(schedule) +@deprecated_functionality def align_equispaced(schedule: Schedule, duration: int) -> Schedule: """Schedule a list of pulse instructions with equivalent interval. @@ -456,6 +421,7 @@ def align_equispaced(schedule: Schedule, duration: int) -> Schedule: return context.align(schedule) +@deprecated_functionality def align_func(schedule: Schedule, duration: int, func: Callable[[int], float]) -> Schedule: """Schedule a list of pulse instructions with schedule position defined by the numerical expression. diff --git a/qiskit/pulse/transforms/base_transforms.py b/qiskit/pulse/transforms/base_transforms.py new file mode 100644 index 000000000000..bce609bb4ccb --- /dev/null +++ b/qiskit/pulse/transforms/base_transforms.py @@ -0,0 +1,72 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. +"""A collection of set of transforms.""" + +# TODO: replace this with proper pulse transformation passes. Qiskit-terra/#6121 + +from typing import Union, Iterable, Tuple + +from qiskit.pulse.instructions import Instruction +from qiskit.pulse.schedule import ScheduleBlock, Schedule +from qiskit.pulse.transforms import canonicalization + +InstructionSched = Union[Tuple[int, Instruction], Instruction] + + +def target_qobj_transform(sched: Union[ScheduleBlock, + Schedule, + InstructionSched, + Iterable[InstructionSched]], + remove_directives: bool = True) -> Schedule: + """A basic pulse program transformation for OpenPulse API execution. + + Args: + sched: Input program to transform. + remove_directives: Set `True` to remove compiler directives. + + Returns: + Transformed program for execution. + """ + if not isinstance(sched, Schedule): + # convert into schedule representation + if isinstance(sched, ScheduleBlock): + sched = canonicalization.block_to_schedule(sched) + else: + sched = Schedule(*_format_schedule_component(sched)) + + # remove subroutines, i.e. Call instructions + sched = canonicalization.inline_subroutines(sched) + + # inline nested schedules + sched = canonicalization.flatten(sched) + + # remove directives, e.g. barriers + if remove_directives: + sched = canonicalization.remove_directives(sched) + + return sched + + +def _format_schedule_component(sched: Union[InstructionSched, Iterable[InstructionSched]]): + """A helper function to convert instructions into list of instructions.""" + # TODO remove schedule initialization with *args, Qiskit-terra/#5093 + + try: + sched = list(sched) + # (t0, inst), or list of it + if isinstance(sched[0], int): + # (t0, inst) tuple + return [tuple(sched)] + else: + return sched + except TypeError: + return [sched] diff --git a/qiskit/pulse/transforms/canonicalization.py b/qiskit/pulse/transforms/canonicalization.py index 43aab220da0c..342e67476466 100644 --- a/qiskit/pulse/transforms/canonicalization.py +++ b/qiskit/pulse/transforms/canonicalization.py @@ -36,6 +36,9 @@ def block_to_schedule(block: ScheduleBlock) -> Schedule: Raises: UnassignedDurationError: When any instruction duration is not assigned. + PulseError: When the alignment context duration is shorter than the schedule duration. + + .. note:: This transform may insert barriers in between contexts. """ if not block.is_schedulable(): raise UnassignedDurationError( @@ -43,10 +46,26 @@ def block_to_schedule(block: ScheduleBlock) -> Schedule: 'Please check `.parameters` to find unassigned parameter objects.') schedule = Schedule(name=block.name, metadata=block.metadata) - for op_data in block.instructions: + + for op_data in block.blocks: if isinstance(op_data, ScheduleBlock): context_schedule = block_to_schedule(op_data) + if hasattr(op_data.alignment_context, 'duration'): + # context may have local scope duration, e.g. EquispacedAlignment for 1000 dt + post_buffer = op_data.alignment_context.duration - context_schedule.duration + if post_buffer < 0: + raise PulseError(f'ScheduleBlock {op_data.name} has longer duration than ' + 'the specified context duration ' + f'{context_schedule.duration} > {op_data.duration}.') + else: + post_buffer = 0 schedule.append(context_schedule, inplace=True) + + # prevent interruption by following instructions. + # padding with delay instructions is no longer necessary, thanks to alignment context. + if post_buffer > 0: + context_boundary = instructions.RelativeBarrier(*op_data.channels) + schedule.append(context_boundary.shift(post_buffer), inplace=True) else: schedule.append(op_data, inplace=True) @@ -161,7 +180,7 @@ def _inline_block(block: ScheduleBlock) -> ScheduleBlock: ret_block = ScheduleBlock(alignment_context=block.alignment_context, name=block.name, metadata=block.metadata) - for inst in block.instructions: + for inst in block.blocks: if isinstance(inst, instructions.Call): # bind parameter subroutine = inst.assigned_subroutine() @@ -236,11 +255,15 @@ def align_measures(schedules: Iterable[ScheduleComponent], from qiskit import pulse from qiskit.pulse import transforms - with pulse.build() as sched: - with pulse.align_sequential(): - pulse.play(pulse.Constant(10, 0.5), pulse.DriveChannel(0)) - pulse.play(pulse.Constant(10, 1.), pulse.MeasureChannel(0)) - pulse.acquire(20, pulse.AcquireChannel(0), pulse.MemorySlot(0)) + d0 = pulse.DriveChannel(0) + m0 = pulse.MeasureChannel(0) + a0 = pulse.AcquireChannel(0) + mem0 = pulse.MemorySlot(0) + + sched = pulse.Schedule() + sched.append(pulse.Play(pulse.Constant(10, 0.5), d0), inplace=True) + sched.append(pulse.Play(pulse.Constant(10, 1.), m0).shift(sched.duration), inplace=True) + sched.append(pulse.Acquire(20, a0, mem0).shift(sched.duration), inplace=True) sched_shifted = sched << 20 @@ -405,3 +428,54 @@ def add_implicit_acquires(schedule: ScheduleComponent, new_schedule.insert(time, inst, inplace=True) return new_schedule + + +def pad(schedule: Schedule, + channels: Optional[Iterable[chans.Channel]] = None, + until: Optional[int] = None, + inplace: bool = False + ) -> Schedule: + """Pad the input Schedule with ``Delay``s on all unoccupied timeslots until + ``schedule.duration`` or ``until`` if not ``None``. + + Args: + schedule: Schedule to pad. + channels: Channels to pad. Defaults to all channels in + ``schedule`` if not provided. If the supplied channel is not a member + 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. + + Returns: + The padded schedule. + """ + until = until or schedule.duration + channels = channels or schedule.channels + + for channel in channels: + if channel not in schedule.channels: + schedule |= instructions.Delay(until, channel) + 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: + 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) + + return schedule diff --git a/qiskit/pulse/transforms/dag.py b/qiskit/pulse/transforms/dag.py index 96b4b7337b31..cf33812be473 100644 --- a/qiskit/pulse/transforms/dag.py +++ b/qiskit/pulse/transforms/dag.py @@ -67,33 +67,33 @@ def block_to_dag(block: ScheduleBlock) -> rx.PyDAG: def _sequential_allocation(block: ScheduleBlock) -> rx.PyDAG: """A helper function to create a DAG of a sequential alignment context.""" - dag_instructions = rx.PyDAG() + dag_blocks = rx.PyDAG() prev_node = None edges = [] - for inst in block.instructions: - current_node = dag_instructions.add_node(inst) + for inst in block.blocks: + current_node = dag_blocks.add_node(inst) if prev_node is not None: edges.append((prev_node, current_node)) prev_node = current_node - dag_instructions.add_edges_from_no_data(edges) + dag_blocks.add_edges_from_no_data(edges) - return dag_instructions + return dag_blocks def _parallel_allocation(block: ScheduleBlock) -> rx.PyDAG: """A helper function to create a DAG of a parallel alignment context.""" - dag_instructions = rx.PyDAG() + dag_blocks = rx.PyDAG() slots = dict() edges = [] - for inst in block.instructions: - current_node = dag_instructions.add_node(inst) + for inst in block.blocks: + current_node = dag_blocks.add_node(inst) for chan in inst.channels: prev_node = slots.pop(chan, None) if prev_node is not None: edges.append((prev_node, current_node)) slots[chan] = current_node - dag_instructions.add_edges_from_no_data(edges) + dag_blocks.add_edges_from_no_data(edges) - return dag_instructions + return dag_blocks diff --git a/qiskit/scheduler/lowering.py b/qiskit/scheduler/lowering.py index 791d837b5536..977811194ea1 100644 --- a/qiskit/scheduler/lowering.py +++ b/qiskit/scheduler/lowering.py @@ -54,6 +54,8 @@ def lower_gates(circuit: QuantumCircuit, schedule_config: ScheduleConfig) -> Lis Raises: QiskitError: If circuit uses a command that isn't defined in config.inst_map. """ + from qiskit.pulse.transforms.base_transforms import target_qobj_transform + circ_pulse_defs = [] inst_map = schedule_config.inst_map @@ -73,6 +75,7 @@ def get_measure_schedule(qubit_mem_slots: Dict[int, int]) -> CircuitPulseDef: for qubit in qubits: try: meas_q = circuit.calibrations[Measure().name][((qubit,), params)] + meas_q = target_qobj_transform(meas_q) acquire_q = meas_q.filter(channels=[AcquireChannel(qubit)]) mem_slot_index = [chan.index for chan in acquire_q.channels if isinstance(chan, MemorySlot)][0] @@ -92,6 +95,7 @@ def get_measure_schedule(qubit_mem_slots: Dict[int, int]) -> CircuitPulseDef: inst_map=inst_map, meas_map=schedule_config.meas_map, qubit_mem_slots=qubit_mem_slots) + meas_sched = target_qobj_transform(meas_sched) meas_sched = meas_sched.exclude(channels=[AcquireChannel(qubit) for qubit in acquire_excludes]) sched |= meas_sched @@ -131,15 +135,17 @@ def get_measure_schedule(qubit_mem_slots: Dict[int, int]) -> CircuitPulseDef: tuple(inst_qubits), tuple(p if getattr(p, 'parameters', None) else float(p) for p in inst.params), )] + schedule = target_qobj_transform(schedule) circ_pulse_defs.append(CircuitPulseDef(schedule=schedule, qubits=inst_qubits)) continue except KeyError: pass # Calibration not defined for this operation try: + schedule = inst_map.get(inst, inst_qubits, *inst.params) + schedule = target_qobj_transform(schedule) circ_pulse_defs.append( - CircuitPulseDef(schedule=inst_map.get(inst, inst_qubits, *inst.params), - qubits=inst_qubits)) + CircuitPulseDef(schedule=schedule, qubits=inst_qubits)) except PulseError as ex: raise QiskitError( f"Operation '{inst.name}' on qubit(s) {inst_qubits} not supported by the " diff --git a/qiskit/visualization/pulse_v2/core.py b/qiskit/visualization/pulse_v2/core.py index 14b471b95de9..bca3689ee10c 100644 --- a/qiskit/visualization/pulse_v2/core.py +++ b/qiskit/visualization/pulse_v2/core.py @@ -73,7 +73,7 @@ import numpy as np from qiskit import pulse -from qiskit.pulse.transforms import flatten, inline_subroutines +from qiskit.pulse.transforms import target_qobj_transform from qiskit.visualization.exceptions import VisualizationError from qiskit.visualization.pulse_v2 import events, types, drawings, device_info from qiskit.visualization.pulse_v2.stylesheet import QiskitPulseStyle @@ -205,8 +205,8 @@ def load_program(self, program: Union[pulse.Waveform, pulse.ParametricPulse, pul Raises: VisualizationError: When input program is invalid data format. """ - if isinstance(program, pulse.Schedule): - self._schedule_loader(flatten(inline_subroutines(program))) + if isinstance(program, (pulse.Schedule, pulse.ScheduleBlock)): + self._schedule_loader(program) elif isinstance(program, (pulse.Waveform, pulse.ParametricPulse)): self._waveform_loader(program) else: @@ -244,7 +244,7 @@ def _waveform_loader(self, program: Union[pulse.Waveform, pulse.ParametricPulse] self.charts.append(chart) - def _schedule_loader(self, program: pulse.Schedule): + def _schedule_loader(self, program: Union[pulse.Schedule, pulse.ScheduleBlock]): """Load Schedule instance. This function is sub-routine of py:method:`load_program`. @@ -252,6 +252,8 @@ def _schedule_loader(self, program: pulse.Schedule): Args: program: `Schedule` to draw. """ + program = target_qobj_transform(program, remove_directives=False) + # initialize scale values self.chan_scales = {} for chan in program.channels: diff --git a/qiskit/visualization/pulse_v2/interface.py b/qiskit/visualization/pulse_v2/interface.py index 80082db4a37f..3e47c2b3aca9 100644 --- a/qiskit/visualization/pulse_v2/interface.py +++ b/qiskit/visualization/pulse_v2/interface.py @@ -25,7 +25,6 @@ from qiskit.providers import BaseBackend from qiskit.pulse import Waveform, ParametricPulse, Schedule, ScheduleBlock -from qiskit.pulse.transforms import block_to_schedule from qiskit.pulse.channels import Channel from qiskit.visualization.exceptions import VisualizationError from qiskit.visualization.pulse_v2 import core, device_info, stylesheet, types @@ -373,9 +372,6 @@ def draw(program: Union[Waveform, ParametricPulse, Schedule, ScheduleBlock], ImportError: When required visualization package is not installed. VisualizationError: When invalid plotter API or invalid time range is specified. """ - if isinstance(program, ScheduleBlock): - program = block_to_schedule(program) - temp_style = stylesheet.QiskitPulseStyle() temp_style.update(style or stylesheet.IQXStandard()) diff --git a/releasenotes/notes/replace-builder-program-ecfc438b1cb19339.yaml b/releasenotes/notes/replace-builder-program-ecfc438b1cb19339.yaml new file mode 100644 index 000000000000..465fc13db1fd --- /dev/null +++ b/releasenotes/notes/replace-builder-program-ecfc438b1cb19339.yaml @@ -0,0 +1,33 @@ +--- +upgrade: + - | + The output program representation of the pulse builder is replaced with + :class:`~qiskit.pulse.ScheduleBlock`. This new representation disables some timing + related operations such as shift and insert. However, this enables parameterized + instruction durations within the builder context. For example: + + .. code-block:: python + + from qiskit import pulse + from qiskit.circuit import Parameter + + dur = Parameter('duration') + + with pulse.build() as sched: + with pulse.align_sequential(): + pulse.delay(dur, pulse.DriveChannel(1)) + pulse.play(pulse.Gaussian(dur, 0.1, dur/4), pulse.DriveChannel(0)) + + assigned0 = sched.assign_parameters({dur: 100}) + assigned1 = sched.assign_parameters({dur: 200}) + + You can directly pass the duration-assigned schedules to the assembler (or backend), + or you can attach them to your quantum circuit as pulse gates. +deprecations: + - | + Builder syntax :func:`pulse.inline` is deprecated. Instead of using this context, + you can just remove alignment contexts within the inline context. + + Builder syntax :func:`pulse.pad` is deprecated. Because :class:`ScheduleBlock` doesn't + support the `.insert` method (and there is no insert syntax in the builder), + timeslot placeholders to block the insertion of other instructions is no longer necessary. diff --git a/test/python/compiler/test_disassembler.py b/test/python/compiler/test_disassembler.py index 8abeb8c71dcc..3f1cf2f1f0cc 100644 --- a/test/python/compiler/test_disassembler.py +++ b/test/python/compiler/test_disassembler.py @@ -23,6 +23,7 @@ from qiskit.circuit import QuantumRegister, ClassicalRegister, QuantumCircuit from qiskit.circuit import Instruction from qiskit.compiler.assembler import assemble +from qiskit.pulse.transforms import target_qobj_transform from qiskit.test import QiskitTestCase from qiskit.test.mock import FakeOpenPulse2Q import qiskit.quantum_info as qi @@ -251,7 +252,7 @@ def test_disassemble_single_schedule(self): self.assertEqual(run_config_out.qubit_lo_freq, self.backend.defaults().qubit_freq_est) self.assertEqual(run_config_out.rep_time, 99) self.assertEqual(len(scheds), 1) - self.assertEqual(scheds[0], sched) + self.assertEqual(scheds[0], target_qobj_transform(sched)) def test_disassemble_multiple_schedules(self): """Test disassembling multiple schedules, all should have the same config. @@ -290,8 +291,8 @@ def test_disassemble_multiple_schedules(self): self.assertEqual(run_config_out.shots, 2000) self.assertEqual(run_config_out.memory, False) self.assertEqual(len(scheds), 2) - self.assertEqual(scheds[0], sched0) - self.assertEqual(scheds[1], sched1) + self.assertEqual(scheds[0], target_qobj_transform(sched0)) + self.assertEqual(scheds[1], target_qobj_transform(sched1)) def test_disassemble_parametric_pulses(self): """Test disassembling multiple schedules all should have the same config. @@ -306,7 +307,7 @@ def test_disassemble_parametric_pulses(self): qobj = assemble(sched, backend=self.backend, shots=2000) scheds, _, _ = disassemble(qobj) - self.assertEqual(scheds[0], sched) + self.assertEqual(scheds[0], target_qobj_transform(sched)) def test_disassemble_schedule_los(self): """Test disassembling schedule los.""" diff --git a/test/python/pulse/__init__.py b/test/python/pulse/__init__.py index 3ec54d578a97..d3a35c2bafeb 100644 --- a/test/python/pulse/__init__.py +++ b/test/python/pulse/__init__.py @@ -11,3 +11,5 @@ # that they have been altered from the originals. """Qiskit pulse tests.""" + +# TODO pulse unittest reorganization Qiskit-terra/#6106 diff --git a/test/python/pulse/test_block.py b/test/python/pulse/test_block.py index b55f4984619b..6964d472ce0a 100644 --- a/test/python/pulse/test_block.py +++ b/test/python/pulse/test_block.py @@ -46,7 +46,7 @@ def _align_func(j): def assertScheduleEqual(self, target, reference): """Check if two block are equal schedule representation.""" - self.assertEqual(transforms.block_to_schedule(target), reference) + self.assertEqual(transforms.target_qobj_transform(target), reference) class TestTransformation(BaseTestBlock): @@ -96,11 +96,8 @@ def test_equispace_alignment(self): ref_sched = pulse.Schedule() ref_sched = ref_sched.insert(0, pulse.Play(self.test_waveform0, self.d0)) - ref_sched = ref_sched.insert(100, pulse.Delay(200, self.d0)) ref_sched = ref_sched.insert(300, pulse.Play(self.test_waveform0, self.d0)) - ref_sched = ref_sched.insert(400, pulse.Delay(200, self.d0)) ref_sched = ref_sched.insert(600, pulse.Play(self.test_waveform0, self.d0)) - ref_sched = ref_sched.insert(700, pulse.Delay(200, self.d0)) ref_sched = ref_sched.insert(900, pulse.Play(self.test_waveform0, self.d0)) self.assertScheduleEqual(block, ref_sched) @@ -112,15 +109,10 @@ def test_func_alignment(self): block = block.append(pulse.Play(self.test_waveform0, self.d0)) ref_sched = pulse.Schedule() - ref_sched = ref_sched.insert(0, pulse.Delay(50, self.d0)) ref_sched = ref_sched.insert(50, pulse.Play(self.test_waveform0, self.d0)) - ref_sched = ref_sched.insert(150, pulse.Delay(50, self.d0)) ref_sched = ref_sched.insert(200, pulse.Play(self.test_waveform0, self.d0)) - ref_sched = ref_sched.insert(300, pulse.Delay(350, self.d0)) ref_sched = ref_sched.insert(650, pulse.Play(self.test_waveform0, self.d0)) - ref_sched = ref_sched.insert(750, pulse.Delay(50, self.d0)) ref_sched = ref_sched.insert(800, pulse.Play(self.test_waveform0, self.d0)) - ref_sched = ref_sched.insert(900, pulse.Delay(100, self.d0)) self.assertScheduleEqual(block, ref_sched) @@ -169,21 +161,21 @@ def test_append_an_instruction_to_empty_block(self): block = pulse.ScheduleBlock() block = block.append(pulse.Play(self.test_waveform0, self.d0)) - self.assertEqual(block.instructions[0], pulse.Play(self.test_waveform0, self.d0)) + self.assertEqual(block.blocks[0], pulse.Play(self.test_waveform0, self.d0)) def test_append_an_instruction_to_empty_block_sugar(self): """Test append instructions to an empty block with syntax sugar.""" block = pulse.ScheduleBlock() block += pulse.Play(self.test_waveform0, self.d0) - self.assertEqual(block.instructions[0], pulse.Play(self.test_waveform0, self.d0)) + self.assertEqual(block.blocks[0], pulse.Play(self.test_waveform0, self.d0)) def test_append_an_instruction_to_empty_block_inplace(self): """Test append instructions to an empty block with inplace.""" block = pulse.ScheduleBlock() block.append(pulse.Play(self.test_waveform0, self.d0), inplace=True) - self.assertEqual(block.instructions[0], pulse.Play(self.test_waveform0, self.d0)) + self.assertEqual(block.blocks[0], pulse.Play(self.test_waveform0, self.d0)) def test_append_a_block_to_empty_block(self): """Test append another ScheduleBlock to empty block.""" @@ -193,7 +185,7 @@ def test_append_a_block_to_empty_block(self): block_main = pulse.ScheduleBlock() block_main = block_main.append(block) - self.assertEqual(block_main.instructions[0], block) + self.assertEqual(block_main.blocks[0], block) def test_append_an_instruction_to_block(self): """Test append instructions to a non-empty block.""" @@ -202,7 +194,7 @@ def test_append_an_instruction_to_block(self): block = block.append(pulse.Delay(100, self.d0)) - self.assertEqual(len(block.instructions), 2) + self.assertEqual(len(block.blocks), 2) def test_append_an_instruction_to_block_inplace(self): """Test append instructions to a non-empty block with inplace.""" @@ -211,7 +203,7 @@ def test_append_an_instruction_to_block_inplace(self): block.append(pulse.Delay(100, self.d0), inplace=True) - self.assertEqual(len(block.instructions), 2) + self.assertEqual(len(block.blocks), 2) def test_duration(self): """Test if correct duration is returned with implicit scheduling.""" @@ -264,7 +256,7 @@ def test_instructions(self): for inst in self.test_blocks: block.append(inst) - self.assertEqual(block.instructions, tuple(self.test_blocks)) + self.assertEqual(block.blocks, tuple(self.test_blocks)) def test_channel_duraction(self): """Test if correct durations is calculated for each channel.""" @@ -331,7 +323,7 @@ def test_replace(self): block_replaced = block.replace(target, replaced, inplace=False) # original schedule is not destroyed - self.assertListEqual(list(block.instructions), self.test_blocks) + self.assertListEqual(list(block.blocks), self.test_blocks) ref_sched = pulse.Schedule() ref_sched = ref_sched.insert(0, pulse.Play(self.test_waveform0, self.d0)) @@ -387,7 +379,7 @@ def test_replace_block_by_instruction(self): pulse.Play(self.test_waveform0, self.d1) ] - self.assertListEqual(list(replaced.instructions), ref_blocks) + self.assertListEqual(list(replaced.blocks), ref_blocks) def test_replace_instruction_by_block(self): """Test replacing instruction with block.""" @@ -416,7 +408,7 @@ def test_replace_instruction_by_block(self): pulse.Play(self.test_waveform0, self.d1) ] - self.assertListEqual(list(replaced.instructions), ref_blocks) + self.assertListEqual(list(replaced.blocks), ref_blocks) def test_len(self): """Test __len__ method""" @@ -765,11 +757,8 @@ def test_parametrized_context(self): ref_sched = pulse.Schedule() ref_sched = ref_sched.insert(0, pulse.Delay(10, self.d0)) - ref_sched = ref_sched.insert(10, pulse.Delay(20, self.d0)) ref_sched = ref_sched.insert(30, pulse.Delay(10, self.d0)) - ref_sched = ref_sched.insert(40, pulse.Delay(20, self.d0)) ref_sched = ref_sched.insert(60, pulse.Delay(10, self.d0)) - ref_sched = ref_sched.insert(70, pulse.Delay(20, self.d0)) ref_sched = ref_sched.insert(90, pulse.Delay(10, self.d0)) self.assertScheduleEqual(block, ref_sched) diff --git a/test/python/pulse/test_builder.py b/test/python/pulse/test_builder.py index d907a0bebdbb..8aa985e92b0a 100644 --- a/test/python/pulse/test_builder.py +++ b/test/python/pulse/test_builder.py @@ -17,9 +17,9 @@ import numpy as np from qiskit import circuit, compiler, pulse -from qiskit.pulse import builder, exceptions, macros, transforms +from qiskit.pulse import builder, exceptions, macros from qiskit.pulse.instructions import directives -from qiskit.pulse.transforms import inline_subroutines +from qiskit.pulse.transforms import target_qobj_transform from qiskit.test import QiskitTestCase from qiskit.test.mock import FakeOpenPulse2Q from qiskit.test.mock.utils import ConfigurableFakeBackend as ConfigurableBackend @@ -36,6 +36,13 @@ def setUp(self): self.defaults = self.backend.defaults() self.inst_map = self.defaults.instruction_schedule_map + def assertScheduleEqual(self, program, target): + """Assert an error when two pulse programs are not equal. + + .. note:: Two programs are converted into standard execution format then compared. + """ + self.assertEqual(target_qobj_transform(program), target_qobj_transform(target)) + class TestBuilderBase(TestBuilder): """Test builder base.""" @@ -50,7 +57,7 @@ def test_schedule_supplied(self): with pulse.build(schedule=reference) as schedule: pass - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) self.assertEqual(schedule.name, 'reference') def test_default_alignment_left(self): @@ -67,7 +74,7 @@ def test_default_alignment_left(self): pulse.delay(10, d0) pulse.delay(20, d1) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_default_alignment_right(self): """Test default right alignment setting.""" @@ -83,7 +90,7 @@ def test_default_alignment_right(self): pulse.delay(10, d0) pulse.delay(20, d1) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_default_alignment_sequential(self): """Test default sequential alignment setting.""" @@ -94,13 +101,12 @@ def test_default_alignment_sequential(self): pulse.delay(10, d0) pulse.delay(20, d1) - reference = pulse.Schedule() - with pulse.build(reference) as reference: + with pulse.build() as reference: with pulse.align_sequential(): pulse.delay(10, d0) pulse.delay(20, d1) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) class TestContexts(TestBuilder): @@ -124,7 +130,7 @@ def test_align_sequential(self): # d1 reference.insert(3, instructions.Delay(5, d1), inplace=True) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_align_left(self): """Test the left alignment context.""" @@ -149,7 +155,7 @@ def test_align_left(self): # d2 reference.insert(0, instructions.Delay(11, d2), inplace=True) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_align_right(self): """Test the right alignment context.""" @@ -174,7 +180,7 @@ def test_align_right(self): # d2 reference.insert(0, instructions.Delay(11, d2), inplace=True) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_inline(self): """Test the inlining context.""" @@ -196,7 +202,7 @@ def test_inline(self): # d1 reference += instructions.Delay(5, d1) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_transpiler_settings(self): """Test the transpiler settings context. @@ -236,7 +242,7 @@ def test_scheduler_settings(self): with pulse.circuit_scheduler_settings(inst_map=inst_map): builder.call(x_qc) - self.assertEqual(schedule, ref_sched) + self.assertScheduleEqual(schedule, ref_sched) def test_phase_offset(self): """Test the phase offset context.""" @@ -251,7 +257,7 @@ def test_phase_offset(self): reference += instructions.Delay(10, d0) reference += instructions.ShiftPhase(-3.14, d0) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_frequency_offset(self): """Test the frequency offset context.""" @@ -266,7 +272,7 @@ def test_frequency_offset(self): reference += instructions.Delay(10, d0) reference += instructions.ShiftFrequency(-1e9, d0) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_phase_compensated_frequency_offset(self): """Test that the phase offset context properly compensates for phase @@ -284,7 +290,7 @@ def test_phase_compensated_frequency_offset(self): -2 * np.pi * ((1e9 * 10 * self.configuration.dt) % 1), d0) reference += instructions.ShiftFrequency(-1e9, d0) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) class TestChannels(TestBuilder): @@ -325,7 +331,7 @@ def test_delay(self): reference = pulse.Schedule() reference += instructions.Delay(10, d0) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_play_parametric_pulse(self): """Test play instruction with parametric pulse.""" @@ -338,7 +344,7 @@ def test_play_parametric_pulse(self): reference = pulse.Schedule() reference += instructions.Play(test_pulse, d0) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_play_sample_pulse(self): """Test play instruction with sample pulse.""" @@ -351,7 +357,7 @@ def test_play_sample_pulse(self): reference = pulse.Schedule() reference += instructions.Play(test_pulse, d0) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_play_array_pulse(self): """Test play instruction on an array directly.""" @@ -365,7 +371,7 @@ def test_play_array_pulse(self): test_pulse = pulse.Waveform(test_array) reference += instructions.Play(test_pulse, d0) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_play_name_argument(self): """Test name argument for play instruction.""" @@ -388,7 +394,7 @@ def test_acquire_memory_slot(self): reference = pulse.Schedule() reference += pulse.Acquire(10, acquire0, mem_slot=mem0) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_acquire_register_slot(self): """Test acquire instruction into register slot.""" @@ -401,7 +407,7 @@ def test_acquire_register_slot(self): reference = pulse.Schedule() reference += pulse.Acquire(10, acquire0, reg_slot=reg0) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_acquire_qubit(self): """Test acquire instruction on qubit.""" @@ -414,7 +420,7 @@ def test_acquire_qubit(self): reference = pulse.Schedule() reference += pulse.Acquire(10, acquire0, mem_slot=mem0) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_instruction_name_argument(self): """Test setting the name of an instruction.""" @@ -436,7 +442,7 @@ def test_set_frequency(self): reference = pulse.Schedule() reference += instructions.SetFrequency(1e9, d0) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_shift_frequency(self): """Test shift frequency instruction.""" @@ -448,7 +454,7 @@ def test_shift_frequency(self): reference = pulse.Schedule() reference += instructions.ShiftFrequency(0.1e9, d0) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_set_phase(self): """Test set phase instruction.""" @@ -460,7 +466,7 @@ def test_set_phase(self): reference = pulse.Schedule() reference += instructions.SetPhase(3.14, d0) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_shift_phase(self): """Test shift phase instruction.""" @@ -472,7 +478,7 @@ def test_shift_phase(self): reference = pulse.Schedule() reference += instructions.ShiftPhase(3.14, d0) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_snapshot(self): """Test snapshot instruction.""" @@ -482,7 +488,7 @@ def test_snapshot(self): reference = pulse.Schedule() reference += instructions.Snapshot('test', 'state') - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) class TestDirectives(TestBuilder): @@ -503,8 +509,6 @@ def test_barrier_with_align_right(self): pulse.delay(5, d1) pulse.delay(7, d0) - schedule = transforms.remove_directives(schedule) - reference = pulse.Schedule() # d0 reference.insert(0, instructions.Delay(3, d0), inplace=True) @@ -514,7 +518,7 @@ def test_barrier_with_align_right(self): # d2 reference.insert(3, instructions.Delay(11, d2), inplace=True) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_barrier_with_align_left(self): """Test barrier directive with left alignment context.""" @@ -531,8 +535,6 @@ def test_barrier_with_align_left(self): pulse.delay(5, d1) pulse.delay(7, d0) - schedule = transforms.remove_directives(schedule) - reference = pulse.Schedule() # d0 reference.insert(0, instructions.Delay(3, d0), inplace=True) @@ -542,14 +544,14 @@ def test_barrier_with_align_left(self): # d2 reference.insert(3, instructions.Delay(11, d2), inplace=True) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_barrier_on_qubits(self): """Test barrier directive on qubits.""" with pulse.build(self.backend) as schedule: pulse.barrier(0, 1) - reference = pulse.Schedule() + reference = pulse.ScheduleBlock() reference += directives.RelativeBarrier( pulse.DriveChannel(0), pulse.DriveChannel(1), @@ -567,7 +569,7 @@ def test_trivial_barrier(self): with pulse.build() as schedule: pulse.barrier(pulse.DriveChannel(0)) - self.assertEqual(schedule, pulse.Schedule()) + self.assertEqual(schedule, pulse.ScheduleBlock()) class TestUtilities(TestBuilder): @@ -585,9 +587,9 @@ def test_append_schedule(self): reference += instructions.Delay(10, d0) with pulse.build() as schedule: - builder.append_schedule(reference) + builder.call(reference) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_append_instruction(self): """Test appending an instruction to the active builder.""" @@ -597,7 +599,7 @@ def test_append_instruction(self): with pulse.build() as schedule: builder.append_instruction(instruction) - self.assertEqual(schedule, instruction) + self.assertScheduleEqual(schedule, (0, instruction)) def test_qubit_channels(self): """Test getting the qubit channels of the active builder's backend.""" @@ -696,7 +698,7 @@ def test(): reference += pulse.Play(pulse.Constant(100, 1.0), pulse.DriveChannel(0)) reference += pulse.Play(pulse.Gaussian(100, 0.5, 20), pulse.DriveChannel(0)) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_measure(self): """Test utility function - measure.""" @@ -710,7 +712,7 @@ def test_measure(self): inst_map=self.inst_map, meas_map=self.configuration.meas_map) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_measure_multi_qubits(self): """Test utility function - measure with multi qubits.""" @@ -724,7 +726,7 @@ def test_measure_multi_qubits(self): inst_map=self.inst_map, meas_map=self.configuration.meas_map) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_measure_all(self): """Test utility function - measure.""" @@ -734,7 +736,7 @@ def test_measure_all(self): self.assertEqual(regs, [pulse.MemorySlot(0), pulse.MemorySlot(1)]) reference = macros.measure_all(self.backend) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) backend_100q = ConfigurableBackend('100q', 100) with pulse.build(backend_100q) as schedule: @@ -743,7 +745,7 @@ def test_measure_all(self): reference = backend_100q.defaults().instruction_schedule_map.\ get('measure', list(range(100))) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_delay_qubit(self): """Test delaying on a qubit macro.""" @@ -763,7 +765,7 @@ def test_delay_qubit(self): reference += instructions.Delay(10, u0) reference += instructions.Delay(10, u1) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_delay_qubits(self): """Test delaying on multiple qubits to make sure we don't insert delays twice.""" @@ -789,7 +791,7 @@ def test_delay_qubits(self): reference += instructions.Delay(10, u0) reference += instructions.Delay(10, u1) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) class TestGates(TestBuilder): @@ -804,7 +806,7 @@ def test_cx(self): reference_qc.cx(0, 1) reference = compiler.schedule(reference_qc, self.backend) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_u1(self): """Test u1 gate.""" @@ -815,7 +817,7 @@ def test_u1(self): reference_qc.append(circuit.library.U1Gate(np.pi), [0]) reference = compiler.schedule(reference_qc, self.backend) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_u2(self): """Test u2 gate.""" @@ -826,7 +828,7 @@ def test_u2(self): reference_qc.append(circuit.library.U2Gate(np.pi, 0), [0]) reference = compiler.schedule(reference_qc, self.backend) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_u3(self): """Test u3 gate.""" @@ -837,7 +839,7 @@ def test_u3(self): reference_qc.append(circuit.library.U3Gate(np.pi, 0, np.pi/2), [0]) reference = compiler.schedule(reference_qc, self.backend) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_x(self): """Test x gate.""" @@ -849,7 +851,7 @@ def test_x(self): reference_qc = compiler.transpile(reference_qc, self.backend) reference = compiler.schedule(reference_qc, self.backend) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_lazy_evaluation_with_transpiler(self): """Test that the two cx gates are optimizied away by the transpiler.""" @@ -860,7 +862,7 @@ def test_lazy_evaluation_with_transpiler(self): reference_qc = circuit.QuantumCircuit(2) reference = compiler.schedule(reference_qc, self.backend) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_measure(self): """Test pulse measurement macro against circuit measurement and @@ -876,7 +878,7 @@ def test_measure(self): reference_qc = compiler.transpile(reference_qc, self.backend) reference = compiler.schedule(reference_qc, self.backend) - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_backend_require(self): """Test that a backend is required to use a gate.""" @@ -962,7 +964,7 @@ def test_complex_build(self): inplace=True) reference += measure_reference - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) class TestSubroutineCall(TestBuilder): @@ -984,13 +986,13 @@ def test_call(self): with pulse.align_right(): builder.call(reference) - self.assertEqual(schedule, ref_sched) + self.assertScheduleEqual(schedule, ref_sched) with pulse.build() as schedule: with pulse.align_right(): pulse.call(reference) - self.assertEqual(schedule, ref_sched) + self.assertScheduleEqual(schedule, ref_sched) def test_call_circuit(self): """Test calling circuit instruction.""" @@ -1011,7 +1013,7 @@ def test_call_circuit(self): with pulse.align_right(): builder.call(u1_qc) - self.assertEqual(schedule, ref_sched) + self.assertScheduleEqual(schedule, ref_sched) def test_call_circuit_with_cregs(self): """Test calling of circuit wiht classical registers.""" @@ -1030,7 +1032,7 @@ def test_call_circuit_with_cregs(self): ref_sched = pulse.Schedule() ref_sched += pulse.instructions.Call(reference) - self.assertEqual(schedule, ref_sched) + self.assertScheduleEqual(schedule, ref_sched) def test_call_gate_and_circuit(self): """Test calling circuit with gates.""" @@ -1064,7 +1066,7 @@ def test_call_gate_and_circuit(self): reference += cx_reference reference += measure_reference << reference.duration - self.assertEqual(schedule, reference) + self.assertScheduleEqual(schedule, reference) def test_subroutine_not_transpiled(self): """Test called circuit is frozen as a subroutine.""" @@ -1079,7 +1081,7 @@ def test_subroutine_not_transpiled(self): pulse.call(subprogram) pulse.call(subprogram) - self.assertNotEqual(len(inline_subroutines(schedule).instructions), 0) + self.assertNotEqual(len(target_qobj_transform(schedule).instructions), 0) def test_subroutine_not_transformed(self): """Test called schedule is not transformed.""" @@ -1090,7 +1092,7 @@ def test_subroutine_not_transformed(self): subprogram.insert(0, pulse.Delay(30, d0), inplace=True) subprogram.insert(10, pulse.Delay(10, d1), inplace=True) - with pulse.build() as schedule: + with pulse.build() as target: with pulse.align_right(): pulse.delay(10, d1) pulse.call(subprogram) @@ -1100,9 +1102,7 @@ def test_subroutine_not_transformed(self): reference.insert(10, pulse.Delay(30, d0), inplace=True) reference.insert(20, pulse.Delay(10, d1), inplace=True) - target = inline_subroutines(schedule) - - self.assertEqual(target, reference) + self.assertScheduleEqual(target, reference) def test_deepcopying_subroutine(self): """Test if deepcopying the schedule can copy inline subroutine.""" @@ -1116,8 +1116,8 @@ def test_deepcopying_subroutine(self): copied_prog = deepcopy(main_prog) - main_call = main_prog.instructions[0][1] - copy_call = copied_prog.instructions[0][1] + main_call = main_prog.instructions[0] + copy_call = copied_prog.instructions[0] self.assertNotEqual(id(main_call), id(copy_call)) @@ -1134,7 +1134,7 @@ def test_call_with_parameters(self): self.assertEqual(main_prog.is_parameterized(), False) - assigned_sched = inline_subroutines(main_prog) + assigned_sched = target_qobj_transform(main_prog) play_0 = assigned_sched.instructions[0][1] play_1 = assigned_sched.instructions[1][1] @@ -1158,7 +1158,7 @@ def test_call_partly_with_parameters(self): main_prog.assign_parameters({amp: 0.5}) self.assertEqual(main_prog.is_parameterized(), False) - assigned_sched = inline_subroutines(main_prog) + assigned_sched = target_qobj_transform(main_prog) play_0 = assigned_sched.instructions[0][1] play_1 = assigned_sched.instructions[1][1] @@ -1188,7 +1188,7 @@ def test_call_with_common_parameter(self): with pulse.build() as main_prog: pulse.call(subroutine, amp=0.1) - assigned_sched = inline_subroutines(main_prog) + assigned_sched = target_qobj_transform(main_prog) play_0 = assigned_sched.instructions[0][1] play_1 = assigned_sched.instructions[1][1] @@ -1209,7 +1209,7 @@ def test_call_with_parameter_name_collision(self): with pulse.build() as main_prog: pulse.call(subroutine, value_dict={amp1: 0.1, amp2: 0.2}, sigma=40) - assigned_sched = inline_subroutines(main_prog) + assigned_sched = target_qobj_transform(main_prog) play_0 = assigned_sched.instructions[0][1] play_1 = assigned_sched.instructions[1][1] diff --git a/test/python/pulse/test_instruction_schedule_map.py b/test/python/pulse/test_instruction_schedule_map.py index bbde340e15e8..016a24f1464a 100644 --- a/test/python/pulse/test_instruction_schedule_map.py +++ b/test/python/pulse/test_instruction_schedule_map.py @@ -17,7 +17,7 @@ from qiskit.circuit.library.standard_gates import U1Gate, U3Gate, CXGate, XGate from qiskit.circuit.parameter import Parameter from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.pulse import (InstructionScheduleMap, Play, PulseError, Schedule, +from qiskit.pulse import (InstructionScheduleMap, Play, PulseError, Schedule, ScheduleBlock, Waveform, ShiftPhase) from qiskit.pulse.channels import DriveChannel from qiskit.qobj import PulseQobjInstruction @@ -32,7 +32,7 @@ class TestInstructionScheduleMap(QiskitTestCase): def test_add(self): """Test add, and that errors are raised when expected.""" sched = Schedule() - sched.append(Play(Waveform(np.ones(5)), DriveChannel(0))) + sched.append(Play(Waveform(np.ones(5)), DriveChannel(0)), inplace=True) inst_map = InstructionScheduleMap() inst_map.add('u1', 1, sched) @@ -47,6 +47,19 @@ def test_add(self): with self.assertRaises(PulseError): inst_map.add('u1', 1, "not a schedule") + def test_add_block(self): + """Test add block, and that errors are raised when expected.""" + sched = ScheduleBlock() + sched.append(Play(Waveform(np.ones(5)), DriveChannel(0)), inplace=True) + inst_map = InstructionScheduleMap() + + inst_map.add('u1', 1, sched) + inst_map.add('u1', 0, sched) + + self.assertIn('u1', inst_map.instructions) + self.assertEqual(inst_map.qubits_with_instruction('u1'), [0, 1]) + self.assertTrue('u1' in inst_map.qubit_instructions(0)) + def test_instructions(self): """Test `instructions`.""" sched = Schedule() @@ -117,7 +130,16 @@ def test_qubit_instructions(self): def test_get(self): """Test `get`.""" sched = Schedule() - sched.append(Play(Waveform(np.ones(5)), DriveChannel(0))) + sched.append(Play(Waveform(np.ones(5)), DriveChannel(0)), inplace=True) + inst_map = InstructionScheduleMap() + inst_map.add('x', 0, sched) + + self.assertEqual(sched, inst_map.get('x', (0,))) + + def test_get_block(self): + """Test `get` block.""" + sched = ScheduleBlock() + sched.append(Play(Waveform(np.ones(5)), DriveChannel(0)), inplace=True) inst_map = InstructionScheduleMap() inst_map.add('x', 0, sched) diff --git a/test/python/pulse/test_instructions.py b/test/python/pulse/test_instructions.py index cb2e9a2c4eb7..a5272f419817 100644 --- a/test/python/pulse/test_instructions.py +++ b/test/python/pulse/test_instructions.py @@ -16,7 +16,7 @@ from qiskit import pulse, circuit from qiskit.pulse import channels, configuration, instructions, library, exceptions -from qiskit.pulse.transforms import inline_subroutines +from qiskit.pulse.transforms import inline_subroutines, target_qobj_transform from qiskit.test import QiskitTestCase @@ -283,7 +283,7 @@ def test_assign_parameters_to_call(self): pulse.play(pulse.Gaussian(160, 0.5, 40), pulse.DriveChannel(0)) pulse.play(pulse.Gaussian(160, 0.1, 40), pulse.DriveChannel(0)) - self.assertEqual(test_sched, ref_sched) + self.assertEqual(target_qobj_transform(test_sched), target_qobj_transform(ref_sched)) def test_call_initialize_with_parameter(self): """Test call instruction with parameterized subroutine with initial dict.""" @@ -295,7 +295,8 @@ def test_call_initialize_with_parameter(self): pulse.play(pulse.Gaussian(160, 0.5, 40), pulse.DriveChannel(0)) pulse.play(pulse.Gaussian(160, 0.1, 40), pulse.DriveChannel(0)) - self.assertEqual(call.assigned_subroutine(), ref_sched) + self.assertEqual(target_qobj_transform(call.assigned_subroutine()), + target_qobj_transform(ref_sched)) def test_call_subroutine_with_different_parameters(self): """Test call subroutines with different parameters in the same schedule.""" @@ -306,8 +307,6 @@ def test_call_subroutine_with_different_parameters(self): pulse.call(self.function, value_dict=init_dict1) pulse.call(self.function, value_dict=init_dict2) - test_sched = inline_subroutines(test_sched) - with pulse.build() as ref_sched: pulse.play(pulse.Gaussian(160, 0.1, 40), pulse.DriveChannel(0)) pulse.play(pulse.Gaussian(160, 0.5, 40), pulse.DriveChannel(0)) @@ -316,4 +315,4 @@ def test_call_subroutine_with_different_parameters(self): pulse.play(pulse.Gaussian(160, 0.7, 40), pulse.DriveChannel(0)) pulse.play(pulse.Gaussian(160, 0.3, 40), pulse.DriveChannel(0)) - self.assertEqual(test_sched, ref_sched) + self.assertEqual(target_qobj_transform(test_sched), target_qobj_transform(ref_sched)) diff --git a/test/python/pulse/test_transforms.py b/test/python/pulse/test_transforms.py index f01b4d3fcbe9..ee4e4b8d3674 100644 --- a/test/python/pulse/test_transforms.py +++ b/test/python/pulse/test_transforms.py @@ -686,9 +686,7 @@ def test_equispaced_with_longer_duration(self): reference = pulse.Schedule() reference.insert(0, Delay(10, d0), inplace=True) - reference.insert(10, Delay(10, d0), inplace=True) reference.insert(20, Delay(10, d0), inplace=True) - reference.insert(30, Delay(10, d0), inplace=True) reference.insert(40, Delay(10, d0), inplace=True) self.assertEqual(sched, reference) @@ -725,8 +723,6 @@ def test_equispaced_with_multiple_channels_longer_duration(self): reference = pulse.Schedule() reference.insert(0, Delay(10, d0), inplace=True) - reference.insert(10, Delay(20, d0), inplace=True) - reference.insert(0, Delay(10, d1), inplace=True) reference.insert(10, Delay(20, d1), inplace=True) self.assertEqual(sched, reference) @@ -768,13 +764,9 @@ def test_numerical_with_longer_duration(self): sched = transforms.align_func(sched, duration=80, func=self._position) reference = pulse.Schedule() - reference.insert(0, Delay(15, d0), inplace=True) reference.insert(15, Delay(10, d0), inplace=True) - reference.insert(25, Delay(10, d0), inplace=True) reference.insert(35, Delay(10, d0), inplace=True) - reference.insert(45, Delay(10, d0), inplace=True) reference.insert(55, Delay(10, d0), inplace=True) - reference.insert(65, Delay(15, d0), inplace=True) self.assertEqual(sched, reference) diff --git a/test/python/scheduler/test_basic_scheduler.py b/test/python/scheduler/test_basic_scheduler.py index 5c5479ad1c2a..275b51bb426e 100644 --- a/test/python/scheduler/test_basic_scheduler.py +++ b/test/python/scheduler/test_basic_scheduler.py @@ -17,8 +17,9 @@ from qiskit.circuit.library import U1Gate, U2Gate, U3Gate from qiskit.exceptions import QiskitError from qiskit.pulse import (Schedule, DriveChannel, AcquireChannel, Acquire, - MeasureChannel, MemorySlot, Gaussian, Play) -from qiskit.pulse import build, macros + MeasureChannel, MemorySlot, Gaussian, Play, + transforms) +from qiskit.pulse import build, macros, play, InstructionScheduleMap from qiskit.test.mock import FakeBackend, FakeOpenPulse2Q, FakeOpenPulse3Q from qiskit.test import QiskitTestCase @@ -419,8 +420,29 @@ def test_scheduler_with_params_not_bound(self): qc = QuantumCircuit(2) qc.append(Gate('pulse_gate', 1, [x]), [0]) with build() as expected_schedule: - Play(Gaussian(duration=160, amp=x, sigma=40), DriveChannel(0)) + play(Gaussian(duration=160, amp=x, sigma=40), DriveChannel(0)) qc.add_calibration(gate='pulse_gate', qubits=[0], schedule=expected_schedule, params=[x]) sched = schedule(qc, self.backend) self.assertEqual(sched, - expected_schedule) + transforms.target_qobj_transform(expected_schedule)) + + def test_schedule_block_in_instmap(self): + """Test schedule block in instmap can be scheduled.""" + duration = Parameter('duration') + + with build() as pulse_prog: + play(Gaussian(duration, 0.1, 10), DriveChannel(0)) + + instmap = InstructionScheduleMap() + instmap.add('block_gate', (0,), pulse_prog, ['duration']) + + qc = QuantumCircuit(1) + qc.append(Gate('block_gate', 1, [duration]), [0]) + qc.assign_parameters({duration: 100}, inplace=True) + + sched = schedule(qc, self.backend, inst_map=instmap) + + ref_sched = Schedule() + ref_sched += Play(Gaussian(100, 0.1, 10), DriveChannel(0)) + + self.assertEqual(sched, ref_sched) diff --git a/test/python/visualization/pulse_v2/test_events.py b/test/python/visualization/pulse_v2/test_events.py index 5b687cdaed61..683fd9bf2ff5 100644 --- a/test/python/visualization/pulse_v2/test_events.py +++ b/test/python/visualization/pulse_v2/test_events.py @@ -148,12 +148,12 @@ def test_zero_duration_delay(self): """ ch = pulse.DriveChannel(0) - with pulse.build() as test_sched: - pulse.play(pulse.Gaussian(160, 0.1, 40), ch) - pulse.delay(0, ch) - pulse.play(pulse.Gaussian(160, 0.1, 40), ch) - pulse.delay(1, ch) - pulse.play(pulse.Gaussian(160, 0.1, 40), ch) + test_sched = pulse.Schedule() + test_sched += pulse.Play(pulse.Gaussian(160, 0.1, 40), ch) + test_sched += pulse.Delay(0, ch) + test_sched += pulse.Play(pulse.Gaussian(160, 0.1, 40), ch) + test_sched += pulse.Delay(1, ch) + test_sched += pulse.Play(pulse.Gaussian(160, 0.1, 40), ch) ch_events = events.ChannelEvents.load_program(test_sched, ch)