diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c0868cbd5bde..28636f7c8ac3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -24,8 +24,6 @@ # Qiskit folders (also their corresponding tests) providers/ @Qiskit/terra-core @jyu00 -pulse/ @Qiskit/terra-core @eggerdj @wshanks @nkanazawa1989 -scheduler/ @Qiskit/terra-core @eggerdj @wshanks @nkanazawa1989 visualization/ @Qiskit/terra-core @nonhermitian primitives/ @Qiskit/terra-core @Qiskit/qiskit-primitives # Override the release notes directories to have _no_ code owners, so any review diff --git a/crates/accelerate/src/basis/basis_translator/mod.rs b/crates/accelerate/src/basis/basis_translator/mod.rs index 26f7d7d3051b..b42f39c0a3d1 100644 --- a/crates/accelerate/src/basis/basis_translator/mod.rs +++ b/crates/accelerate/src/basis/basis_translator/mod.rs @@ -367,6 +367,7 @@ fn extract_basis_target( /// This needs to use a Python instance of `QuantumCircuit` due to it needing /// to access `has_calibration_for()` which is unavailable through rust. However, /// this API will be removed with the deprecation of `Pulse`. +/// TODO: pulse is removed, we can use op.blocks fn extract_basis_target_circ( circuit: &Bound, source_basis: &mut IndexSet, diff --git a/docs/apidoc/index.rst b/docs/apidoc/index.rst index e718a5be507e..e8a05c4037fc 100644 --- a/docs/apidoc/index.rst +++ b/docs/apidoc/index.rst @@ -68,13 +68,6 @@ Serialization: qasm3 qpy -Pulse-level programming: - -.. toctree:: - :maxdepth: 1 - - pulse - Other: .. toctree:: diff --git a/docs/apidoc/pulse.rst b/docs/apidoc/pulse.rst deleted file mode 100644 index 980821d10bfb..000000000000 --- a/docs/apidoc/pulse.rst +++ /dev/null @@ -1,6 +0,0 @@ -.. _qiskit-pulse: - -.. automodule:: qiskit.pulse - :no-members: - :no-inherited-members: - :no-special-members: diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index cc3c22707d0f..71ee428facad 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -22,7 +22,6 @@ from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.dagcircuit import DAGCircuit from qiskit.providers.backend import Backend -from qiskit.pulse import Schedule from qiskit.transpiler import Layout, CouplingMap, PropertySet from qiskit.transpiler.basepasses import BasePass from qiskit.transpiler.exceptions import TranspilerError, CircuitTooWideForTarget @@ -302,16 +301,7 @@ def callback_func(**kwargs): if not circuits: return [] - # transpiling schedules is not supported yet. start_time = time() - if all(isinstance(c, Schedule) for c in circuits): - warnings.warn("Transpiling schedules is not supported yet.", UserWarning) - end_time = time() - _log_transpile_time(start_time, end_time) - if arg_circuits_list: - return circuits - else: - return circuits[0] if optimization_level is None: # Take optimization level from the configuration or 1 as default. diff --git a/qiskit/providers/__init__.py b/qiskit/providers/__init__.py index c011693b2102..c93d4b9b1d5f 100644 --- a/qiskit/providers/__init__.py +++ b/qiskit/providers/__init__.py @@ -128,8 +128,7 @@ a backend and its operation for the :mod:`~qiskit.transpiler` so that circuits can be compiled to something that is optimized and can execute on the backend. It also provides the :meth:`~qiskit.providers.BackendV2.run` method which can -run the :class:`~qiskit.circuit.QuantumCircuit` objects and/or -:class:`~qiskit.pulse.Schedule` objects. This enables users and other Qiskit +run the :class:`~qiskit.circuit.QuantumCircuit` objects. This enables users and other Qiskit APIs to get results from executing circuits on devices in a standard fashion regardless of how the backend is implemented. At a high level the basic diff --git a/qiskit/providers/backend.py b/qiskit/providers/backend.py index fcf067017f37..0cbb8443421b 100644 --- a/qiskit/providers/backend.py +++ b/qiskit/providers/backend.py @@ -18,10 +18,9 @@ from abc import ABC from abc import abstractmethod import datetime -from typing import List, Union, Iterable, Tuple +from typing import List, Union, Tuple from qiskit.circuit.gate import Instruction -from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency class Backend: @@ -202,7 +201,7 @@ def instruction_durations(self): @property @abstractmethod def max_circuits(self): - """The maximum number of circuits (or Pulse schedules) that can be + """The maximum number of circuits that can be run in a single job. If there is no limit this will return None @@ -267,18 +266,6 @@ def meas_map(self) -> List[List[int]]: """ raise NotImplementedError - @property - @deprecate_pulse_dependency(is_property=True) - def instruction_schedule_map(self): - """Return the :class:`~qiskit.pulse.InstructionScheduleMap` for the - instructions defined in this backend's target.""" - return self._instruction_schedule_map - - @property - def _instruction_schedule_map(self): - """An alternative private path to be used internally to avoid pulse deprecation warnings.""" - return self.target._get_instruction_schedule_map() - def qubit_properties( self, qubit: Union[int, List[int]] ) -> Union[QubitProperties, List[QubitProperties]]: @@ -313,77 +300,6 @@ def qubit_properties( return self.target.qubit_properties[qubit] return [self.target.qubit_properties[q] for q in qubit] - @deprecate_pulse_dependency - def drive_channel(self, qubit: int): - """Return the drive channel for the given qubit. - - This is required to be implemented if the backend supports Pulse - scheduling. - - Returns: - DriveChannel: The Qubit drive channel - - Raises: - NotImplementedError: if the backend doesn't support querying the - measurement mapping - """ - raise NotImplementedError - - @deprecate_pulse_dependency - def measure_channel(self, qubit: int): - """Return the measure stimulus channel for the given qubit. - - This is required to be implemented if the backend supports Pulse - scheduling. - - Returns: - MeasureChannel: The Qubit measurement stimulus line - - Raises: - NotImplementedError: if the backend doesn't support querying the - measurement mapping - """ - raise NotImplementedError - - @deprecate_pulse_dependency - def acquire_channel(self, qubit: int): - """Return the acquisition channel for the given qubit. - - This is required to be implemented if the backend supports Pulse - scheduling. - - Returns: - AcquireChannel: The Qubit measurement acquisition line. - - Raises: - NotImplementedError: if the backend doesn't support querying the - measurement mapping - """ - raise NotImplementedError - - @deprecate_pulse_dependency - def control_channel(self, qubits: Iterable[int]): - """Return the secondary drive channel for the given qubit - - This is typically utilized for controlling multiqubit interactions. - This channel is derived from other channels. - - This is required to be implemented if the backend supports Pulse - scheduling. - - Args: - qubits: Tuple or list of qubits of the form - ``(control_qubit, target_qubit)``. - - Returns: - List[ControlChannel]: The multi qubit control line. - - Raises: - NotImplementedError: if the backend doesn't support querying the - measurement mapping - """ - raise NotImplementedError - def set_options(self, **fields): """Set the options fields for the backend @@ -433,9 +349,8 @@ def run(self, run_input, **options): class can handle either situation. Args: - run_input (QuantumCircuit or Schedule or ScheduleBlock or list): An - individual or a list of :class:`.QuantumCircuit`, - :class:`~qiskit.pulse.ScheduleBlock`, or :class:`~qiskit.pulse.Schedule` objects to + run_input (QuantumCircuit or list): An + individual or a list of :class:`.QuantumCircuit` objects to run on the backend. options: Any kwarg options to pass to the backend for running the config. If a key is also present in the options diff --git a/qiskit/providers/fake_provider/generic_backend_v2.py b/qiskit/providers/fake_provider/generic_backend_v2.py index 4d4aded058b7..a65da731cd84 100644 --- a/qiskit/providers/fake_provider/generic_backend_v2.py +++ b/qiskit/providers/fake_provider/generic_backend_v2.py @@ -308,16 +308,13 @@ def run(self, run_input, **options): """Run on the backend using a simulator. This method runs circuit jobs (an individual or a list of :class:`~.QuantumCircuit` - ) and pulse jobs (an individual or a list of :class:`~.Schedule` or - :class:`~.ScheduleBlock`) using :class:`~.BasicSimulator` or Aer simulator and returns a + ) using :class:`~.BasicSimulator` or Aer simulator and returns a :class:`~qiskit.providers.Job` object. If qiskit-aer is installed, jobs will be run using the ``AerSimulator`` with noise model of the backend. Otherwise, jobs will be run using the ``BasicSimulator`` simulator without noise. - Noisy simulations of pulse jobs are not yet supported in :class:`~.GenericBackendV2`. - Args: run_input (QuantumCircuit or list): An individual or a list of :class:`~qiskit.circuit.QuantumCircuit` @@ -332,7 +329,8 @@ def run(self, run_input, **options): Job: The job object for the run Raises: - QiskitError: If a pulse job is supplied and qiskit_aer is not installed. + QiskitError: If input is not :class:`~qiskit.circuit.QuantumCircuit` or a list of + :class:`~qiskit.circuit.QuantumCircuit` objects. """ circuits = run_input if not isinstance(circuits, QuantumCircuit) and ( diff --git a/qiskit/pulse/__init__.py b/qiskit/pulse/__init__.py deleted file mode 100644 index 58f13cda060e..000000000000 --- a/qiskit/pulse/__init__.py +++ /dev/null @@ -1,158 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# 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. - -r""" -=========================== -Pulse (:mod:`qiskit.pulse`) -=========================== - -.. currentmodule:: qiskit.pulse - -Qiskit-Pulse is a pulse-level quantum programming kit. This lower level of -programming offers the user more control than programming with -:py:class:`~qiskit.circuit.QuantumCircuit`\ s. - -Extracting the greatest performance from quantum hardware requires real-time -pulse-level instructions. Pulse answers that need: it enables the quantum -physicist *user* to specify the exact time dynamics of an experiment. -It is especially powerful for error mitigation techniques. - -The input is given as arbitrary, time-ordered signals (see: :ref:`Instructions `) -scheduled in parallel over multiple virtual hardware or simulator resources -(see: :ref:`Channels `). The system also allows the user to recover the -time dynamics of the measured output. - -This is sufficient to allow the quantum physicist to explore and correct for -noise in a quantum system. - -.. automodule:: qiskit.pulse.instructions -.. automodule:: qiskit.pulse.library -.. automodule:: qiskit.pulse.channels -.. automodule:: qiskit.pulse.schedule -.. automodule:: qiskit.pulse.transforms -.. automodule:: qiskit.pulse.builder - -.. currentmodule:: qiskit.pulse - -Configuration -============= - -.. autosummary:: - :toctree: ../stubs/ - - InstructionScheduleMap - -Exceptions -========== - -.. autoexception:: PulseError -.. autoexception:: BackendNotSet -.. autoexception:: NoActiveBuilder -.. autoexception:: UnassignedDurationError -.. autoexception:: UnassignedReferenceError -""" - -# Builder imports. -from qiskit.pulse.builder import ( - # Construction methods. - active_backend, - build, - num_qubits, - qubit_channels, - samples_to_seconds, - seconds_to_samples, - # Instructions. - acquire, - barrier, - call, - delay, - play, - reference, - set_frequency, - set_phase, - shift_frequency, - shift_phase, - snapshot, - # Channels. - acquire_channel, - control_channels, - drive_channel, - measure_channel, - # Contexts. - align_equispaced, - align_func, - align_left, - align_right, - align_sequential, - frequency_offset, - phase_offset, - # Macros. - macro, - measure, - measure_all, - delay_qubits, -) -from qiskit.pulse.channels import ( - AcquireChannel, - ControlChannel, - DriveChannel, - MeasureChannel, - MemorySlot, - RegisterSlot, - SnapshotChannel, -) -from qiskit.pulse.configuration import ( - Discriminator, - Kernel, - LoConfig, - LoRange, -) -from qiskit.pulse.exceptions import ( - PulseError, - BackendNotSet, - NoActiveBuilder, - UnassignedDurationError, - UnassignedReferenceError, -) -from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap -from qiskit.pulse.instructions import ( - Acquire, - Delay, - Instruction, - Play, - SetFrequency, - SetPhase, - ShiftFrequency, - ShiftPhase, - Snapshot, -) -from qiskit.pulse.library import ( - Constant, - Drag, - Gaussian, - GaussianSquare, - GaussianSquareDrag, - gaussian_square_echo, - Sin, - Cos, - Sawtooth, - Triangle, - Square, - GaussianDeriv, - Sech, - SechDeriv, - SymbolicPulse, - ScalableSymbolicPulse, - Waveform, -) -from qiskit.pulse.library.samplers.decorators import functional_pulse -from qiskit.pulse.schedule import Schedule, ScheduleBlock diff --git a/qiskit/pulse/builder.py b/qiskit/pulse/builder.py deleted file mode 100644 index 2f383911e658..000000000000 --- a/qiskit/pulse/builder.py +++ /dev/null @@ -1,2077 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -r""" - -.. _pulse_builder: - -============= -Pulse Builder -============= - -.. - We actually want people to think of these functions as being defined within the ``qiskit.pulse`` - namespace, not the submodule ``qiskit.pulse.builder``. - -.. currentmodule: qiskit.pulse - -Use the pulse builder DSL to write pulse programs with an imperative syntax. - -.. warning:: - The pulse builder interface is still in active development. It may have - breaking API changes without deprecation warnings in future releases until - otherwise indicated. - - -The pulse builder provides an imperative API for writing pulse programs -with less difficulty than the :class:`~qiskit.pulse.Schedule` API. -It contextually constructs a pulse schedule and then emits the schedule for -execution. For example, to play a series of pulses on channels is as simple as: - - -.. plot:: - :alt: Output from the previous code. - :include-source: - - from qiskit import pulse - - dc = pulse.DriveChannel - d0, d1, d2, d3, d4 = dc(0), dc(1), dc(2), dc(3), dc(4) - - with pulse.build(name='pulse_programming_in') as pulse_prog: - pulse.play([1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1], d0) - pulse.play([1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0], d1) - pulse.play([1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0], d2) - pulse.play([1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0], d3) - pulse.play([1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0], d4) - - pulse_prog.draw() - -To begin pulse programming we must first initialize our program builder -context with :func:`build`, after which we can begin adding program -statements. For example, below we write a simple program that :func:`play`\s -a pulse: - -.. plot:: - :alt: Output from the previous code. - :include-source: - - from qiskit import pulse - - d0 = pulse.DriveChannel(0) - - with pulse.build() as pulse_prog: - pulse.play(pulse.Constant(100, 1.0), d0) - - pulse_prog.draw() - -The builder initializes a :class:`.pulse.Schedule`, ``pulse_prog`` -and then begins to construct the program within the context. The output pulse -schedule will survive after the context is exited and can be used like a -normal Qiskit schedule. - -Pulse programming has a simple imperative style. This leaves the programmer -to worry about the raw experimental physics of pulse programming and not -constructing cumbersome data structures. - -We can optionally pass a :class:`~qiskit.providers.Backend` to -:func:`build` to enable enhanced functionality. Below, we prepare a Bell state -by automatically compiling the required pulses from their gate-level -representations, while simultaneously applying a long decoupling pulse to a -neighboring qubit. We terminate the experiment with a measurement to observe the -state we prepared. This program which mixes circuits and pulses will be -automatically lowered to be run as a pulse program. - -With the pulse builder we are able to blend programming on qubits and channels. -While the pulse schedule is based on instructions that operate on -channels, the pulse builder automatically handles the mapping from qubits to -channels for you. - -.. autofunction:: build - - -Channels -======== - -Methods to return the correct channels for the respective qubit indices. - -.. code-block:: python - - from qiskit import pulse - from qiskit.providers.fake_provider import GenericBackendV2 - - backend = GenericBackendV2(num_qubits=2) - - with pulse.build(backend) as drive_sched: - d0 = pulse.drive_channel(0) - print(d0) - -.. code-block:: text - - DriveChannel(0) - -.. autofunction:: acquire_channel -.. autofunction:: control_channels -.. autofunction:: drive_channel -.. autofunction:: measure_channel - - -Instructions -============ - -Pulse instructions are available within the builder interface. Here's an example: - -.. code-block:: python - - from qiskit import pulse - from qiskit.providers.fake_provider import GenericBackendV2 - - backend = GenericBackendV2(num_qubits=2) - - with pulse.build(backend) as drive_sched: - d0 = pulse.drive_channel(0) - a0 = pulse.acquire_channel(0) - - pulse.play(pulse.Constant(10, 1.0), d0) - pulse.delay(20, d0) - pulse.shift_phase(3.14/2, d0) - pulse.set_phase(3.14, d0) - pulse.shift_frequency(1e7, d0) - pulse.set_frequency(5e9, d0) - - with pulse.build() as temp_sched: - pulse.play(pulse.Gaussian(20, 1.0, 3.0), d0) - pulse.play(pulse.Gaussian(20, -1.0, 3.0), d0) - - pulse.call(temp_sched) - pulse.acquire(30, a0, pulse.MemorySlot(0)) - - drive_sched.draw() - -.. autofunction:: acquire -.. autofunction:: barrier -.. autofunction:: call -.. autofunction:: delay -.. autofunction:: play -.. autofunction:: reference -.. autofunction:: set_frequency -.. autofunction:: set_phase -.. autofunction:: shift_frequency -.. autofunction:: shift_phase -.. autofunction:: snapshot - - -Contexts -======== - -Builder aware contexts that modify the construction of a pulse program. For -example an alignment context like :func:`align_right` may -be used to align all pulses as late as possible in a pulse program. - -.. code-block:: python - - from qiskit import pulse - - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - - with pulse.build() as pulse_prog: - 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 - pulse.play(pulse.Constant(20, 1.0), d1) - - pulse_prog.draw() - -.. autofunction:: align_equispaced -.. autofunction:: align_func -.. autofunction:: align_left -.. autofunction:: align_right -.. autofunction:: align_sequential -.. autofunction:: frequency_offset -.. autofunction:: phase_offset - - -Macros -====== - -Macros help you add more complex functionality to your pulse program. - -.. code-block:: python - - from qiskit import pulse - from qiskit.providers.fake_provider import GenericBackendV2 - - backend = GenericBackendV2(num_qubits=2) - - with pulse.build(backend) as measure_sched: - mem_slot = pulse.measure(0) - print(mem_slot) - -.. code-block:: text - - MemorySlot(0) - -.. autofunction:: measure -.. autofunction:: measure_all -.. autofunction:: delay_qubits - - -Utilities -========= - -The utility functions can be used to gather attributes about the backend and modify -how the program is built. - -.. code-block:: python - - from qiskit import pulse - - from qiskit.providers.fake_provider import GenericBackendV2 - - backend = GenericBackendV2(num_qubits=2) - - with pulse.build(backend) as u3_sched: - print('Number of qubits in backend: {}'.format(pulse.num_qubits())) - - samples = 160 - print('There are {} samples in {} seconds'.format( - samples, pulse.samples_to_seconds(160))) - - seconds = 1e-6 - print('There are {} seconds in {} samples.'.format( - seconds, pulse.seconds_to_samples(1e-6))) - -.. code-block:: text - - Number of qubits in backend: 1 - There are 160 samples in 3.5555555555555554e-08 seconds - There are 1e-06 seconds in 4500 samples. - -.. autofunction:: active_backend -.. autofunction:: num_qubits -.. autofunction:: qubit_channels -.. autofunction:: samples_to_seconds -.. autofunction:: seconds_to_samples -""" -from __future__ import annotations -import contextvars -import functools -import itertools -import sys -import uuid -import warnings -from collections.abc import Generator, Callable, Iterable -from contextlib import contextmanager -from functools import singledispatchmethod -from typing import TypeVar, ContextManager, TypedDict, Union, Optional, Dict - -import numpy as np - -from qiskit.circuit.parameterexpression import ParameterExpression, ParameterValueType -from qiskit.pulse import ( - channels as chans, - configuration, - exceptions, - instructions, - macros, - library, - transforms, -) -from qiskit.providers.backend import BackendV2 -from qiskit.pulse.instructions import directives -from qiskit.pulse.schedule import Schedule, ScheduleBlock -from qiskit.pulse.transforms.alignments import AlignmentKind -from qiskit.utils.deprecate_pulse import deprecate_pulse_func - - -if sys.version_info >= (3, 12): - from typing import Unpack -else: - from typing_extensions import Unpack - -#: contextvars.ContextVar[BuilderContext]: active builder -BUILDER_CONTEXTVAR: contextvars.ContextVar["_PulseBuilder"] = contextvars.ContextVar("backend") - -T = TypeVar("T") - -StorageLocation = Union[chans.MemorySlot, chans.RegisterSlot] - - -def _requires_backend(function: Callable[..., T]) -> Callable[..., T]: - """Decorator a function to raise if it is called without a builder with a - set backend. - """ - - @functools.wraps(function) - def wrapper(self, *args, **kwargs): - if self.backend is None: - raise exceptions.BackendNotSet( - 'This function requires the builder to have a "backend" set.' - ) - return function(self, *args, **kwargs) - - return wrapper - - -class _PulseBuilder: - """Builder context class.""" - - __alignment_kinds__ = { - "left": transforms.AlignLeft(), - "right": transforms.AlignRight(), - "sequential": transforms.AlignSequential(), - } - - def __init__( - self, - backend=None, - block: ScheduleBlock | None = None, - name: str | None = None, - default_alignment: str | AlignmentKind = "left", - ): - """Initialize the builder context. - - .. note:: - At some point we may consider incorporating the builder into - the :class:`~qiskit.pulse.Schedule` class. However, the risk of - this is tying the user interface to the intermediate - representation. For now we avoid this at the cost of some code - duplication. - - Args: - backend (Backend): Input backend to use in - builder. If not set certain functionality will be unavailable. - 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. - - Raises: - PulseError: When invalid ``default_alignment`` or `block` is specified. - """ - #: Backend: Backend instance for context builder. - self._backend = backend - - # Token for this ``_PulseBuilder``'s ``ContextVar``. - self._backend_ctx_token: contextvars.Token[_PulseBuilder] | None = None - - # Stack of context. - self._context_stack: list[ScheduleBlock] = [] - - #: 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 = self._naive_typecast_schedule(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 - if isinstance(default_alignment, AlignmentKind): # AlignmentKind instance - alignment = default_alignment - else: # str identifier - 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. - - Returns: - The schedule that the builder will build on. - """ - self._backend_ctx_token = BUILDER_CONTEXTVAR.set(self) - output = self._context_stack[0] - output._name = self._name or output.name - - return output - - def __exit__(self, exc_type, exc_val, exc_tb): - """Exit the builder context and compile the built pulse program.""" - self.compile() - BUILDER_CONTEXTVAR.reset(self._backend_ctx_token) - - @property - def backend(self): - """Returns the builder backend if set. - - Returns: - Optional[Backend]: The builder's backend. - """ - return self._backend - - def push_context(self, alignment: AlignmentKind): - """Push new context to the stack.""" - self._context_stack.append(ScheduleBlock(alignment_context=alignment)) - - 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_subroutine` or `.append_instruction` method. - Use above methods rather than directly accessing to the current context. - """ - return self._context_stack[-1] - - @property - @_requires_backend - def num_qubits(self): - """Get the number of qubits in the backend.""" - # backendV2 - if isinstance(self.backend, BackendV2): - return self.backend.num_qubits - return self.backend.configuration().n_qubits - - 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. - - while len(self._context_stack) > 1: - current = self.pop_context() - self.append_subroutine(current) - - return self._context_stack[0] - - 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) - - def append_reference(self, name: str, *extra_keys: str): - """Add external program as a :class:`~qiskit.pulse.instructions.Reference` instruction. - - Args: - name: Name of subroutine. - extra_keys: Assistance keys to uniquely specify the subroutine. - """ - inst = instructions.Reference(name, *extra_keys) - self.append_instruction(inst) - - def append_subroutine(self, subroutine: Schedule | ScheduleBlock): - """Append a :class:`ScheduleBlock` to the builder's context schedule. - - This operation doesn't create a reference. Subroutine is directly - appended to current context schedule. - - Args: - subroutine: ScheduleBlock to append to the current context block. - - Raises: - PulseError: When subroutine is not Schedule nor ScheduleBlock. - """ - if not isinstance(subroutine, (ScheduleBlock, Schedule)): - raise exceptions.PulseError( - f"'{subroutine.__class__.__name__}' is not valid data format in the builder. " - "'Schedule' and 'ScheduleBlock' can be appended to the builder context." - ) - - if len(subroutine) == 0: - return - if isinstance(subroutine, Schedule): - subroutine = self._naive_typecast_schedule(subroutine) - self._context_stack[-1].append(subroutine) - - @singledispatchmethod - def call_subroutine( - self, - subroutine: Schedule | ScheduleBlock, - name: str | None = None, - value_dict: dict[ParameterExpression, ParameterValueType] | None = None, - **kw_params: ParameterValueType, - ): - """Call a schedule or circuit defined outside of the current scope. - - The ``subroutine`` is appended to the context schedule as a call instruction. - This logic just generates a convenient program representation in the compiler. - Thus, this doesn't affect execution of inline subroutines. - See :class:`~pulse.instructions.Call` for more details. - - Args: - subroutine: Target schedule or circuit to append to the current context. - name: Name of subroutine if defined. - value_dict: Parameter object and assigned value mapping. This is more precise way to - identify a parameter since mapping is managed with unique object id rather than - name. Especially there is any name collision in a parameter table. - kw_params: Parameter values to bind to the target subroutine - with string parameter names. If there are parameter name overlapping, - these parameters are updated with the same assigned value. - - Raises: - PulseError: - - When input subroutine is not valid data format. - """ - raise exceptions.PulseError( - f"Subroutine type {subroutine.__class__.__name__} is " - "not valid data format. Call " - "Schedule, or ScheduleBlock." - ) - - @call_subroutine.register - def _( - self, - target_block: ScheduleBlock, - name: Optional[str] = None, - value_dict: Optional[Dict[ParameterExpression, ParameterValueType]] = None, - **kw_params: ParameterValueType, - ): - if len(target_block) == 0: - return - - # Create local parameter assignment - local_assignment = {} - for param_name, value in kw_params.items(): - params = target_block.get_parameters(param_name) - if not params: - raise exceptions.PulseError( - f"Parameter {param_name} is not defined in the target subroutine. " - f'{", ".join(map(str, target_block.parameters))} can be specified.' - ) - for param in params: - local_assignment[param] = value - - if value_dict: - if local_assignment.keys() & value_dict.keys(): - warnings.warn( - "Some parameters provided by 'value_dict' conflict with one through " - "keyword arguments. Parameter values in the keyword arguments " - "are overridden by the dictionary values.", - UserWarning, - ) - local_assignment.update(value_dict) - - if local_assignment: - target_block = target_block.assign_parameters(local_assignment, inplace=False) - - if name is None: - # Add unique string, not to accidentally override existing reference entry. - keys: tuple[str, ...] = (target_block.name, uuid.uuid4().hex) - else: - keys = (name,) - - self.append_reference(*keys) - self.get_context().assign_references({keys: target_block}, inplace=True) - - @call_subroutine.register - def _( - self, - target_schedule: Schedule, - name: Optional[str] = None, - value_dict: Optional[Dict[ParameterExpression, ParameterValueType]] = None, - **kw_params: ParameterValueType, - ): - if len(target_schedule) == 0: - return - - self.call_subroutine( - self._naive_typecast_schedule(target_schedule), - name=name, - value_dict=value_dict, - **kw_params, - ) - - @staticmethod - def _naive_typecast_schedule(schedule: Schedule): - # Naively convert into ScheduleBlock - from qiskit.pulse.transforms import inline_subroutines, flatten, pad - - preprocessed_schedule = inline_subroutines(flatten(schedule)) - pad(preprocessed_schedule, inplace=True, pad_with=instructions.TimeBlockade) - - # default to left alignment, namely ASAP scheduling - target_block = ScheduleBlock(name=schedule.name) - for _, inst in preprocessed_schedule.instructions: - target_block.append(inst, inplace=True) - - return target_block - - def get_dt(self): - """Retrieve dt differently based on the type of Backend""" - if isinstance(self.backend, BackendV2): - return self.backend.dt - return self.backend.configuration().dt - - -@deprecate_pulse_func -def build( - backend=None, - schedule: ScheduleBlock | None = None, - name: str | None = None, - default_alignment: str | AlignmentKind | None = "left", -) -> ContextManager[ScheduleBlock]: - """Create a context manager for launching the imperative pulse builder DSL. - - Args: - backend (Backend): A Qiskit backend. If not supplied certain - builder functionality will be unavailable. - 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 context. - - Returns: - A new builder context which has the active builder initialized. - """ - return _PulseBuilder( - backend=backend, - block=schedule, - name=name, - default_alignment=default_alignment, - ) - - -# Builder Utilities - - -def _active_builder() -> _PulseBuilder: - """Get the active builder in the active context. - - Returns: - The active active builder in this context. - - Raises: - exceptions.NoActiveBuilder: If a pulse builder function is called - outside of a builder context. - """ - try: - return BUILDER_CONTEXTVAR.get() - except LookupError as ex: - raise exceptions.NoActiveBuilder( - "A Pulse builder function was called outside of " - "a builder context. Try calling within a builder " - 'context, eg., "with pulse.build() as schedule: ...".' - ) from ex - - -@deprecate_pulse_func -def active_backend(): - """Get the backend of the currently active builder context. - - Returns: - Backend: The active backend in the currently active - builder context. - - Raises: - exceptions.BackendNotSet: If the builder does not have a backend set. - """ - builder = _active_builder().backend - if builder is None: - raise exceptions.BackendNotSet( - 'This function requires the active builder to have a "backend" set.' - ) - return builder - - -def append_schedule(schedule: Schedule | ScheduleBlock): - """Call a schedule by appending to the active builder's context block. - - Args: - schedule: Schedule or ScheduleBlock to append. - """ - _active_builder().append_subroutine(schedule) - - -def append_instruction(instruction: instructions.Instruction): - """Append an instruction to the active builder's context schedule. - - Examples: - - .. code-block:: python - - from qiskit import pulse - - d0 = pulse.DriveChannel(0) - - with pulse.build() as pulse_prog: - pulse.builder.append_instruction(pulse.Delay(10, d0)) - - print(pulse_prog.instructions) - - .. code-block:: text - - ((0, Delay(10, DriveChannel(0))),) - """ - _active_builder().append_instruction(instruction) - - -@deprecate_pulse_func -def num_qubits() -> int: - """Return number of qubits in the currently active backend. - - Examples: - - .. code-block:: python - - from qiskit import pulse - from qiskit.providers.fake_provider import FakeOpenPulse2Q - backend = FakeOpenPulse2Q() - with pulse.build(backend): - print(pulse.num_qubits()) - - .. code-block:: text - - 2 - - .. note:: Requires the active builder context to have a backend set. - """ - if isinstance(active_backend(), BackendV2): - return active_backend().num_qubits - return active_backend().configuration().n_qubits - - -@deprecate_pulse_func -def seconds_to_samples(seconds: float | np.ndarray) -> int | np.ndarray: - """Obtain the number of samples that will elapse in ``seconds`` on the - active backend. - - Rounds down. - - Args: - seconds: Time in seconds to convert to samples. - - Returns: - The number of samples for the time to elapse - """ - dt = _active_builder().get_dt() - if isinstance(seconds, np.ndarray): - return (seconds / dt).astype(int) - return int(seconds / dt) - - -@deprecate_pulse_func -def samples_to_seconds(samples: int | np.ndarray) -> float | np.ndarray: - """Obtain the time in seconds that will elapse for the input number of - samples on the active backend. - - Args: - samples: Number of samples to convert to time in seconds. - - Returns: - The time that elapses in ``samples``. - """ - return samples * _active_builder().get_dt() - - -@deprecate_pulse_func -def qubit_channels(qubit: int) -> set[chans.Channel]: - """Returns the set of channels associated with a qubit. - - Examples: - - .. code-block:: python - - from qiskit import pulse - from qiskit.providers.fake_provider import FakeOpenPulse2Q - backend = FakeOpenPulse2Q() - with pulse.build(backend): - print(pulse.qubit_channels(0)) - - .. code-block:: text - - {MeasureChannel(0), ControlChannel(0), DriveChannel(0), AcquireChannel(0), ControlChannel(1)} - - .. note:: Requires the active builder context to have a backend set. - - .. note:: A channel may still be associated with another qubit in this list - such as in the case where significant crosstalk exists. - - """ - - # implement as the inner function to avoid API change for a patch release in 0.24.2. - def get_qubit_channels_v2(backend: BackendV2, qubit: int): - r"""Return a list of channels which operate on the given ``qubit``. - Returns: - List of ``Channel``\s operated on my the given ``qubit``. - """ - channels = [] - - # add multi-qubit channels - for node_qubits in backend.coupling_map: - if qubit in node_qubits: - control_channel = backend.control_channel(node_qubits) - if control_channel: - channels.extend(control_channel) - - # add single qubit channels - channels.append(backend.drive_channel(qubit)) - channels.append(backend.measure_channel(qubit)) - channels.append(backend.acquire_channel(qubit)) - return channels - - # backendV2 - if isinstance(active_backend(), BackendV2): - return set(get_qubit_channels_v2(active_backend(), qubit)) - return set(active_backend().configuration().get_qubit_channels(qubit)) - - -def _qubits_to_channels(*channels_or_qubits: int | chans.Channel) -> set[chans.Channel]: - """Returns the unique channels of the input qubits.""" - channels = set() - for channel_or_qubit in channels_or_qubits: - if isinstance(channel_or_qubit, int): - channels |= qubit_channels(channel_or_qubit) - elif isinstance(channel_or_qubit, chans.Channel): - channels.add(channel_or_qubit) - else: - raise exceptions.PulseError( - f'{channel_or_qubit} is not a "Channel" or qubit (integer).' - ) - return channels - - -# Contexts - - -@contextmanager -@deprecate_pulse_func -def align_left() -> Generator[None, None, None]: - """Left alignment pulse scheduling context. - - Pulse instructions within this context are scheduled as early as possible - by shifting them left to the earliest available time. - - Examples: - - .. plot:: - :include-source: - :nofigs: - - from qiskit import pulse - - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - - with pulse.build() as pulse_prog: - with pulse.align_left(): - # this pulse will start at t=0 - 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_subroutine(current) - - -@contextmanager -@deprecate_pulse_func -def align_right() -> Generator[None, None, None]: - """Right alignment pulse scheduling context. - - Pulse instructions within this context are scheduled as late as possible - by shifting them right to the latest available time. - - Examples: - - .. plot:: - :include-source: - :nofigs: - - from qiskit import pulse - - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - - with pulse.build() as pulse_prog: - 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 - 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_subroutine(current) - - -@contextmanager -@deprecate_pulse_func -def align_sequential() -> Generator[None, None, None]: - """Sequential alignment pulse scheduling context. - - Pulse instructions within this context are scheduled sequentially in time - such that no two instructions will be played at the same time. - - Examples: - - .. plot:: - :include-source: - :nofigs: - - from qiskit import pulse - - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - - with pulse.build() as pulse_prog: - with pulse.align_sequential(): - # this pulse will start at t=0 - 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_subroutine(current) - - -@contextmanager -@deprecate_pulse_func -def align_equispaced(duration: int | ParameterExpression) -> Generator[None, None, None]: - """Equispaced alignment pulse scheduling context. - - Pulse instructions within this context are scheduled with the same interval spacing such that - the total length of the context block is ``duration``. - If the total free ``duration`` cannot be evenly divided by the number of instructions - within the context, the modulo is split and then prepended and appended to - the returned schedule. Delay instructions are automatically inserted in between pulses. - - This context is convenient to write a schedule for periodical dynamic decoupling or - the Hahn echo sequence. - - Examples: - - .. plot:: - :alt: Output from the previous code. - :include-source: - - from qiskit import pulse - - d0 = pulse.DriveChannel(0) - x90 = pulse.Gaussian(10, 0.1, 3) - x180 = pulse.Gaussian(10, 0.2, 3) - - with pulse.build() as hahn_echo: - with pulse.align_equispaced(duration=100): - pulse.play(x90, d0) - pulse.play(x180, d0) - pulse.play(x90, d0) - - hahn_echo.draw() - - 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_subroutine(current) - - -@contextmanager -@deprecate_pulse_func -def align_func( - duration: int | ParameterExpression, func: Callable[[int], float] -) -> Generator[None, None, None]: - """Callback defined alignment pulse scheduling context. - - Pulse instructions within this context are scheduled at the location specified by - arbitrary callback function `position` that takes integer index and returns - the associated fractional location within [0, 1]. - Delay instruction is automatically inserted in between pulses. - - This context may be convenient to write a schedule of arbitrary dynamical decoupling - sequences such as Uhrig dynamical decoupling. - - Examples: - - .. plot:: - :alt: Output from the previous code. - :include-source: - - import numpy as np - from qiskit import pulse - - d0 = pulse.DriveChannel(0) - x90 = pulse.Gaussian(10, 0.1, 3) - x180 = pulse.Gaussian(10, 0.2, 3) - - def udd10_pos(j): - return np.sin(np.pi*j/(2*10 + 2))**2 - - with pulse.build() as udd_sched: - pulse.play(x90, d0) - with pulse.align_func(duration=300, func=udd10_pos): - for _ in range(10): - pulse.play(x180, d0) - pulse.play(x90, d0) - - udd_sched.draw() - - Args: - duration: Duration of context. This should be larger than the schedule duration. - func: A function that takes an index of sub-schedule and returns the - fractional coordinate of of that sub-schedule. - 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_subroutine(current) - - -@contextmanager -def general_transforms(alignment_context: AlignmentKind) -> Generator[None, None, None]: - """Arbitrary alignment transformation defined by a subclass instance of - :class:`~qiskit.pulse.transforms.alignments.AlignmentKind`. - - Args: - alignment_context: Alignment context instance that defines schedule transformation. - - Yields: - None - - Raises: - PulseError: When input ``alignment_context`` is not ``AlignmentKind`` subclasses. - """ - 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_subroutine(current) - - -@contextmanager -@deprecate_pulse_func -def phase_offset(phase: float, *channels: chans.PulseChannel) -> Generator[None, None, None]: - """Shift the phase of input channels on entry into context and undo on exit. - - Examples: - - .. plot:: - :include-source: - :nofigs: - - import math - - from qiskit import pulse - - d0 = pulse.DriveChannel(0) - - with pulse.build() as pulse_prog: - with pulse.phase_offset(math.pi, d0): - pulse.play(pulse.Constant(10, 1.0), d0) - - assert len(pulse_prog.instructions) == 3 - - Args: - phase: Amount of phase offset in radians. - channels: Channels to offset phase of. - - Yields: - None - """ - for channel in channels: - shift_phase(phase, channel) - try: - yield - finally: - for channel in channels: - shift_phase(-phase, channel) - - -@contextmanager -@deprecate_pulse_func -def frequency_offset( - frequency: float, *channels: chans.PulseChannel, compensate_phase: bool = False -) -> Generator[None, None, None]: - """Shift the frequency of inputs channels on entry into context and undo on exit. - - Examples: - - .. code-block:: python - - from qiskit import pulse - from qiskit.providers.fake_provider import FakeOpenPulse2Q - - backend = FakeOpenPulse2Q() - d0 = pulse.DriveChannel(0) - - with pulse.build(backend) as pulse_prog: - # shift frequency by 1GHz - with pulse.frequency_offset(1e9, d0): - pulse.play(pulse.Constant(10, 1.0), d0) - - assert len(pulse_prog.instructions) == 3 - - with pulse.build(backend) as pulse_prog: - # Shift frequency by 1GHz. - # Undo accumulated phase in the shifted frequency frame - # when exiting the context. - with pulse.frequency_offset(1e9, d0, compensate_phase=True): - pulse.play(pulse.Constant(10, 1.0), d0) - - assert len(pulse_prog.instructions) == 4 - - - Args: - frequency: Amount of frequency offset in Hz. - channels: Channels to offset frequency of. - compensate_phase: Compensate for accumulated phase accumulated with - respect to the channels' frame at its initial frequency. - - Yields: - None - """ - builder = _active_builder() - # 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) - try: - yield - finally: - if compensate_phase: - duration = builder.get_context().duration - t0 - - accumulated_phase = 2 * np.pi * ((duration * builder.get_dt() * frequency) % 1) - for channel in channels: - shift_phase(-accumulated_phase, channel) - - for channel in channels: - shift_frequency(-frequency, channel) - - -# Channels -@deprecate_pulse_func -def drive_channel(qubit: int) -> chans.DriveChannel: - """Return ``DriveChannel`` for ``qubit`` on the active builder backend. - - Examples: - - .. code-block:: python - - from qiskit import pulse - from qiskit.providers.fake_provider import FakeOpenPulse2Q - - backend = FakeOpenPulse2Q() - - with pulse.build(backend): - assert pulse.drive_channel(0) == pulse.DriveChannel(0) - - .. note:: Requires the active builder context to have a backend set. - """ - # backendV2 - if isinstance(active_backend(), BackendV2): - return active_backend().drive_channel(qubit) - return active_backend().configuration().drive(qubit) - - -@deprecate_pulse_func -def measure_channel(qubit: int) -> chans.MeasureChannel: - """Return ``MeasureChannel`` for ``qubit`` on the active builder backend. - - Examples: - - .. code-block:: python - - from qiskit import pulse - from qiskit.providers.fake_provider import FakeOpenPulse2Q - - backend = FakeOpenPulse2Q() - - with pulse.build(backend): - assert pulse.measure_channel(0) == pulse.MeasureChannel(0) - - .. note:: Requires the active builder context to have a backend set. - """ - # backendV2 - if isinstance(active_backend(), BackendV2): - return active_backend().measure_channel(qubit) - return active_backend().configuration().measure(qubit) - - -@deprecate_pulse_func -def acquire_channel(qubit: int) -> chans.AcquireChannel: - """Return ``AcquireChannel`` for ``qubit`` on the active builder backend. - - Examples: - - .. code-block:: python - - from qiskit import pulse - from qiskit.providers.fake_provider import FakeOpenPulse2Q - - backend = FakeOpenPulse2Q() - - with pulse.build(backend): - assert pulse.acquire_channel(0) == pulse.AcquireChannel(0) - - .. note:: Requires the active builder context to have a backend set. - """ - # backendV2 - if isinstance(active_backend(), BackendV2): - return active_backend().acquire_channel(qubit) - return active_backend().configuration().acquire(qubit) - - -@deprecate_pulse_func -def control_channels(*qubits: Iterable[int]) -> list[chans.ControlChannel]: - """Return ``ControlChannel`` for ``qubit`` on the active builder backend. - - Return the secondary drive channel for the given qubit -- typically - utilized for controlling multi-qubit interactions. - - Examples: - - .. code-block:: python - - from qiskit import pulse - from qiskit.providers.fake_provider import FakeOpenPulse2Q - - backend = FakeOpenPulse2Q() - - with pulse.build(backend): - assert pulse.control_channels(0, 1) == [pulse.ControlChannel(0)] - - .. note:: Requires the active builder context to have a backend set. - - Args: - qubits: Tuple or list of ordered qubits of the form - `(control_qubit, target_qubit)`. - - Returns: - List of control channels associated with the supplied ordered list - of qubits. - """ - # backendV2 - if isinstance(active_backend(), BackendV2): - return active_backend().control_channel(qubits) - return active_backend().configuration().control(qubits=qubits) - - -# Base Instructions -@deprecate_pulse_func -def delay(duration: int, channel: chans.Channel, name: str | None = None): - """Delay on a ``channel`` for a ``duration``. - - Examples: - - .. plot:: - :include-source: - :nofigs: - - from qiskit import pulse - - d0 = pulse.DriveChannel(0) - - with pulse.build() as pulse_prog: - pulse.delay(10, d0) - - Args: - duration: Number of cycles to delay for on ``channel``. - channel: Channel to delay on. - name: Name of the instruction. - """ - append_instruction(instructions.Delay(duration, channel, name=name)) - - -@deprecate_pulse_func -def play(pulse: library.Pulse | np.ndarray, channel: chans.PulseChannel, name: str | None = None): - """Play a ``pulse`` on a ``channel``. - - Examples: - - .. plot:: - :include-source: - :nofigs: - - from qiskit import pulse - - d0 = pulse.DriveChannel(0) - - with pulse.build() as pulse_prog: - pulse.play(pulse.Constant(10, 1.0), d0) - - Args: - pulse: Pulse to play. - channel: Channel to play pulse on. - name: Name of the pulse. - """ - if not isinstance(pulse, library.Pulse): - pulse = library.Waveform(pulse) - - append_instruction(instructions.Play(pulse, channel, name=name)) - - -class _MetaDataType(TypedDict, total=False): - kernel: configuration.Kernel - discriminator: configuration.Discriminator - mem_slot: chans.MemorySlot - reg_slot: chans.RegisterSlot - name: str - - -@deprecate_pulse_func -def acquire( - duration: int, - qubit_or_channel: int | chans.AcquireChannel, - register: StorageLocation, - **metadata: Unpack[_MetaDataType], -): - """Acquire for a ``duration`` on a ``channel`` and store the result - in a ``register``. - - Examples: - - .. plot:: - :include-source: - :nofigs: - - from qiskit import pulse - - acq0 = pulse.AcquireChannel(0) - mem0 = pulse.MemorySlot(0) - - with pulse.build() as pulse_prog: - pulse.acquire(100, acq0, mem0) - - # measurement metadata - kernel = pulse.configuration.Kernel('linear_discriminator') - pulse.acquire(100, acq0, mem0, kernel=kernel) - - .. note:: The type of data acquire will depend on the execution ``meas_level``. - - Args: - duration: Duration to acquire data for - qubit_or_channel: Either the qubit to acquire data for or the specific - :class:`~qiskit.pulse.channels.AcquireChannel` to acquire on. - register: Location to store measured result. - metadata: Additional metadata for measurement. See - :class:`~qiskit.pulse.instructions.Acquire` for more information. - - Raises: - exceptions.PulseError: If the register type is not supported. - """ - if isinstance(qubit_or_channel, int): - qubit_or_channel = chans.AcquireChannel(qubit_or_channel) - - if isinstance(register, chans.MemorySlot): - append_instruction( - instructions.Acquire(duration, qubit_or_channel, mem_slot=register, **metadata) - ) - elif isinstance(register, chans.RegisterSlot): - append_instruction( - instructions.Acquire(duration, qubit_or_channel, reg_slot=register, **metadata) - ) - else: - raise exceptions.PulseError(f'Register of type: "{type(register)}" is not supported') - - -@deprecate_pulse_func -def set_frequency(frequency: float, channel: chans.PulseChannel, name: str | None = None): - """Set the ``frequency`` of a pulse ``channel``. - - Examples: - - .. plot:: - :include-source: - :nofigs: - - from qiskit import pulse - - d0 = pulse.DriveChannel(0) - - with pulse.build() as pulse_prog: - pulse.set_frequency(1e9, d0) - - Args: - frequency: Frequency in Hz to set channel to. - channel: Channel to set frequency of. - name: Name of the instruction. - """ - append_instruction(instructions.SetFrequency(frequency, channel, name=name)) - - -@deprecate_pulse_func -def shift_frequency(frequency: float, channel: chans.PulseChannel, name: str | None = None): - """Shift the ``frequency`` of a pulse ``channel``. - - Examples: - - .. plot:: - :include-source: - :nofigs: - - from qiskit import pulse - - d0 = pulse.DriveChannel(0) - - with pulse.build() as pulse_prog: - pulse.shift_frequency(1e9, d0) - - Args: - frequency: Frequency in Hz to shift channel frequency by. - channel: Channel to shift frequency of. - name: Name of the instruction. - """ - append_instruction(instructions.ShiftFrequency(frequency, channel, name=name)) - - -@deprecate_pulse_func -def set_phase(phase: float, channel: chans.PulseChannel, name: str | None = None): - """Set the ``phase`` of a pulse ``channel``. - - Examples: - - .. plot:: - :include-source: - :nofigs: - - import math - - from qiskit import pulse - - d0 = pulse.DriveChannel(0) - - with pulse.build() as pulse_prog: - pulse.set_phase(math.pi, d0) - - Args: - phase: Phase in radians to set channel carrier signal to. - channel: Channel to set phase of. - name: Name of the instruction. - """ - append_instruction(instructions.SetPhase(phase, channel, name=name)) - - -@deprecate_pulse_func -def shift_phase(phase: float, channel: chans.PulseChannel, name: str | None = None): - """Shift the ``phase`` of a pulse ``channel``. - - Examples: - - .. plot:: - :include-source: - :nofigs: - - import math - - from qiskit import pulse - - d0 = pulse.DriveChannel(0) - - with pulse.build() as pulse_prog: - pulse.shift_phase(math.pi, d0) - - Args: - phase: Phase in radians to shift channel carrier signal by. - channel: Channel to shift phase of. - name: Name of the instruction. - """ - append_instruction(instructions.ShiftPhase(phase, channel, name)) - - -@deprecate_pulse_func -def snapshot(label: str, snapshot_type: str = "statevector"): - """Simulator snapshot. - - Examples: - - .. plot:: - :include-source: - :nofigs: - - from qiskit import pulse - - with pulse.build() as pulse_prog: - pulse.snapshot('first', 'statevector') - - Args: - label: Label for snapshot. - snapshot_type: Type of snapshot. - """ - append_instruction(instructions.Snapshot(label, snapshot_type=snapshot_type)) - - -@deprecate_pulse_func -def call( - target: Schedule | ScheduleBlock | None, - name: str | None = None, - value_dict: dict[ParameterValueType, ParameterValueType] | None = None, - **kw_params: ParameterValueType, -): - """Call the subroutine within the currently active builder context with arbitrary - parameters which will be assigned to the target program. - - .. note:: - - If the ``target`` program is a :class:`.ScheduleBlock`, then a :class:`.Reference` - instruction will be created and appended to the current context. - The ``target`` program will be immediately assigned to the current scope as a subroutine. - If the ``target`` program is :class:`.Schedule`, it will be wrapped by the - :class:`.Call` instruction and appended to the current context to avoid - a mixed representation of :class:`.ScheduleBlock` and :class:`.Schedule`. - If the ``target`` program is a :class:`.QuantumCircuit` it will be scheduled - and the new :class:`.Schedule` will be added as a :class:`.Call` instruction. - - Examples: - - 1. Calling a schedule block (recommended) - - .. code-block:: python - - from qiskit import circuit, pulse - from qiskit.providers.fake_provider import GenericBackendV2 - - backend = GenericBackendV2(num_qubits=5) - - with pulse.build() as x_sched: - pulse.play(pulse.Gaussian(160, 0.1, 40), pulse.DriveChannel(0)) - - with pulse.build() as pulse_prog: - pulse.call(x_sched) - - print(pulse_prog) - - .. code-block:: text - - ScheduleBlock( - ScheduleBlock( - Play( - Gaussian(duration=160, amp=(0.1+0j), sigma=40), - DriveChannel(0) - ), - name="block0", - transform=AlignLeft() - ), - name="block1", - transform=AlignLeft() - ) - - The actual program is stored in the reference table attached to the schedule. - - .. code-block:: python - - print(pulse_prog.references) - - .. code-block:: text - - ReferenceManager: - - ('block0', '634b3b50bd684e26a673af1fbd2d6c81'): ScheduleBlock(Play(Gaussian(... - - In addition, you can call a parameterized target program with parameter assignment. - - .. code-block:: python - - amp = circuit.Parameter("amp") - - with pulse.build() as subroutine: - pulse.play(pulse.Gaussian(160, amp, 40), pulse.DriveChannel(0)) - - with pulse.build() as pulse_prog: - pulse.call(subroutine, amp=0.1) - pulse.call(subroutine, amp=0.3) - - print(pulse_prog) - - .. code-block:: text - - ScheduleBlock( - ScheduleBlock( - Play( - Gaussian(duration=160, amp=(0.1+0j), sigma=40), - DriveChannel(0) - ), - name="block2", - transform=AlignLeft() - ), - ScheduleBlock( - Play( - Gaussian(duration=160, amp=(0.3+0j), sigma=40), - DriveChannel(0) - ), - name="block2", - transform=AlignLeft() - ), - name="block3", - transform=AlignLeft() - ) - - If there is a name collision between parameters, you can distinguish them by specifying - each parameter object in a python dictionary. For example, - - .. code-block:: python - - amp1 = circuit.Parameter('amp') - amp2 = circuit.Parameter('amp') - - with pulse.build() as subroutine: - pulse.play(pulse.Gaussian(160, amp1, 40), pulse.DriveChannel(0)) - pulse.play(pulse.Gaussian(160, amp2, 40), pulse.DriveChannel(1)) - - with pulse.build() as pulse_prog: - pulse.call(subroutine, value_dict={amp1: 0.1, amp2: 0.3}) - - print(pulse_prog) - - .. code-block:: text - - ScheduleBlock( - ScheduleBlock( - Play(Gaussian(duration=160, amp=(0.1+0j), sigma=40), DriveChannel(0)), - Play(Gaussian(duration=160, amp=(0.3+0j), sigma=40), DriveChannel(1)), - name="block4", - transform=AlignLeft() - ), - name="block5", - transform=AlignLeft() - ) - - 2. Calling a schedule - - .. code-block:: python - - x_sched = backend.instruction_schedule_map.get("x", (0,)) - - with pulse.build(backend) as pulse_prog: - pulse.call(x_sched) - - print(pulse_prog) - - .. code-block:: text - - ScheduleBlock( - Call( - Schedule( - ( - 0, - Play( - Drag( - duration=160, - amp=(0.18989731546729305+0j), - sigma=40, - beta=-1.201258305015517, - name='drag_86a8' - ), - DriveChannel(0), - name='drag_86a8' - ) - ), - name="x" - ), - name='x' - ), - name="block6", - transform=AlignLeft() - ) - - Currently, the backend calibrated gates are provided in the form of :class:`~.Schedule`. - The parameter assignment mechanism is available also for schedules. - However, the called schedule is not treated as a reference. - - - Args: - target: Target circuit or pulse schedule to call. - name: Optional. A unique name of subroutine if defined. When the name is explicitly - provided, one cannot call different schedule blocks with the same name. - value_dict: Optional. Parameters assigned to the ``target`` program. - If this dictionary is provided, the ``target`` program is copied and - then stored in the main built schedule and its parameters are assigned to the given values. - This dictionary is keyed on :class:`~.Parameter` objects, - allowing parameter name collision to be avoided. - kw_params: Alternative way to provide parameters. - Since this is keyed on the string parameter name, - the parameters having the same name are all updated together. - If you want to avoid name collision, use ``value_dict`` with :class:`~.Parameter` - objects instead. - """ - _active_builder().call_subroutine(target, name, value_dict, **kw_params) - - -@deprecate_pulse_func -def reference(name: str, *extra_keys: str): - """Refer to undefined subroutine by string keys. - - A :class:`~qiskit.pulse.instructions.Reference` instruction is implicitly created - and a schedule can be separately registered to the reference at a later stage. - - .. plot:: - :include-source: - :nofigs: - - from qiskit import pulse - - with pulse.build() as main_prog: - pulse.reference("x_gate", "q0") - - with pulse.build() as subroutine: - pulse.play(pulse.Gaussian(160, 0.1, 40), pulse.DriveChannel(0)) - - main_prog.assign_references(subroutine_dict={("x_gate", "q0"): subroutine}) - - Args: - name: Name of subroutine. - extra_keys: Helper keys to uniquely specify the subroutine. - """ - _active_builder().append_reference(name, *extra_keys) - - -# Directives -@deprecate_pulse_func -def barrier(*channels_or_qubits: chans.Channel | int, name: str | None = None): - """Barrier directive for a set of channels and qubits. - - This directive prevents the compiler from moving instructions across - the barrier. Consider the case where we want to enforce that one pulse - happens after another on separate channels, this can be done with: - - .. code-block:: python - - from qiskit import pulse - from qiskit.providers.fake_provider import FakeOpenPulse2Q - - backend = FakeOpenPulse2Q() - - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - - with pulse.build(backend) as barrier_pulse_prog: - pulse.play(pulse.Constant(10, 1.0), d0) - pulse.barrier(d0, d1) - pulse.play(pulse.Constant(10, 1.0), d1) - - Of course this could have been accomplished with: - - .. code-block:: python - - from qiskit.pulse import transforms - - with pulse.build(backend) as aligned_pulse_prog: - with pulse.align_sequential(): - pulse.play(pulse.Constant(10, 1.0), d0) - pulse.play(pulse.Constant(10, 1.0), d1) - - 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 - scheduling alignment operations across channels. For example - in the case where we are calling an outside circuit or schedule and - want to align a pulse at the end of one call: - - .. code-block:: python - - import math - from qiskit import pulse - from qiskit.providers.fake_provider import FakeOpenPulse2Q - - backend = FakeOpenPulse2Q() - - d0 = pulse.DriveChannel(0) - - with pulse.build(backend) as pulse_prog: - with pulse.align_right(): - pulse.call(backend.defaults().instruction_schedule_map.get('u1', (1,))) - # Barrier qubit 1 and d0. - pulse.barrier(1, d0) - # Due to barrier this will play before the gate on qubit 1. - pulse.play(pulse.Constant(10, 1.0), d0) - # This will end at the same time as the pulse above due to - # the barrier. - pulse.call(backend.defaults().instruction_schedule_map.get('u1', (1,))) - - .. note:: Requires the active builder context to have a backend set if - qubits are barriered on. - - Args: - channels_or_qubits: Channels or qubits to barrier. - name: Name for the barrier - """ - channels = _qubits_to_channels(*channels_or_qubits) - if len(channels) > 1: - append_instruction(directives.RelativeBarrier(*channels, name=name)) - - -# Macros -def macro(func: Callable): - """Wrap a Python function and activate the parent builder context at calling time. - - This enables embedding Python functions as builder macros. This generates a new - :class:`pulse.Schedule` that is embedded in the parent builder context with - every call of the decorated macro function. The decorated macro function will - behave as if the function code was embedded inline in the parent builder context - after parameter substitution. - - Examples: - - .. code-block:: python - - from qiskit import pulse - from qiskit.providers.fake_provider import FakeOpenPulse2Q - - @pulse.macro - def measure(qubit: int): - pulse.play(pulse.GaussianSquare(16384, 256, 15872), pulse.measure_channel(qubit)) - mem_slot = pulse.MemorySlot(qubit) - pulse.acquire(16384, pulse.acquire_channel(qubit), mem_slot) - - return mem_slot - - backend = FakeOpenPulse2Q() - - with pulse.build(backend=backend) as sched: - mem_slot = measure(0) - print(f"Qubit measured into {mem_slot}") - - sched.draw() - - Args: - func: The Python function to enable as a builder macro. There are no - requirements on the signature of the function, any calls to pulse - builder methods will be added to builder context the wrapped function - is called from. - - Returns: - Callable: The wrapped ``func``. - """ - func_name = getattr(func, "__name__", repr(func)) - - @functools.wraps(func) - def wrapper(*args, **kwargs): - _builder = _active_builder() - # activate the pulse builder before calling the function - with build(backend=_builder.backend, name=func_name) as built: - output = func(*args, **kwargs) - - _builder.call_subroutine(built) - return output - - return wrapper - - -@deprecate_pulse_func -def measure( - qubits: list[int] | int, - registers: list[StorageLocation] | StorageLocation = None, -) -> list[StorageLocation] | StorageLocation: - """Measure a qubit within the currently active builder context. - - At the pulse level a measurement is composed of both a stimulus pulse and - an acquisition instruction which tells the systems measurement unit to - acquire data and process it. We provide this measurement macro to automate - the process for you, but if desired full control is still available with - :func:`acquire` and :func:`play`. - - For now it is not possible to do much with the handle to ``reg`` but in the - future we will support using this handle to a result register to build - up ones program. - - .. note:: Requires the active builder context to have a backend set. - - Args: - qubits: Physical qubit to measure. - registers: Register to store result in. If not selected the current - behavior is to return the :class:`MemorySlot` with the same - index as ``qubit``. This register will be returned. - Returns: - The ``register`` the qubit measurement result will be stored in. - """ - backend = active_backend() - - try: - qubits = list(qubits) - except TypeError: - qubits = [qubits] - - if registers is None: - registers = [chans.MemorySlot(qubit) for qubit in qubits] - else: - try: - registers = list(registers) - except TypeError: - registers = [registers] - measure_sched = macros.measure( - qubits=qubits, - backend=backend, - qubit_mem_slots={qubit: register.index for qubit, register in zip(qubits, registers)}, - ) - - # note this is not a subroutine. - # just a macro to automate combination of stimulus and acquisition. - # prepare unique reference name based on qubit and memory slot index. - qubits_repr = "&".join(map(str, qubits)) - mslots_repr = "&".join((str(r.index) for r in registers)) - _active_builder().call_subroutine(measure_sched, name=f"measure_{qubits_repr}..{mslots_repr}") - - if len(qubits) == 1: - return registers[0] - else: - return registers - - -@deprecate_pulse_func -def measure_all() -> list[chans.MemorySlot]: - r"""Measure all qubits within the currently active builder context. - - A simple macro function to measure all of the qubits in the device at the - same time. This is useful for handling device ``meas_map`` and single - measurement constraints. - - Examples: - - .. code-block:: python - - from qiskit import pulse - from qiskit.providers.fake_provider import FakeOpenPulse2Q - - backend = FakeOpenPulse2Q() - - with pulse.build(backend) as pulse_prog: - # Measure all qubits and return associated registers. - regs = pulse.measure_all() - - .. note:: - Requires the active builder context to have a backend set. - - Returns: - The ``register``\s the qubit measurement results will be stored in. - """ - backend = active_backend() - qubits = range(num_qubits()) - registers = [chans.MemorySlot(qubit) for qubit in qubits] - - measure_sched = macros.measure( - qubits=qubits, - backend=backend, - qubit_mem_slots={qubit: qubit for qubit in qubits}, - ) - - # note this is not a subroutine. - # just a macro to automate combination of stimulus and acquisition. - _active_builder().call_subroutine(measure_sched, name="measure_all") - - return registers - - -@deprecate_pulse_func -def delay_qubits(duration: int, *qubits: int): - r"""Insert delays on all the :class:`channels.Channel`\s that correspond - to the input ``qubits`` at the same time. - - Examples: - - .. code-block:: python - - from qiskit import pulse - from qiskit.providers.fake_provider import FakeOpenPulse3Q - - backend = FakeOpenPulse3Q() - - with pulse.build(backend) as pulse_prog: - # Delay for 100 cycles on qubits 0, 1 and 2. - regs = pulse.delay_qubits(100, 0, 1, 2) - - .. note:: Requires the active builder context to have a backend set. - - Args: - duration: Duration to delay for. - qubits: Physical qubits to delay on. Delays will be inserted based on - the channels returned by :func:`pulse.qubit_channels`. - """ - qubit_chans = set(itertools.chain.from_iterable(qubit_channels(qubit) for qubit in qubits)) - with align_left(): - for chan in qubit_chans: - delay(duration, chan) diff --git a/qiskit/pulse/calibration_entries.py b/qiskit/pulse/calibration_entries.py deleted file mode 100644 index 0055e48f0682..000000000000 --- a/qiskit/pulse/calibration_entries.py +++ /dev/null @@ -1,283 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Internal format of calibration data in target.""" -from __future__ import annotations -import inspect -from abc import ABCMeta, abstractmethod -from collections.abc import Sequence, Callable -from enum import IntEnum -from typing import Any - -from qiskit.pulse.exceptions import PulseError -from qiskit.pulse.schedule import Schedule, ScheduleBlock - - -IncompletePulseQobj = object() -"""A None-like constant that represents the PulseQobj is incomplete.""" - - -class CalibrationPublisher(IntEnum): - """Defines who defined schedule entry.""" - - BACKEND_PROVIDER = 0 - QISKIT = 1 - EXPERIMENT_SERVICE = 2 - - -class CalibrationEntry(metaclass=ABCMeta): - """A metaclass of a calibration entry. - - This class defines a standard model of Qiskit pulse program that is - agnostic to the underlying in-memory representation. - - This entry distinguishes whether this is provided by end-users or a backend - by :attr:`.user_provided` attribute which may be provided when - the actual calibration data is provided to the entry with by :meth:`define`. - - Note that a custom entry provided by an end-user may appear in the wire-format - as an inline calibration, e.g. :code:`defcal` of the QASM3, - that may update the backend instruction set architecture for execution. - - .. note:: - - This and built-in subclasses are expected to be private without stable user-facing API. - The purpose of this class is to wrap different - in-memory pulse program representations in Qiskit, so that it can provide - the standard data model and API which are primarily used by the transpiler ecosystem. - It is assumed that end-users will never directly instantiate this class, - but :class:`.Target` or :class:`.InstructionScheduleMap` internally use this data model - to avoid implementing a complicated branching logic to - manage different calibration data formats. - - """ - - @abstractmethod - def define(self, definition: Any, user_provided: bool): - """Attach definition to the calibration entry. - - Args: - definition: Definition of this entry. - user_provided: If this entry is defined by user. - If the flag is set, this calibration may appear in the wire format - as an inline calibration, to override the backend instruction set architecture. - """ - pass - - @abstractmethod - def get_signature(self) -> inspect.Signature: - """Return signature object associated with entry definition. - - Returns: - Signature object. - """ - pass - - @abstractmethod - def get_schedule(self, *args, **kwargs) -> Schedule | ScheduleBlock: - """Generate schedule from entry definition. - - If the pulse program is templated with :class:`.Parameter` objects, - you can provide corresponding parameter values for this method - to get a particular pulse program with assigned parameters. - - Args: - args: Command parameters. - kwargs: Command keyword parameters. - - Returns: - Pulse schedule with assigned parameters. - """ - pass - - @property - @abstractmethod - def user_provided(self) -> bool: - """Return if this entry is user defined.""" - pass - - -class ScheduleDef(CalibrationEntry): - """In-memory Qiskit Pulse representation. - - A pulse schedule must provide signature with the .parameters attribute. - This entry can be parameterized by a Qiskit Parameter object. - The .get_schedule method returns a parameter-assigned pulse program. - - .. see_also:: - :class:`.CalibrationEntry` for the purpose of this class. - - """ - - def __init__(self, arguments: Sequence[str] | None = None): - """Define an empty entry. - - Args: - arguments: User provided argument names for this entry, if parameterized. - - Raises: - PulseError: When `arguments` is not a sequence of string. - """ - if arguments and not all(isinstance(arg, str) for arg in arguments): - raise PulseError(f"Arguments must be name of parameters. Not {arguments}.") - if arguments: - arguments = list(arguments) - self._user_arguments = arguments - - self._definition: Callable | Schedule | None = None - self._signature: inspect.Signature | None = None - self._user_provided: bool | None = None - - @property - def user_provided(self) -> bool: - return self._user_provided - - def _parse_argument(self): - """Generate signature from program and user provided argument names.""" - # This doesn't assume multiple parameters with the same name - # Parameters with the same name are treated identically - all_argnames = {x.name for x in self._definition.parameters} - - if self._user_arguments: - if set(self._user_arguments) != all_argnames: - raise PulseError( - "Specified arguments don't match with schedule parameters. " - f"{self._user_arguments} != {self._definition.parameters}." - ) - argnames = list(self._user_arguments) - else: - argnames = sorted(all_argnames) - - params = [] - for argname in argnames: - param = inspect.Parameter( - argname, - kind=inspect.Parameter.POSITIONAL_OR_KEYWORD, - ) - params.append(param) - signature = inspect.Signature( - parameters=params, - return_annotation=type(self._definition), - ) - self._signature = signature - - def define( - self, - definition: Schedule | ScheduleBlock, - user_provided: bool = True, - ): - self._definition = definition - self._parse_argument() - self._user_provided = user_provided - - def get_signature(self) -> inspect.Signature: - return self._signature - - def get_schedule(self, *args, **kwargs) -> Schedule | ScheduleBlock: - if not args and not kwargs: - out = self._definition - else: - try: - to_bind = self.get_signature().bind_partial(*args, **kwargs) - except TypeError as ex: - raise PulseError( - "Assigned parameter doesn't match with schedule parameters." - ) from ex - value_dict = {} - for param in self._definition.parameters: - # Schedule allows partial bind. This results in parameterized Schedule. - try: - value_dict[param] = to_bind.arguments[param.name] - except KeyError: - pass - out = self._definition.assign_parameters(value_dict, inplace=False) - if "publisher" not in out.metadata: - if self.user_provided: - out.metadata["publisher"] = CalibrationPublisher.QISKIT - else: - out.metadata["publisher"] = CalibrationPublisher.BACKEND_PROVIDER - return out - - def __eq__(self, other): - # This delegates equality check to Schedule or ScheduleBlock. - if hasattr(other, "_definition"): - return self._definition == other._definition - return False - - def __str__(self): - out = f"Schedule {self._definition.name}" - params_str = ", ".join(self.get_signature().parameters.keys()) - if params_str: - out += f"({params_str})" - return out - - -class CallableDef(CalibrationEntry): - """Python callback function that generates Qiskit Pulse program. - - A callable is inspected by the python built-in inspection module and - provide the signature. This entry is parameterized by the function signature - and .get_schedule method returns a non-parameterized pulse program - by consuming the provided arguments and keyword arguments. - - .. see_also:: - :class:`.CalibrationEntry` for the purpose of this class. - - """ - - def __init__(self): - """Define an empty entry.""" - self._definition = None - self._signature = None - self._user_provided = None - - @property - def user_provided(self) -> bool: - return self._user_provided - - def define( - self, - definition: Callable, - user_provided: bool = True, - ): - self._definition = definition - self._signature = inspect.signature(definition) - self._user_provided = user_provided - - def get_signature(self) -> inspect.Signature: - return self._signature - - def get_schedule(self, *args, **kwargs) -> Schedule | ScheduleBlock: - try: - # Python function doesn't allow partial bind, but default value can exist. - to_bind = self._signature.bind(*args, **kwargs) - to_bind.apply_defaults() - except TypeError as ex: - raise PulseError("Assigned parameter doesn't match with function signature.") from ex - out = self._definition(**to_bind.arguments) - if "publisher" not in out.metadata: - if self.user_provided: - out.metadata["publisher"] = CalibrationPublisher.QISKIT - else: - out.metadata["publisher"] = CalibrationPublisher.BACKEND_PROVIDER - return out - - def __eq__(self, other): - # We cannot evaluate function equality without parsing python AST. - # This simply compares weather they are the same object. - if hasattr(other, "_definition"): - return self._definition == other._definition - return False - - def __str__(self): - params_str = ", ".join(self.get_signature().parameters.keys()) - return f"Callable {self._definition.__name__}({params_str})" diff --git a/qiskit/pulse/channels.py b/qiskit/pulse/channels.py deleted file mode 100644 index 8b196345c179..000000000000 --- a/qiskit/pulse/channels.py +++ /dev/null @@ -1,227 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# 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. - -""" -.. _pulse-channels: - -======================================= -Channels (:mod:`qiskit.pulse.channels`) -======================================= - -Pulse is meant to be agnostic to the underlying hardware implementation, while still allowing -low-level control. Therefore, our signal channels are *virtual* hardware channels. The backend -which executes our programs is responsible for mapping these virtual channels to the proper -physical channel within the quantum control hardware. - -Channels are characterized by their type and their index. Channels include: - -* transmit channels, which should subclass ``PulseChannel`` -* receive channels, such as :class:`AcquireChannel` -* non-signal "channels" such as :class:`SnapshotChannel`, :class:`MemorySlot` and - :class:`RegisterChannel`. - -Novel channel types can often utilize the :class:`ControlChannel`, but if this is not sufficient, -new channel types can be created. Then, they must be supported in the PulseQobj schema and the -assembler. Channels are characterized by their type and their index. See each channel type below to -learn more. - -.. autosummary:: - :toctree: ../stubs/ - - DriveChannel - MeasureChannel - AcquireChannel - ControlChannel - RegisterSlot - MemorySlot - SnapshotChannel - -All channels are children of the same abstract base class: - -.. autoclass:: Channel -""" -from __future__ import annotations -from abc import ABCMeta -from typing import Any - -import numpy as np - -from qiskit.circuit import Parameter -from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.pulse.exceptions import PulseError -from qiskit.utils.deprecate_pulse import deprecate_pulse_func - - -class Channel(metaclass=ABCMeta): - """Base class of channels. Channels provide a Qiskit-side label for typical quantum control - hardware signal channels. The final label -> physical channel mapping is the responsibility - of the hardware backend. For instance, ``DriveChannel(0)`` holds instructions which the backend - should map to the signal line driving gate operations on the qubit labeled (indexed) 0. - - When serialized channels are identified by their serialized name ````. - The type of the channel is interpreted from the prefix, - and the index often (but not always) maps to the qubit index. - All concrete channel classes must have a ``prefix`` class attribute - (and instances of that class have an index attribute). Base classes which have - ``prefix`` set to ``None`` are prevented from being instantiated. - - To implement a new channel inherit from :class:`Channel` and provide a unique string identifier - for the ``prefix`` class attribute. - """ - - prefix: str | None = None - """A shorthand string prefix for characterizing the channel type.""" - - # pylint: disable=unused-argument - def __new__(cls, *args, **kwargs): - if cls.prefix is None: - raise NotImplementedError( - "Cannot instantiate abstract channel. " - "See Channel documentation for more information." - ) - - return super().__new__(cls) - - @deprecate_pulse_func - def __init__(self, index: int): - """Channel class. - - Args: - index: Index of channel. - """ - self._validate_index(index) - self._index = index - - @property - def index(self) -> int | ParameterExpression: - """Return the index of this channel. The index is a label for a control signal line - typically mapped trivially to a qubit index. For instance, ``DriveChannel(0)`` labels - the signal line driving the qubit labeled with index 0. - """ - return self._index - - def _validate_index(self, index: Any) -> None: - """Raise a PulseError if the channel index is invalid, namely, if it's not a positive - integer. - - Raises: - PulseError: If ``index`` is not a nonnegative integer. - """ - if isinstance(index, ParameterExpression) and index.parameters: - # Parameters are unbound - return - elif isinstance(index, ParameterExpression): - index = float(index) - if index.is_integer(): - index = int(index) - - if not isinstance(index, (int, np.integer)) or index < 0: - raise PulseError("Channel index must be a nonnegative integer") - - @property - def parameters(self) -> set[Parameter]: - """Parameters which determine the channel index.""" - if isinstance(self.index, ParameterExpression): - return self.index.parameters - return set() - - def is_parameterized(self) -> bool: - """Return True iff the channel is parameterized.""" - return isinstance(self.index, ParameterExpression) - - @property - def name(self) -> str: - """Return the shorthand alias for this channel, which is based on its type and index.""" - return f"{self.__class__.prefix}{self._index}" - - def __repr__(self): - return f"{self.__class__.__name__}({self._index})" - - def __eq__(self, other: object) -> bool: - """Return True iff self and other are equal, specifically, iff they have the same type - and the same index. - - Args: - other: The channel to compare to this channel. - - Returns: - True iff equal. - """ - if not isinstance(other, Channel): - return NotImplemented - return type(self) is type(other) and self._index == other._index - - def __hash__(self): - return hash((type(self), self._index)) - - -class PulseChannel(Channel, metaclass=ABCMeta): - """Base class of transmit Channels. Pulses can be played on these channels.""" - - pass - - -class ClassicalIOChannel(Channel, metaclass=ABCMeta): - """Base class of classical IO channels. These cannot have instructions scheduled on them.""" - - pass - - -class DriveChannel(PulseChannel): - """Drive channels transmit signals to qubits which enact gate operations.""" - - prefix = "d" - - -class MeasureChannel(PulseChannel): - """Measure channels transmit measurement stimulus pulses for readout.""" - - prefix = "m" - - -class ControlChannel(PulseChannel): - """Control channels provide supplementary control over the qubit to the drive channel. - These are often associated with multi-qubit gate operations. They may not map trivially - to a particular qubit index. - """ - - prefix = "u" - - -class AcquireChannel(Channel): - """Acquire channels are used to collect data.""" - - prefix = "a" - - -class SnapshotChannel(ClassicalIOChannel): - """Snapshot channels are used to specify instructions for simulators.""" - - prefix = "s" - - def __init__(self): - """Create new snapshot channel.""" - super().__init__(0) - - -class MemorySlot(ClassicalIOChannel): - """Memory slot channels represent classical memory storage.""" - - prefix = "m" - - -class RegisterSlot(ClassicalIOChannel): - """Classical resister slot channels represent classical registers (low-latency classical - memory). - """ - - prefix = "c" diff --git a/qiskit/pulse/configuration.py b/qiskit/pulse/configuration.py deleted file mode 100644 index 1bfd1f13e2ee..000000000000 --- a/qiskit/pulse/configuration.py +++ /dev/null @@ -1,245 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# 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. - -""" -Configurations for pulse experiments. -""" -from __future__ import annotations -import numpy as np - -from .channels import DriveChannel, MeasureChannel -from .exceptions import PulseError - - -def _assert_nested_dict_equal(a: dict, b: dict): - if len(a) != len(b): - return False - for key in a: - if key in b: - if isinstance(a[key], dict): - if not _assert_nested_dict_equal(a[key], b[key]): - return False - elif isinstance(a[key], np.ndarray): - if not np.all(a[key] == b[key]): - return False - else: - if a[key] != b[key]: - return False - else: - return False - return True - - -class Kernel: - """Settings for this Kernel, which is responsible for integrating time series (raw) data - into IQ points. - """ - - def __init__(self, name: str | None = None, **params): - """Create new kernel. - - Args: - name: Name of kernel to be used - params: Any settings for kerneling. - """ - self.name = name - self.params = params - - def __repr__(self): - name_repr = "'" + self.name + "', " - params_repr = ", ".join(f"{str(k)}={str(v)}" for k, v in self.params.items()) - return f"{self.__class__.__name__}({name_repr}{params_repr})" - - def __eq__(self, other): - if isinstance(other, Kernel): - return _assert_nested_dict_equal(self.__dict__, other.__dict__) - return False - - -class Discriminator: - """Setting for this Discriminator, which is responsible for classifying kerneled IQ points - into 0/1 state results. - """ - - def __init__(self, name: str | None = None, **params): - """Create new discriminator. - - Args: - name: Name of discriminator to be used - params: Any settings for discrimination. - """ - self.name = name - self.params = params - - def __repr__(self): - name_repr = "'" + self.name + "', " or "" - params_repr = ", ".join(f"{str(k)}={str(v)}" for k, v in self.params.items()) - return f"{self.__class__.__name__}({name_repr}{params_repr})" - - def __eq__(self, other): - if isinstance(other, Discriminator): - return _assert_nested_dict_equal(self.__dict__, other.__dict__) - return False - - -class LoRange: - """Range of LO frequency.""" - - def __init__(self, lower_bound: float, upper_bound: float): - self._lb = lower_bound - self._ub = upper_bound - - def includes(self, lo_freq: complex) -> bool: - """Whether `lo_freq` is within the `LoRange`. - - Args: - lo_freq: LO frequency to be validated - - Returns: - bool: True if lo_freq is included in this range, otherwise False - """ - if self._lb <= abs(lo_freq) <= self._ub: - return True - return False - - @property - def lower_bound(self) -> float: - """Lower bound of the LO range""" - return self._lb - - @property - def upper_bound(self) -> float: - """Upper bound of the LO range""" - return self._ub - - def __repr__(self): - return f"{self.__class__.__name__}({self._lb:f}, {self._ub:f})" - - def __eq__(self, other): - """Two LO ranges are the same if they are of the same type, and - have the same frequency range - - Args: - other (LoRange): other LoRange - - Returns: - bool: are self and other equal. - """ - if type(self) is type(other) and self._ub == other._ub and self._lb == other._lb: - return True - return False - - -class LoConfig: - """Pulse channel LO frequency container.""" - - def __init__( - self, - channel_los: dict[DriveChannel | MeasureChannel, float] | None = None, - lo_ranges: dict[DriveChannel | MeasureChannel, LoRange | tuple[int, int]] | None = None, - ): - """Lo channel configuration data structure. - - Args: - channel_los: Dictionary of mappings from configurable channel to lo - lo_ranges: Dictionary of mappings to be enforced from configurable channel to `LoRange` - - Raises: - PulseError: If channel is not configurable or set lo is out of range. - - """ - self._q_lo_freq: dict[DriveChannel, float] = {} - self._m_lo_freq: dict[MeasureChannel, float] = {} - self._lo_ranges: dict[DriveChannel | MeasureChannel, LoRange] = {} - - lo_ranges = lo_ranges if lo_ranges else {} - for channel, freq in lo_ranges.items(): - self.add_lo_range(channel, freq) - - channel_los = channel_los if channel_los else {} - for channel, freq in channel_los.items(): - self.add_lo(channel, freq) - - def add_lo(self, channel: DriveChannel | MeasureChannel, freq: float): - """Add a lo mapping for a channel.""" - if isinstance(channel, DriveChannel): - # add qubit_lo_freq - self.check_lo(channel, freq) - self._q_lo_freq[channel] = freq - elif isinstance(channel, MeasureChannel): - # add meas_lo_freq - self.check_lo(channel, freq) - self._m_lo_freq[channel] = freq - else: - raise PulseError(f"Specified channel {channel.name} cannot be configured.") - - def add_lo_range( - self, channel: DriveChannel | MeasureChannel, lo_range: LoRange | tuple[int, int] - ): - """Add lo range to configuration. - - Args: - channel: Channel to add lo range for - lo_range: Lo range to add - - """ - if isinstance(lo_range, (list, tuple)): - lo_range = LoRange(*lo_range) - self._lo_ranges[channel] = lo_range - - def check_lo(self, channel: DriveChannel | MeasureChannel, freq: float) -> bool: - """Check that lo is valid for channel. - - Args: - channel: Channel to validate lo for - freq: lo frequency - Raises: - PulseError: If freq is outside of channels range - Returns: - True if lo is valid for channel - """ - lo_ranges = self._lo_ranges - if channel in lo_ranges: - lo_range = lo_ranges[channel] - if not lo_range.includes(freq): - raise PulseError(f"Specified LO freq {freq:f} is out of range {lo_range}") - return True - - def channel_lo(self, channel: DriveChannel | MeasureChannel) -> float: - """Return channel lo. - - Args: - channel: Channel to get lo for - Raises: - PulseError: If channel is not configured - Returns: - Lo of supplied channel if present - """ - if isinstance(channel, DriveChannel): - if channel in self.qubit_los: - return self.qubit_los[channel] - - if isinstance(channel, MeasureChannel): - if channel in self.meas_los: - return self.meas_los[channel] - - raise PulseError(f"Channel {channel} is not configured") - - @property - def qubit_los(self) -> dict[DriveChannel, float]: - """Returns dictionary mapping qubit channels (DriveChannel) to los.""" - return self._q_lo_freq - - @property - def meas_los(self) -> dict[MeasureChannel, float]: - """Returns dictionary mapping measure channels (MeasureChannel) to los.""" - return self._m_lo_freq diff --git a/qiskit/pulse/exceptions.py b/qiskit/pulse/exceptions.py deleted file mode 100644 index 29d5288bc121..000000000000 --- a/qiskit/pulse/exceptions.py +++ /dev/null @@ -1,45 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# 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. - -"""Exception for errors raised by the pulse module.""" -from qiskit.exceptions import QiskitError -from qiskit.utils.deprecate_pulse import deprecate_pulse_func - - -class PulseError(QiskitError): - """Errors raised by the pulse module.""" - - @deprecate_pulse_func - def __init__(self, *message): - """Set the error message.""" - super().__init__(*message) - self.message = " ".join(message) - - def __str__(self): - """Return the message.""" - return repr(self.message) - - -class BackendNotSet(PulseError): - """Raised if the builder context does not have a backend.""" - - -class NoActiveBuilder(PulseError): - """Raised if no builder context is active.""" - - -class UnassignedDurationError(PulseError): - """Raised if instruction duration is unassigned.""" - - -class UnassignedReferenceError(PulseError): - """Raised if subroutine is unassigned.""" diff --git a/qiskit/pulse/filters.py b/qiskit/pulse/filters.py deleted file mode 100644 index 7d9dc33bdf8d..000000000000 --- a/qiskit/pulse/filters.py +++ /dev/null @@ -1,309 +0,0 @@ -# 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 functions that filter instructions in a pulse program.""" -from __future__ import annotations -import abc -from functools import singledispatch -from collections.abc import Iterable -from typing import Callable, Any, List - -import numpy as np - -from qiskit.pulse import Schedule, ScheduleBlock, Instruction -from qiskit.pulse.channels import Channel -from qiskit.pulse.schedule import Interval -from qiskit.pulse.exceptions import PulseError - - -@singledispatch -def filter_instructions( - sched, - filters: List[Callable[..., bool]], - negate: bool = False, - recurse_subroutines: bool = True, -): - """A catch-TypeError function which will only get called if none of the other decorated - functions, namely handle_schedule() and handle_scheduleblock(), handle the type passed. - """ - raise TypeError( - f"Type '{type(sched)}' is not valid data format as an input to filter_instructions." - ) - - -@filter_instructions.register -def handle_schedule( - sched: Schedule, - filters: List[Callable[..., bool]], - negate: bool = False, - recurse_subroutines: bool = True, -) -> Schedule: - """A filtering function that takes a schedule and returns a schedule consisting of - filtered instructions. - - Args: - sched: A pulse schedule to be filtered. - filters: List of callback functions that take an instruction and return boolean. - negate: Set `True` to accept an instruction if a filter function returns `False`. - Otherwise the instruction is accepted when the filter function returns `False`. - recurse_subroutines: Set `True` to individually filter instructions inside of a subroutine - defined by the :py:class:`~qiskit.pulse.instructions.Call` instruction. - - Returns: - Filtered pulse schedule. - """ - from qiskit.pulse.transforms import flatten, inline_subroutines - - target_sched = flatten(sched) - if recurse_subroutines: - target_sched = inline_subroutines(target_sched) - - time_inst_tuples = np.array(target_sched.instructions) - - valid_insts = np.ones(len(time_inst_tuples), dtype=bool) - for filt in filters: - valid_insts = np.logical_and(valid_insts, np.array(list(map(filt, time_inst_tuples)))) - - if negate and len(filters) > 0: - valid_insts = ~valid_insts - - filter_schedule = Schedule.initialize_from(sched) - for time, inst in time_inst_tuples[valid_insts]: - filter_schedule.insert(time, inst, inplace=True) - - return filter_schedule - - -@filter_instructions.register -def handle_scheduleblock( - sched_blk: ScheduleBlock, - filters: List[Callable[..., bool]], - negate: bool = False, - recurse_subroutines: bool = True, -) -> ScheduleBlock: - """A filtering function that takes a schedule_block and returns a schedule_block consisting of - filtered instructions. - - Args: - sched_blk: A pulse schedule_block to be filtered. - filters: List of callback functions that take an instruction and return boolean. - negate: Set `True` to accept an instruction if a filter function returns `False`. - Otherwise the instruction is accepted when the filter function returns `False`. - recurse_subroutines: Set `True` to individually filter instructions inside of a subroutine - defined by the :py:class:`~qiskit.pulse.instructions.Call` instruction. - - Returns: - Filtered pulse schedule_block. - """ - from qiskit.pulse.transforms import inline_subroutines - - target_sched_blk = sched_blk - if recurse_subroutines: - target_sched_blk = inline_subroutines(target_sched_blk) - - def apply_filters_to_insts_in_scheblk(blk: ScheduleBlock) -> ScheduleBlock: - blk_new = ScheduleBlock.initialize_from(blk) - for element in blk.blocks: - if isinstance(element, ScheduleBlock): - inner_blk = apply_filters_to_insts_in_scheblk(element) - if len(inner_blk) > 0: - blk_new.append(inner_blk) - - elif isinstance(element, Instruction): - valid_inst = all(filt(element) for filt in filters) - if negate: - valid_inst ^= True - if valid_inst: - blk_new.append(element) - - else: - raise PulseError( - f"An unexpected element '{element}' is included in ScheduleBlock.blocks." - ) - return blk_new - - filter_sched_blk = apply_filters_to_insts_in_scheblk(target_sched_blk) - return filter_sched_blk - - -def composite_filter( - channels: Iterable[Channel] | Channel | None = None, - instruction_types: Iterable[abc.ABCMeta] | abc.ABCMeta | None = None, - time_ranges: Iterable[tuple[int, int]] | None = None, - intervals: Iterable[Interval] | None = None, -) -> list[Callable]: - """A helper function to generate a list of filter functions based on - typical elements to be filtered. - - Args: - channels: For example, ``[DriveChannel(0), AcquireChannel(0)]``. - instruction_types (Optional[Iterable[Type[qiskit.pulse.Instruction]]]): For example, - ``[PulseInstruction, AcquireInstruction]``. - time_ranges: For example, ``[(0, 5), (6, 10)]``. - intervals: For example, ``[(0, 5), (6, 10)]``. - - Returns: - List of filtering functions. - """ - filters = [] - - # An empty list is also valid input for filter generators. - # See unittest `test.python.pulse.test_schedule.TestScheduleFilter.test_empty_filters`. - if channels is not None: - filters.append(with_channels(channels)) - if instruction_types is not None: - filters.append(with_instruction_types(instruction_types)) - if time_ranges is not None: - filters.append(with_intervals(time_ranges)) - if intervals is not None: - filters.append(with_intervals(intervals)) - - return filters - - -def with_channels(channels: Iterable[Channel] | Channel) -> Callable: - """Channel filter generator. - - Args: - channels: List of channels to filter. - - Returns: - A callback function to filter channels. - """ - channels = _if_scalar_cast_to_list(channels) - - @singledispatch - def channel_filter(time_inst): - """A catch-TypeError function which will only get called if none of the other decorated - functions, namely handle_numpyndarray() and handle_instruction(), handle the type passed. - """ - raise TypeError( - f"Type '{type(time_inst)}' is not valid data format as an input to channel_filter." - ) - - @channel_filter.register - def handle_numpyndarray(time_inst: np.ndarray) -> bool: - """Filter channel. - - Args: - time_inst (numpy.ndarray([int, Instruction])): Time - - Returns: - If instruction matches with condition. - """ - return any(chan in channels for chan in time_inst[1].channels) - - @channel_filter.register - def handle_instruction(inst: Instruction) -> bool: - """Filter channel. - - Args: - inst: Instruction - - Returns: - If instruction matches with condition. - """ - return any(chan in channels for chan in inst.channels) - - return channel_filter - - -def with_instruction_types(types: Iterable[abc.ABCMeta] | abc.ABCMeta) -> Callable: - """Instruction type filter generator. - - Args: - types: List of instruction types to filter. - - Returns: - A callback function to filter instructions. - """ - types = _if_scalar_cast_to_list(types) - - @singledispatch - def instruction_filter(time_inst) -> bool: - """A catch-TypeError function which will only get called if none of the other decorated - functions, namely handle_numpyndarray() and handle_instruction(), handle the type passed. - """ - raise TypeError( - f"Type '{type(time_inst)}' is not valid data format as an input to instruction_filter." - ) - - @instruction_filter.register - def handle_numpyndarray(time_inst: np.ndarray) -> bool: - """Filter instruction. - - Args: - time_inst (numpy.ndarray([int, Instruction])): Time - - Returns: - If instruction matches with condition. - """ - return isinstance(time_inst[1], tuple(types)) - - @instruction_filter.register - def handle_instruction(inst: Instruction) -> bool: - """Filter instruction. - - Args: - inst: Instruction - - Returns: - If instruction matches with condition. - """ - return isinstance(inst, tuple(types)) - - return instruction_filter - - -def with_intervals(ranges: Iterable[Interval] | Interval) -> Callable: - """Interval filter generator. - - Args: - ranges: List of intervals ``[t0, t1]`` to filter. - - Returns: - A callback function to filter intervals. - """ - ranges = _if_scalar_cast_to_list(ranges) - - def interval_filter(time_inst) -> bool: - """Filter interval. - Args: - time_inst (Tuple[int, Instruction]): Time - - Returns: - If instruction matches with condition. - """ - for t0, t1 in ranges: - inst_start = time_inst[0] - inst_stop = inst_start + time_inst[1].duration - if t0 <= inst_start and inst_stop <= t1: - return True - return False - - return interval_filter - - -def _if_scalar_cast_to_list(to_list: Any) -> list[Any]: - """A helper function to create python list of input arguments. - - Args: - to_list: Arbitrary object can be converted into a python list. - - Returns: - Python list of input object. - """ - try: - iter(to_list) - except TypeError: - to_list = [to_list] - return to_list diff --git a/qiskit/pulse/instruction_schedule_map.py b/qiskit/pulse/instruction_schedule_map.py deleted file mode 100644 index 96e8ad0a48a6..000000000000 --- a/qiskit/pulse/instruction_schedule_map.py +++ /dev/null @@ -1,421 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019. -# -# 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. - -# pylint: disable=unused-import - -""" -A convenient way to track reusable subschedules by name and qubit. - -This can be used for scheduling circuits with custom definitions, for instance:: - - inst_map = InstructionScheduleMap() - inst_map.add('new_inst', 0, qubit_0_new_inst_schedule) - - sched = schedule(quantum_circuit, backend, inst_map) - -An instance of this class is instantiated by Pulse-enabled backends and populated with defaults -(if available):: - - inst_map = backend.defaults().instruction_schedule_map - -""" -from __future__ import annotations -import functools -import warnings -from collections import defaultdict -from collections.abc import Iterable, Callable - -from qiskit import circuit -from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.pulse.calibration_entries import ( - CalibrationEntry, - ScheduleDef, - CallableDef, -) -from qiskit.pulse.exceptions import PulseError -from qiskit.pulse.schedule import Schedule, ScheduleBlock -from qiskit.utils.deprecate_pulse import deprecate_pulse_func - - -class InstructionScheduleMap: - """Mapping from :py:class:`~qiskit.circuit.QuantumCircuit` - :py:class:`qiskit.circuit.Instruction` names and qubits to - :py:class:`~qiskit.pulse.Schedule` s. In particular, the mapping is formatted as type:: - - Dict[str, Dict[Tuple[int], Schedule]] - - where the first key is the name of a circuit instruction (e.g. ``'u1'``, ``'measure'``), the - second key is a tuple of qubit indices, and the final value is a Schedule implementing the - requested instruction. - - These can usually be seen as gate calibrations. - """ - - @deprecate_pulse_func - def __init__(self): - """Initialize a circuit instruction to schedule mapper instance.""" - # The processed and reformatted circuit instruction definitions - - # Do not use lambda function for nested defaultdict, i.e. lambda: defaultdict(CalibrationEntry). - # This crashes qiskit parallel. Note that parallel framework passes args as - # pickled object, however lambda function cannot be pickled. - self._map: dict[str | circuit.instruction.Instruction, dict[tuple, CalibrationEntry]] = ( - defaultdict(functools.partial(defaultdict, CalibrationEntry)) - ) - - # A backwards mapping from qubit to supported instructions - self._qubit_instructions: dict[tuple[int, ...], set] = defaultdict(set) - - def has_custom_gate(self) -> bool: - """Return ``True`` if the map has user provided instruction.""" - for qubit_inst in self._map.values(): - for entry in qubit_inst.values(): - if entry.user_provided: - return True - return False - - @property - def instructions(self) -> list[str]: - """Return all instructions which have definitions. - - By default, these are typically the basis gates along with other instructions such as - measure and reset. - - Returns: - The names of all the circuit instructions which have Schedule definitions in this. - """ - return list(self._map.keys()) - - def qubits_with_instruction( - self, instruction: str | circuit.instruction.Instruction - ) -> list[int | tuple[int, ...]]: - """Return a list of the qubits for which the given instruction is defined. Single qubit - instructions return a flat list, and multiqubit instructions return a list of ordered - tuples. - - Args: - instruction: The name of the circuit instruction. - - Returns: - Qubit indices which have the given instruction defined. This is a list of tuples if the - instruction has an arity greater than 1, or a flat list of ints otherwise. - - Raises: - PulseError: If the instruction is not found. - """ - instruction = _get_instruction_string(instruction) - if instruction not in self._map: - return [] - return [ - qubits[0] if len(qubits) == 1 else qubits - for qubits in sorted(self._map[instruction].keys()) - ] - - def qubit_instructions(self, qubits: int | Iterable[int]) -> list[str]: - """Return a list of the instruction names that are defined by the backend for the given - qubit or qubits. - - Args: - qubits: A qubit index, or a list or tuple of indices. - - Returns: - All the instructions which are defined on the qubits. - - For 1 qubit, all the 1Q instructions defined. For multiple qubits, all the instructions - which apply to that whole set of qubits (e.g. ``qubits=[0, 1]`` may return ``['cx']``). - """ - if _to_tuple(qubits) in self._qubit_instructions: - return list(self._qubit_instructions[_to_tuple(qubits)]) - return [] - - def has( - self, instruction: str | circuit.instruction.Instruction, qubits: int | Iterable[int] - ) -> bool: - """Is the instruction defined for the given qubits? - - Args: - instruction: The instruction for which to look. - qubits: The specific qubits for the instruction. - - Returns: - True iff the instruction is defined. - """ - instruction = _get_instruction_string(instruction) - return instruction in self._map and _to_tuple(qubits) in self._map[instruction] - - def assert_has( - self, instruction: str | circuit.instruction.Instruction, qubits: int | Iterable[int] - ) -> None: - """Error if the given instruction is not defined. - - Args: - instruction: The instruction for which to look. - qubits: The specific qubits for the instruction. - - Raises: - PulseError: If the instruction is not defined on the qubits. - """ - instruction = _get_instruction_string(instruction) - if not self.has(instruction, _to_tuple(qubits)): - # TODO: PulseError is deprecated, this code will be removed in 2.0. - # In the meantime, we catch the deprecation - # warning not to overload users with non-actionable messages - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", - category=DeprecationWarning, - message=".*The entire Qiskit Pulse package*", - module="qiskit", - ) - if instruction in self._map: - raise PulseError( - f"Operation '{instruction}' exists, but is only defined for qubits " - f"{self.qubits_with_instruction(instruction)}." - ) - raise PulseError(f"Operation '{instruction}' is not defined for this system.") - - def get( - self, - instruction: str | circuit.instruction.Instruction, - qubits: int | Iterable[int], - *params: complex | ParameterExpression, - **kwparams: complex | ParameterExpression, - ) -> 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. - - Args: - instruction: Name of the instruction or the instruction itself. - qubits: The qubits for the instruction. - *params: Command parameters for generating the output schedule. - **kwparams: Keyworded command parameters for generating the schedule. - - Returns: - The Schedule defined for the input. - """ - return self._get_calibration_entry(instruction, qubits).get_schedule(*params, **kwparams) - - def _get_calibration_entry( - self, - instruction: str | circuit.instruction.Instruction, - qubits: int | Iterable[int], - ) -> CalibrationEntry: - """Return the :class:`.CalibrationEntry` without generating schedule. - - When calibration entry is un-parsed Pulse Qobj, this returns calibration - without parsing it. :meth:`CalibrationEntry.get_schedule` method - must be manually called with assigned parameters to get corresponding pulse schedule. - - This method is expected be directly used internally by the V2 backend converter - for faster loading of the backend calibrations. - - Args: - instruction: Name of the instruction or the instruction itself. - qubits: The qubits for the instruction. - - Returns: - The calibration entry. - """ - instruction = _get_instruction_string(instruction) - self.assert_has(instruction, qubits) - - return self._map[instruction][_to_tuple(qubits)] - - def add( - self, - instruction: str | circuit.instruction.Instruction, - qubits: int | Iterable[int], - schedule: Schedule | ScheduleBlock | Callable[..., Schedule | ScheduleBlock], - arguments: list[str] | None = None, - ) -> None: - """Add a new known instruction for the given qubits and its mapping to a pulse schedule. - - Args: - instruction: The name of the instruction to add. - qubits: The qubits which the instruction applies to. - schedule: The Schedule that implements the given instruction. - arguments: List of parameter names to create a parameter-bound schedule from the - associated gate instruction. If :py:meth:`get` is called with arguments rather - than keyword arguments, this parameter list is used to map the input arguments to - parameter objects stored in the target schedule. - - Raises: - PulseError: If the qubits are provided as an empty iterable. - """ - instruction = _get_instruction_string(instruction) - - # validation of target qubit - qubits = _to_tuple(qubits) - if not qubits: - raise PulseError(f"Cannot add definition {instruction} with no target qubits.") - - # generate signature - if isinstance(schedule, (Schedule, ScheduleBlock)): - entry: CalibrationEntry = ScheduleDef(arguments) - elif callable(schedule): - if arguments: - warnings.warn( - "Arguments are overruled by the callback function signature. " - "Input `arguments` are ignored.", - UserWarning, - ) - entry = CallableDef() - else: - raise PulseError( - "Supplied schedule must be one of the Schedule, ScheduleBlock or a " - "callable that outputs a schedule." - ) - entry.define(schedule, user_provided=True) - self._add(instruction, qubits, entry) - - def _add( - self, - instruction_name: str, - qubits: tuple[int, ...], - entry: CalibrationEntry, - ): - """A method to resister calibration entry. - - .. note:: - - This is internal fast-path function, and caller must ensure - the entry is properly formatted. This function may be used by other programs - that load backend calibrations to create Qiskit representation of it. - - Args: - instruction_name: Name of instruction. - qubits: List of qubits that this calibration is applied. - entry: Calibration entry to register. - - :meta public: - """ - self._map[instruction_name][qubits] = entry - self._qubit_instructions[qubits].add(instruction_name) - - def remove( - self, instruction: str | circuit.instruction.Instruction, qubits: int | Iterable[int] - ) -> None: - """Remove the given instruction from the listing of instructions defined in self. - - Args: - instruction: The name of the instruction to add. - qubits: The qubits which the instruction applies to. - """ - instruction = _get_instruction_string(instruction) - qubits = _to_tuple(qubits) - self.assert_has(instruction, qubits) - - del self._map[instruction][qubits] - if not self._map[instruction]: - del self._map[instruction] - - self._qubit_instructions[qubits].remove(instruction) - if not self._qubit_instructions[qubits]: - del self._qubit_instructions[qubits] - - def pop( - self, - instruction: str | circuit.instruction.Instruction, - qubits: int | Iterable[int], - *params: complex | ParameterExpression, - **kwparams: complex | ParameterExpression, - ) -> Schedule | ScheduleBlock: - """Remove and return the defined schedule for the given instruction on the given - qubits. - - Args: - instruction: Name of the instruction. - qubits: The qubits for the instruction. - *params: Command parameters for generating the output schedule. - **kwparams: Keyworded command parameters for generating the schedule. - - Returns: - The Schedule defined for the input. - """ - instruction = _get_instruction_string(instruction) - schedule = self.get(instruction, qubits, *params, **kwparams) - self.remove(instruction, qubits) - return schedule - - def get_parameters( - self, instruction: str | circuit.instruction.Instruction, qubits: int | Iterable[int] - ) -> tuple[str, ...]: - """Return the list of parameters taken by the given instruction on the given qubits. - - Args: - instruction: Name of the instruction. - qubits: The qubits for the instruction. - - Returns: - The names of the parameters required by the instruction. - """ - instruction = _get_instruction_string(instruction) - - self.assert_has(instruction, qubits) - with warnings.catch_warnings(): - warnings.simplefilter(action="ignore", category=DeprecationWarning) - # Prevent `get_signature` from emitting pulse package deprecation warnings - signature = self._map[instruction][_to_tuple(qubits)].get_signature() - return tuple(signature.parameters.keys()) - - def __str__(self): - single_q_insts = "1Q instructions:\n" - multi_q_insts = "Multi qubit instructions:\n" - for qubits, insts in self._qubit_instructions.items(): - if len(qubits) == 1: - single_q_insts += f" q{qubits[0]}: {insts}\n" - else: - multi_q_insts += f" {qubits}: {insts}\n" - instructions = single_q_insts + multi_q_insts - return f"<{self.__class__.__name__}({instructions})>" - - def __eq__(self, other): - if not isinstance(other, InstructionScheduleMap): - return False - - for inst in self.instructions: - for qinds in self.qubits_with_instruction(inst): - try: - if self._map[inst][_to_tuple(qinds)] != other._map[inst][_to_tuple(qinds)]: - return False - except KeyError: - return False - return True - - -def _to_tuple(values: int | Iterable[int]) -> tuple[int, ...]: - """Return the input as a tuple. - - Args: - values: An integer, or iterable of integers. - - Returns: - The input values as a sorted tuple. - """ - try: - return tuple(values) - except TypeError: - return (values,) - - -def _get_instruction_string(inst: str | circuit.instruction.Instruction) -> str: - if isinstance(inst, str): - return inst - else: - try: - return inst.name - except AttributeError as ex: - raise PulseError( - 'Input "inst" has no attribute "name". This should be a circuit "Instruction".' - ) from ex diff --git a/qiskit/pulse/instructions/__init__.py b/qiskit/pulse/instructions/__init__.py deleted file mode 100644 index e97ac27fc62f..000000000000 --- a/qiskit/pulse/instructions/__init__.py +++ /dev/null @@ -1,67 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -r""" -.. _pulse-insts: - -=============================================== -Instructions (:mod:`qiskit.pulse.instructions`) -=============================================== - -The ``instructions`` module holds the various :obj:`Instruction`\ s which are supported by -Qiskit Pulse. Instructions have operands, which typically include at least one -:py:class:`~qiskit.pulse.channels.Channel` specifying where the instruction will be applied. - -Every instruction has a duration, whether explicitly included as an operand or implicitly defined. -For instance, a :py:class:`~qiskit.pulse.instructions.ShiftPhase` instruction can be instantiated -with operands *phase* and *channel*, for some float ``phase`` and a -:py:class:`~qiskit.pulse.channels.Channel` ``channel``:: - - ShiftPhase(phase, channel) - -The duration of this instruction is implicitly zero. On the other hand, the -:py:class:`~qiskit.pulse.instructions.Delay` instruction takes an explicit duration:: - - Delay(duration, channel) - -An instruction can be added to a :py:class:`~qiskit.pulse.Schedule`, which is a -sequence of scheduled Pulse ``Instruction`` s over many channels. ``Instruction`` s and -``Schedule`` s implement the same interface. - -.. autosummary:: - :toctree: ../stubs/ - - Acquire - Reference - Delay - Play - RelativeBarrier - SetFrequency - ShiftFrequency - SetPhase - ShiftPhase - Snapshot - TimeBlockade - -These are all instances of the same base class: - -.. autoclass:: Instruction -""" -from .acquire import Acquire -from .delay import Delay -from .directives import Directive, RelativeBarrier, TimeBlockade -from .instruction import Instruction -from .frequency import SetFrequency, ShiftFrequency -from .phase import ShiftPhase, SetPhase -from .play import Play -from .snapshot import Snapshot -from .reference import Reference diff --git a/qiskit/pulse/instructions/acquire.py b/qiskit/pulse/instructions/acquire.py deleted file mode 100644 index 3b5964d4ee3e..000000000000 --- a/qiskit/pulse/instructions/acquire.py +++ /dev/null @@ -1,150 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""The Acquire instruction is used to trigger the qubit measurement unit and provide -some metadata for the acquisition process, for example, where to store classified readout data. -""" -from __future__ import annotations -from qiskit.circuit import ParameterExpression -from qiskit.pulse.channels import MemorySlot, RegisterSlot, AcquireChannel -from qiskit.pulse.configuration import Kernel, Discriminator -from qiskit.pulse.exceptions import PulseError -from qiskit.pulse.instructions.instruction import Instruction -from qiskit.utils.deprecate_pulse import deprecate_pulse_func - - -class Acquire(Instruction): - """The Acquire instruction is used to trigger the ADC associated with a particular qubit; - e.g. instantiated with AcquireChannel(0), the Acquire command will trigger data collection - for the channel associated with qubit 0 readout. This instruction also provides acquisition - metadata: - - * the number of cycles during which to acquire (in terms of dt), - - * the register slot to store classified, intermediary readout results, - - * the memory slot to return classified results, - - * the kernel to integrate raw data for each shot, and - - * the discriminator to classify kerneled IQ points. - """ - - @deprecate_pulse_func - def __init__( - self, - duration: int | ParameterExpression, - channel: AcquireChannel, - mem_slot: MemorySlot | None = None, - reg_slot: RegisterSlot | None = None, - kernel: Kernel | None = None, - discriminator: Discriminator | None = None, - name: str | None = None, - ): - """Create a new Acquire instruction. - - Args: - duration: Length of time to acquire data in terms of dt. - channel: The channel that will acquire data. - mem_slot: The classical memory slot in which to store the classified readout result. - reg_slot: The fast-access register slot in which to store the classified readout - result for fast feedback. - kernel: A ``Kernel`` for integrating raw data. - discriminator: A ``Discriminator`` for discriminating kerneled IQ data into 0/1 - results. - name: Name of the instruction for display purposes. - """ - super().__init__( - operands=(duration, channel, mem_slot, reg_slot, kernel, discriminator), - name=name, - ) - - def _validate(self): - """Called after initialization to validate instruction data. - - Raises: - PulseError: If the input ``channel`` is not type :class:`AcquireChannel`. - PulseError: If the input ``mem_slot`` is not type :class:`MemorySlot`. - PulseError: If the input ``reg_slot`` is not type :class:`RegisterSlot`. - PulseError: When memory slot and register slot are both empty. - """ - if not isinstance(self.channel, AcquireChannel): - raise PulseError(f"Expected an acquire channel, got {self.channel} instead.") - - if self.mem_slot and not isinstance(self.mem_slot, MemorySlot): - raise PulseError(f"Expected a memory slot, got {self.mem_slot} instead.") - - if self.reg_slot and not isinstance(self.reg_slot, RegisterSlot): - raise PulseError(f"Expected a register slot, got {self.reg_slot} instead.") - - if self.mem_slot is None and self.reg_slot is None: - raise PulseError("Neither MemorySlots nor RegisterSlots were supplied.") - - @property - def channel(self) -> AcquireChannel: - """Return the :py:class:`~qiskit.pulse.channels.Channel` that this instruction is - scheduled on. - """ - return self.operands[1] - - @property - def channels(self) -> tuple[AcquireChannel | MemorySlot | RegisterSlot, ...]: - """Returns the channels that this schedule uses.""" - return tuple(self.operands[ind] for ind in (1, 2, 3) if self.operands[ind] is not None) - - @property - def duration(self) -> int | ParameterExpression: - """Duration of this instruction.""" - return self.operands[0] - - @property - def kernel(self) -> Kernel: - """Return kernel settings.""" - return self._operands[4] - - @property - def discriminator(self) -> Discriminator: - """Return discrimination settings.""" - return self._operands[5] - - @property - def acquire(self) -> AcquireChannel: - """Acquire channel to acquire data. The ``AcquireChannel`` index maps trivially to - qubit index. - """ - return self.channel - - @property - def mem_slot(self) -> MemorySlot: - """The classical memory slot which will store the classified readout result.""" - return self.operands[2] - - @property - def reg_slot(self) -> RegisterSlot: - """The fast-access register slot which will store the classified readout result for - fast-feedback computation. - """ - return self.operands[3] - - def is_parameterized(self) -> bool: - """Return True iff the instruction is parameterized.""" - return isinstance(self.duration, ParameterExpression) or super().is_parameterized() - - def __repr__(self) -> str: - mem_slot_repr = str(self.mem_slot) if self.mem_slot else "" - reg_slot_repr = str(self.reg_slot) if self.reg_slot else "" - kernel_repr = str(self.kernel) if self.kernel else "" - discriminator_repr = str(self.discriminator) if self.discriminator else "" - return ( - f"{self.__class__.__name__}({self.duration}, {str(self.channel)}, " - f"{mem_slot_repr}, {reg_slot_repr}, {kernel_repr}, {discriminator_repr})" - ) diff --git a/qiskit/pulse/instructions/delay.py b/qiskit/pulse/instructions/delay.py deleted file mode 100644 index 6dd028c94fcd..000000000000 --- a/qiskit/pulse/instructions/delay.py +++ /dev/null @@ -1,71 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""An instruction for blocking time on a channel; useful for scheduling alignment.""" -from __future__ import annotations - -from qiskit.circuit import ParameterExpression -from qiskit.pulse.channels import Channel -from qiskit.pulse.instructions.instruction import Instruction -from qiskit.utils.deprecate_pulse import deprecate_pulse_func - - -class Delay(Instruction): - """A blocking instruction with no other effect. The delay is used for aligning and scheduling - other instructions. - - Example: - - To schedule an instruction at time = 10, on a channel assigned to the variable ``channel``, - the following could be used:: - - sched = Schedule(name="Delay instruction example") - sched += Delay(10, channel) - sched += Gaussian(duration, amp, sigma, channel) - - The ``channel`` will output no signal from time=0 up until time=10. - """ - - @deprecate_pulse_func - def __init__( - self, - duration: int | ParameterExpression, - channel: Channel, - name: str | None = None, - ): - """Create a new delay instruction. - - No other instruction may be scheduled within a ``Delay``. - - Args: - duration: Length of time of the delay in terms of dt. - channel: The channel that will have the delay. - name: Name of the delay for display purposes. - """ - super().__init__(operands=(duration, channel), name=name) - - @property - def channel(self) -> Channel: - """Return the :py:class:`~qiskit.pulse.channels.Channel` that this instruction is - scheduled on. - """ - return self.operands[1] - - @property - def channels(self) -> tuple[Channel]: - """Returns the channels that this schedule uses.""" - return (self.channel,) - - @property - def duration(self) -> int | ParameterExpression: - """Duration of this instruction.""" - return self.operands[0] diff --git a/qiskit/pulse/instructions/directives.py b/qiskit/pulse/instructions/directives.py deleted file mode 100644 index 1a9731798fea..000000000000 --- a/qiskit/pulse/instructions/directives.py +++ /dev/null @@ -1,162 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Directives are hints to the pulse compiler for how to process its input programs.""" -from __future__ import annotations - -from abc import ABC - -from qiskit.pulse import channels as chans -from qiskit.pulse.instructions import instruction -from qiskit.pulse.exceptions import PulseError -from qiskit.utils.deprecate_pulse import deprecate_pulse_func - - -class Directive(instruction.Instruction, ABC): - """A compiler directive. - - This is a hint to the pulse compiler and is not loaded into hardware. - """ - - @property - def duration(self) -> int: - """Duration of this instruction.""" - return 0 - - -class RelativeBarrier(Directive): - """Pulse ``RelativeBarrier`` directive.""" - - @deprecate_pulse_func - def __init__(self, *channels: chans.Channel, name: str | None = None): - """Create a relative barrier directive. - - The barrier directive blocks instructions within the same schedule - as the barrier on channels contained within this barrier from moving - through the barrier in time. - - Args: - channels: The channel that the barrier applies to. - name: Name of the directive for display purposes. - """ - super().__init__(operands=tuple(channels), name=name) - - @property - def channels(self) -> tuple[chans.Channel, ...]: - """Returns the channels that this schedule uses.""" - return self.operands - - def __eq__(self, other: object) -> bool: - """Verify two barriers are equivalent.""" - return isinstance(other, type(self)) and set(self.channels) == set(other.channels) - - -class TimeBlockade(Directive): - """Pulse ``TimeBlockade`` directive. - - This instruction is intended to be used internally within the pulse builder, - to convert :class:`.Schedule` into :class:`.ScheduleBlock`. - Because :class:`.ScheduleBlock` cannot take an absolute instruction time interval, - this directive helps the block representation to find the starting time of an instruction. - - Example: - - This schedule plays constant pulse at t0 = 120. - - .. plot:: - :include-source: - :nofigs: - - from qiskit.pulse import Schedule, Play, Constant, DriveChannel - - schedule = Schedule() - schedule.insert(120, Play(Constant(10, 0.1), DriveChannel(0))) - - This schedule block is expected to be identical to above at a time of execution. - - .. plot:: - :include-source: - :nofigs: - :context: reset - - from qiskit.pulse import ScheduleBlock, Play, Constant, DriveChannel - from qiskit.pulse.instructions import TimeBlockade - - block = ScheduleBlock() - block.append(TimeBlockade(120, DriveChannel(0))) - block.append(Play(Constant(10, 0.1), DriveChannel(0))) - - Such conversion may be done by - - .. plot:: - :include-source: - :nofigs: - :context: - - from qiskit.pulse.transforms import block_to_schedule, remove_directives - - schedule = remove_directives(block_to_schedule(block)) - - - .. note:: - - The TimeBlockade instruction behaves almost identically - to :class:`~qiskit.pulse.instructions.Delay` instruction. - However, the TimeBlockade is just a compiler directive and must be removed before execution. - This may be done by :func:`~qiskit.pulse.transforms.remove_directives` transform. - Once these directives are removed, occupied timeslots are released and - user can insert another instruction without timing overlap. - """ - - @deprecate_pulse_func - def __init__( - self, - duration: int, - channel: chans.Channel, - name: str | None = None, - ): - """Create a time blockade directive. - - Args: - duration: Length of time of the occupation in terms of dt. - channel: The channel that will be the occupied. - name: Name of the time blockade for display purposes. - """ - super().__init__(operands=(duration, channel), name=name) - - def _validate(self): - """Called after initialization to validate instruction data. - - Raises: - PulseError: If the input ``duration`` is not integer value. - """ - if not isinstance(self.duration, int): - raise PulseError( - "TimeBlockade duration cannot be parameterized. Specify an integer duration value." - ) - - @property - def channel(self) -> chans.Channel: - """Return the :py:class:`~qiskit.pulse.channels.Channel` that this instruction is - scheduled on. - """ - return self.operands[1] - - @property - def channels(self) -> tuple[chans.Channel]: - """Returns the channels that this schedule uses.""" - return (self.channel,) - - @property - def duration(self) -> int: - """Duration of this instruction.""" - return self.operands[0] diff --git a/qiskit/pulse/instructions/frequency.py b/qiskit/pulse/instructions/frequency.py deleted file mode 100644 index 545d26c92639..000000000000 --- a/qiskit/pulse/instructions/frequency.py +++ /dev/null @@ -1,135 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Frequency instructions module. These instructions allow the user to manipulate -the frequency of a channel. -""" -from typing import Optional, Union, Tuple - -from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.pulse.channels import PulseChannel -from qiskit.pulse.instructions.instruction import Instruction -from qiskit.pulse.exceptions import PulseError -from qiskit.utils.deprecate_pulse import deprecate_pulse_func - - -class SetFrequency(Instruction): - r"""Set the channel frequency. This instruction operates on ``PulseChannel`` s. - A ``PulseChannel`` creates pulses of the form - - .. math:: - Re[\exp(i 2\pi f jdt + \phi) d_j]. - - Here, :math:`f` is the frequency of the channel. The instruction ``SetFrequency`` allows - the user to set the value of :math:`f`. All pulses that are played on a channel - after SetFrequency has been called will have the corresponding frequency. - - The duration of SetFrequency is 0. - """ - - @deprecate_pulse_func - def __init__( - self, - frequency: Union[float, ParameterExpression], - channel: PulseChannel, - name: Optional[str] = None, - ): - """Creates a new set channel frequency instruction. - - Args: - frequency: New frequency of the channel in Hz. - channel: The channel this instruction operates on. - name: Name of this set channel frequency instruction. - """ - super().__init__(operands=(frequency, channel), name=name) - - def _validate(self): - """Called after initialization to validate instruction data. - - Raises: - PulseError: If the input ``channel`` is not type :class:`PulseChannel`. - """ - if not isinstance(self.channel, PulseChannel): - raise PulseError(f"Expected a pulse channel, got {self.channel} instead.") - - @property - def frequency(self) -> Union[float, ParameterExpression]: - """New frequency.""" - return self.operands[0] - - @property - def channel(self) -> PulseChannel: - """Return the :py:class:`~qiskit.pulse.channels.Channel` that this instruction is - scheduled on. - """ - return self.operands[1] - - @property - def channels(self) -> Tuple[PulseChannel]: - """Returns the channels that this schedule uses.""" - return (self.channel,) - - @property - def duration(self) -> int: - """Duration of this instruction.""" - return 0 - - -class ShiftFrequency(Instruction): - """Shift the channel frequency away from the current frequency.""" - - @deprecate_pulse_func - def __init__( - self, - frequency: Union[float, ParameterExpression], - channel: PulseChannel, - name: Optional[str] = None, - ): - """Creates a new shift frequency instruction. - - Args: - frequency: Frequency shift of the channel in Hz. - channel: The channel this instruction operates on. - name: Name of this set channel frequency instruction. - """ - super().__init__(operands=(frequency, channel), name=name) - - def _validate(self): - """Called after initialization to validate instruction data. - - Raises: - PulseError: If the input ``channel`` is not type :class:`PulseChannel`. - """ - if not isinstance(self.channel, PulseChannel): - raise PulseError(f"Expected a pulse channel, got {self.channel} instead.") - - @property - def frequency(self) -> Union[float, ParameterExpression]: - """Frequency shift from the set frequency.""" - return self.operands[0] - - @property - def channel(self) -> PulseChannel: - """Return the :py:class:`~qiskit.pulse.channels.Channel` that this instruction is - scheduled on. - """ - return self.operands[1] - - @property - def channels(self) -> Tuple[PulseChannel]: - """Returns the channels that this schedule uses.""" - return (self.channel,) - - @property - def duration(self) -> int: - """Duration of this instruction.""" - return 0 diff --git a/qiskit/pulse/instructions/instruction.py b/qiskit/pulse/instructions/instruction.py deleted file mode 100644 index fda854d2b7eb..000000000000 --- a/qiskit/pulse/instructions/instruction.py +++ /dev/null @@ -1,270 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019. -# -# 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. - -""" -``Instruction`` s are single operations within a :py:class:`~qiskit.pulse.Schedule`, and can be -used the same way as :py:class:`~qiskit.pulse.Schedule` s. - -For example:: - - duration = 10 - channel = DriveChannel(0) - sched = Schedule() - sched += Delay(duration, channel) # Delay is a specific subclass of Instruction -""" -from __future__ import annotations -from abc import ABC, abstractmethod -from collections.abc import Iterable - -from qiskit.circuit import Parameter, ParameterExpression -from qiskit.pulse.channels import Channel -from qiskit.pulse.exceptions import PulseError -from qiskit.utils.deprecate_pulse import deprecate_pulse_func - - -# pylint: disable=bad-docstring-quotes - - -class Instruction(ABC): - """The smallest schedulable unit: a single instruction. It has a fixed duration and specified - channels. - """ - - @deprecate_pulse_func - def __init__( - self, - operands: tuple, - name: str | None = None, - ): - """Instruction initializer. - - Args: - operands: The argument list. - name: Optional display name for this instruction. - """ - self._operands = operands - self._name = name - self._validate() - - def _validate(self): - """Called after initialization to validate instruction data. - - Raises: - PulseError: If the input ``channels`` are not all of type :class:`Channel`. - """ - for channel in self.channels: - if not isinstance(channel, Channel): - raise PulseError(f"Expected a channel, got {channel} instead.") - - @property - def name(self) -> str: - """Name of this instruction.""" - return self._name - - @property - def id(self) -> int: # pylint: disable=invalid-name - """Unique identifier for this instruction.""" - return id(self) - - @property - def operands(self) -> tuple: - """Return instruction operands.""" - return self._operands - - @property - @abstractmethod - def channels(self) -> tuple[Channel, ...]: - """Returns the channels that this schedule uses.""" - raise NotImplementedError - - @property - def start_time(self) -> int: - """Relative begin time of this instruction.""" - return 0 - - @property - def stop_time(self) -> int: - """Relative end time of this instruction.""" - return self.duration - - @property - def duration(self) -> int | ParameterExpression: - """Duration of this instruction.""" - raise NotImplementedError - - @property - def _children(self) -> tuple["Instruction", ...]: - """Instruction has no child nodes.""" - return () - - @property - def instructions(self) -> tuple[tuple[int, "Instruction"], ...]: - """Iterable for getting instructions from Schedule tree.""" - return tuple(self._instructions()) - - def ch_duration(self, *channels: Channel) -> int: - """Return duration of the supplied channels in this Instruction. - - Args: - *channels: Supplied channels - """ - return self.ch_stop_time(*channels) - - def ch_start_time(self, *channels: Channel) -> int: - # pylint: disable=unused-argument - """Return minimum start time for supplied channels. - - Args: - *channels: Supplied channels - """ - return 0 - - def ch_stop_time(self, *channels: Channel) -> int: - """Return maximum start time for supplied channels. - - Args: - *channels: Supplied channels - """ - if any(chan in self.channels for chan in channels): - return self.duration - return 0 - - def _instructions(self, time: int = 0) -> Iterable[tuple[int, "Instruction"]]: - """Iterable for flattening Schedule tree. - - Args: - time: Shifted time of this node due to parent - - Yields: - Tuple[int, Union['Schedule, 'Instruction']]: Tuple of the form - (start_time, instruction). - """ - yield (time, self) - - def shift(self, time: int, name: str | None = None): - """Return a new schedule shifted forward by `time`. - - Args: - time: Time to shift by - name: Name of the new schedule. Defaults to name of self - - Returns: - Schedule: The shifted schedule. - """ - from qiskit.pulse.schedule import Schedule - - if name is None: - name = self.name - return Schedule((time, self), name=name) - - def insert(self, start_time: int, schedule, name: str | None = None): - """Return a new :class:`~qiskit.pulse.Schedule` with ``schedule`` inserted within - ``self`` at ``start_time``. - - Args: - start_time: Time to insert the schedule schedule - schedule (Union['Schedule', 'Instruction']): Schedule or instruction to insert - name: Name of the new schedule. Defaults to name of self - - Returns: - Schedule: A new schedule with ``schedule`` inserted with this instruction at t=0. - """ - from qiskit.pulse.schedule import Schedule - - if name is None: - name = self.name - return Schedule(self, (start_time, schedule), name=name) - - def append(self, schedule, name: str | None = None): - """Return a new :class:`~qiskit.pulse.Schedule` with ``schedule`` inserted at the - maximum time over all channels shared between ``self`` and ``schedule``. - - Args: - schedule (Union['Schedule', 'Instruction']): Schedule or instruction to be appended - name: Name of the new schedule. Defaults to name of self - - Returns: - Schedule: A new schedule with ``schedule`` a this instruction at t=0. - """ - common_channels = set(self.channels) & set(schedule.channels) - time = self.ch_stop_time(*common_channels) - return self.insert(time, schedule, name=name) - - @property - def parameters(self) -> set: - """Parameters which determine the instruction behavior.""" - - def _get_parameters_recursive(obj): - params = set() - if hasattr(obj, "parameters"): - for param in obj.parameters: - if isinstance(param, Parameter): - params.add(param) - else: - params |= _get_parameters_recursive(param) - return params - - parameters = set() - for op in self.operands: - parameters |= _get_parameters_recursive(op) - return parameters - - def is_parameterized(self) -> bool: - """Return True iff the instruction is parameterized.""" - return any(self.parameters) - - def __eq__(self, other: object) -> bool: - """Check if this Instruction is equal to the `other` instruction. - - Equality is determined by the instruction sharing the same operands and channels. - """ - if not isinstance(other, Instruction): - return NotImplemented - return isinstance(other, type(self)) and self.operands == other.operands - - def __hash__(self) -> int: - return hash((type(self), self.operands, self.name)) - - def __add__(self, other): - """Return a new schedule with `other` inserted within `self` at `start_time`. - - Args: - other (Union['Schedule', 'Instruction']): Schedule or instruction to be appended - - Returns: - Schedule: A new schedule with ``schedule`` appended after this instruction at t=0. - """ - return self.append(other) - - def __or__(self, other): - """Return a new schedule which is the union of `self` and `other`. - - Args: - other (Union['Schedule', 'Instruction']): Schedule or instruction to union with - - Returns: - Schedule: A new schedule with ``schedule`` inserted with this instruction at t=0 - """ - return self.insert(0, other) - - def __lshift__(self, time: int): - """Return a new schedule which is shifted forward by `time`. - - Returns: - Schedule: The shifted schedule - """ - return self.shift(time) - - def __repr__(self) -> str: - operands = ", ".join(str(op) for op in self.operands) - name_repr = f", name='{self.name}'" if self.name else "" - return f"{self.__class__.__name__}({operands}{name_repr})" diff --git a/qiskit/pulse/instructions/phase.py b/qiskit/pulse/instructions/phase.py deleted file mode 100644 index 1d08918cb7e9..000000000000 --- a/qiskit/pulse/instructions/phase.py +++ /dev/null @@ -1,152 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""The phase instructions update the modulation phase of pulses played on a channel. -This includes ``SetPhase`` instructions which lock the modulation to a particular phase -at that moment, and ``ShiftPhase`` instructions which increase the existing phase by a -relative amount. -""" -from typing import Optional, Union, Tuple - -from qiskit.circuit import ParameterExpression -from qiskit.pulse.channels import PulseChannel -from qiskit.pulse.instructions.instruction import Instruction -from qiskit.pulse.exceptions import PulseError -from qiskit.utils.deprecate_pulse import deprecate_pulse_func - - -class ShiftPhase(Instruction): - r"""The shift phase instruction updates the modulation phase of proceeding pulses played on the - same :py:class:`~qiskit.pulse.channels.Channel`. It is a relative increase in phase determined - by the ``phase`` operand. - - In particular, a PulseChannel creates pulses of the form - - .. math:: - Re[\exp(i 2\pi f jdt + \phi) d_j]. - - The ``ShiftPhase`` instruction causes :math:`\phi` to be increased by the instruction's - ``phase`` operand. This will affect all pulses following on the same channel. - - The qubit phase is tracked in software, enabling instantaneous, nearly error-free Z-rotations - by using a ShiftPhase to update the frame tracking the qubit state. - """ - - @deprecate_pulse_func - def __init__( - self, - phase: Union[complex, ParameterExpression], - channel: PulseChannel, - name: Optional[str] = None, - ): - """Instantiate a shift phase instruction, increasing the output signal phase on ``channel`` - by ``phase`` [radians]. - - Args: - phase: The rotation angle in radians. - channel: The channel this instruction operates on. - name: Display name for this instruction. - """ - super().__init__(operands=(phase, channel), name=name) - - def _validate(self): - """Called after initialization to validate instruction data. - - Raises: - PulseError: If the input ``channel`` is not type :class:`PulseChannel`. - """ - if not isinstance(self.channel, PulseChannel): - raise PulseError(f"Expected a pulse channel, got {self.channel} instead.") - - @property - def phase(self) -> Union[complex, ParameterExpression]: - """Return the rotation angle enacted by this instruction in radians.""" - return self.operands[0] - - @property - def channel(self) -> PulseChannel: - """Return the :py:class:`~qiskit.pulse.channels.Channel` that this instruction is - scheduled on. - """ - return self.operands[1] - - @property - def channels(self) -> Tuple[PulseChannel]: - """Returns the channels that this schedule uses.""" - return (self.channel,) - - @property - def duration(self) -> int: - """Duration of this instruction.""" - return 0 - - -class SetPhase(Instruction): - r"""The set phase instruction sets the phase of the proceeding pulses on that channel - to ``phase`` radians. - - In particular, a PulseChannel creates pulses of the form - - .. math:: - - Re[\exp(i 2\pi f jdt + \phi) d_j] - - The ``SetPhase`` instruction sets :math:`\phi` to the instruction's ``phase`` operand. - """ - - @deprecate_pulse_func - def __init__( - self, - phase: Union[complex, ParameterExpression], - channel: PulseChannel, - name: Optional[str] = None, - ): - """Instantiate a set phase instruction, setting the output signal phase on ``channel`` - to ``phase`` [radians]. - - Args: - phase: The rotation angle in radians. - channel: The channel this instruction operates on. - name: Display name for this instruction. - """ - super().__init__(operands=(phase, channel), name=name) - - def _validate(self): - """Called after initialization to validate instruction data. - - Raises: - PulseError: If the input ``channel`` is not type :class:`PulseChannel`. - """ - if not isinstance(self.channel, PulseChannel): - raise PulseError(f"Expected a pulse channel, got {self.channel} instead.") - - @property - def phase(self) -> Union[complex, ParameterExpression]: - """Return the rotation angle enacted by this instruction in radians.""" - return self.operands[0] - - @property - def channel(self) -> PulseChannel: - """Return the :py:class:`~qiskit.pulse.channels.Channel` that this instruction is - scheduled on. - """ - return self.operands[1] - - @property - def channels(self) -> Tuple[PulseChannel]: - """Returns the channels that this schedule uses.""" - return (self.channel,) - - @property - def duration(self) -> int: - """Duration of this instruction.""" - return 0 diff --git a/qiskit/pulse/instructions/play.py b/qiskit/pulse/instructions/play.py deleted file mode 100644 index 8c86555bbc8c..000000000000 --- a/qiskit/pulse/instructions/play.py +++ /dev/null @@ -1,99 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""An instruction to transmit a given pulse on a ``PulseChannel`` (i.e., those which support -transmitted pulses, such as ``DriveChannel``). -""" -from __future__ import annotations - -from qiskit.circuit import Parameter -from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.pulse.channels import PulseChannel -from qiskit.pulse.exceptions import PulseError -from qiskit.pulse.instructions.instruction import Instruction -from qiskit.pulse.library.pulse import Pulse -from qiskit.utils.deprecate_pulse import deprecate_pulse_func - - -class Play(Instruction): - """This instruction is responsible for applying a pulse on a channel. - - The pulse specifies the exact time dynamics of the output signal envelope for a limited - time. The output is modulated by a phase and frequency which are controlled by separate - instructions. The pulse duration must be fixed, and is implicitly given in terms of the - cycle time, dt, of the backend. - """ - - @deprecate_pulse_func - def __init__(self, pulse: Pulse, channel: PulseChannel, name: str | None = None): - """Create a new pulse instruction. - - Args: - pulse: A pulse waveform description, such as - :py:class:`~qiskit.pulse.library.Waveform`. - channel: The channel to which the pulse is applied. - name: Name of the instruction for display purposes. Defaults to ``pulse.name``. - """ - if name is None: - name = pulse.name - super().__init__(operands=(pulse, channel), name=name) - - def _validate(self): - """Called after initialization to validate instruction data. - - Raises: - PulseError: If pulse is not a Pulse type. - PulseError: If the input ``channel`` is not type :class:`PulseChannel`. - """ - if not isinstance(self.pulse, Pulse): - raise PulseError("The `pulse` argument to `Play` must be of type `library.Pulse`.") - - if not isinstance(self.channel, PulseChannel): - raise PulseError(f"Expected a pulse channel, got {self.channel} instead.") - - @property - def pulse(self) -> Pulse: - """A description of the samples that will be played.""" - return self.operands[0] - - @property - def channel(self) -> PulseChannel: - """Return the :py:class:`~qiskit.pulse.channels.Channel` that this instruction is - scheduled on. - """ - return self.operands[1] - - @property - def channels(self) -> tuple[PulseChannel]: - """Returns the channels that this schedule uses.""" - return (self.channel,) - - @property - def duration(self) -> int | ParameterExpression: - """Duration of this instruction.""" - return self.pulse.duration - - @property - def parameters(self) -> set[Parameter]: - """Parameters which determine the instruction behavior.""" - parameters: set[Parameter] = set() - - # Note that Pulse.parameters returns dict rather than set for convention. - # We need special handling for Play instruction. - for pulse_param_expr in self.pulse.parameters.values(): - if isinstance(pulse_param_expr, ParameterExpression): - parameters = parameters | pulse_param_expr.parameters - - if self.channel.is_parameterized(): - parameters = parameters | self.channel.parameters - - return parameters diff --git a/qiskit/pulse/instructions/reference.py b/qiskit/pulse/instructions/reference.py deleted file mode 100644 index 1f17327877a7..000000000000 --- a/qiskit/pulse/instructions/reference.py +++ /dev/null @@ -1,100 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Reference instruction that is a placeholder for subroutine.""" -from __future__ import annotations - -from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.pulse.channels import Channel -from qiskit.pulse.exceptions import PulseError, UnassignedReferenceError -from qiskit.pulse.instructions import instruction -from qiskit.utils.deprecate_pulse import deprecate_pulse_func - - -class Reference(instruction.Instruction): - """Pulse compiler directive that refers to a subroutine. - - If a pulse program uses the same subset of instructions multiple times, then - using the :class:`~.Reference` class may significantly reduce the memory footprint of - the program. This instruction only stores the set of strings to identify the subroutine. - - The actual pulse program can be stored in the :attr:`ScheduleBlock.references` of the - :class:`.ScheduleBlock` that this reference instruction belongs to. - - You can later assign schedules with the :meth:`ScheduleBlock.assign_references` method. - This allows you to build the main program without knowing the actual subroutine, - that is supplied at a later time. - """ - - # Delimiter for representing nested scope. - scope_delimiter = "::" - - # Delimiter for tuple keys. - key_delimiter = "," - - @deprecate_pulse_func - def __init__(self, name: str, *extra_keys: str): - """Create new reference. - - Args: - name: Name of subroutine. - extra_keys: Optional. A set of string keys that may be necessary to - refer to a particular subroutine. For example, when we use - "sx" as a name to refer to the subroutine of an sx pulse, - this name might be used among schedules for different qubits. - In this example, you may specify "q0" in the extra keys - to distinguish the sx schedule for qubit 0 from others. - The user can use an arbitrary number of extra string keys to - uniquely determine the subroutine. - """ - # Run validation - ref_keys = (name,) + tuple(extra_keys) - super().__init__(operands=ref_keys, name=name) - - def _validate(self): - """Called after initialization to validate instruction data. - - Raises: - PulseError: When a key is not a string. - PulseError: When a key in ``ref_keys`` contains the scope delimiter. - """ - for key in self.ref_keys: - if not isinstance(key, str): - raise PulseError(f"Keys must be strings. '{repr(key)}' is not a valid object.") - if self.scope_delimiter in key or self.key_delimiter in key: - raise PulseError( - f"'{self.scope_delimiter}' and '{self.key_delimiter}' are reserved. " - f"'{key}' is not a valid key string." - ) - - @property - def ref_keys(self) -> tuple[str, ...]: - """Returns unique key of the subroutine.""" - return self.operands - - @property - def duration(self) -> int | ParameterExpression: - """Duration of this instruction.""" - raise UnassignedReferenceError(f"Subroutine is not assigned to {self.ref_keys}.") - - @property - def channels(self) -> tuple[Channel, ...]: - """Returns the channels that this schedule uses.""" - raise UnassignedReferenceError(f"Subroutine is not assigned to {self.ref_keys}.") - - @property - def parameters(self) -> set: - """Parameters which determine the instruction behavior.""" - return set() - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.key_delimiter.join(self.ref_keys)})" diff --git a/qiskit/pulse/instructions/snapshot.py b/qiskit/pulse/instructions/snapshot.py deleted file mode 100644 index 1692d13abc8f..000000000000 --- a/qiskit/pulse/instructions/snapshot.py +++ /dev/null @@ -1,82 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019. -# -# 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 simulator instruction to capture output within a simulation. The types of snapshot -instructions available are determined by the simulator being used. -""" -from typing import Optional, Tuple - -from qiskit.pulse.channels import SnapshotChannel -from qiskit.pulse.exceptions import PulseError -from qiskit.pulse.instructions.instruction import Instruction -from qiskit.utils.deprecate_pulse import deprecate_pulse_func - - -class Snapshot(Instruction): - """An instruction targeted for simulators, to capture a moment in the simulation.""" - - @deprecate_pulse_func - def __init__(self, label: str, snapshot_type: str = "statevector", name: Optional[str] = None): - """Create new snapshot. - - Args: - label: Snapshot label which is used to identify the snapshot in the output. - snapshot_type: Type of snapshot, e.g., “state” (take a snapshot of the quantum state). - The types of snapshots offered are defined by the simulator used. - name: Snapshot name which defaults to ``label``. This parameter is only for display - purposes and is not taken into account during comparison. - """ - self._channel = SnapshotChannel() - - if name is None: - name = label - super().__init__(operands=(label, snapshot_type), name=name) - - def _validate(self): - """Called after initialization to validate instruction data. - - Raises: - PulseError: If snapshot label is invalid. - """ - if not isinstance(self.label, str): - raise PulseError("Snapshot label must be a string.") - - @property - def label(self) -> str: - """Label of snapshot.""" - return self.operands[0] - - @property - def type(self) -> str: - """Type of snapshot.""" - return self.operands[1] - - @property - def channel(self) -> SnapshotChannel: - """Return the :py:class:`~qiskit.pulse.channels.Channel` that this instruction is - scheduled on; trivially, a ``SnapshotChannel``. - """ - return self._channel - - @property - def channels(self) -> Tuple[SnapshotChannel]: - """Returns the channels that this schedule uses.""" - return (self.channel,) - - @property - def duration(self) -> int: - """Duration of this instruction.""" - return 0 - - def is_parameterized(self) -> bool: - """Return True iff the instruction is parameterized.""" - return False diff --git a/qiskit/pulse/library/__init__.py b/qiskit/pulse/library/__init__.py deleted file mode 100644 index 99d382e63f91..000000000000 --- a/qiskit/pulse/library/__init__.py +++ /dev/null @@ -1,97 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# 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. - -""" -=========================================== -Pulse Library (:mod:`qiskit.pulse.library`) -=========================================== - -This library provides Pulse users with convenient methods to build Pulse waveforms. - -A pulse programmer can choose from one of several :ref:`pulse_models` such as -:class:`~Waveform` and :class:`~SymbolicPulse` to create a pulse program. -The :class:`~Waveform` model directly stores the waveform data points in each class instance. -This model provides the most flexibility to express arbitrary waveforms and allows -a rapid prototyping of new control techniques. However, this model is typically memory -inefficient and might be hard to scale to large-size quantum processors. -A user can directly instantiate the :class:`~Waveform` class with ``samples`` argument -which is usually a complex numpy array or any kind of array-like data. - -In contrast, the :class:`~SymbolicPulse` model only stores the function and its parameters -that generate the waveform in a class instance. -It thus provides greater memory efficiency at the price of less flexibility in the waveform. -This model also defines a small set of pulse subclasses in :ref:`symbolic_pulses` -which are commonly used in superconducting quantum processors. -An instance of these subclasses can be serialized in the :ref:`qpy_format` -while keeping the memory-efficient parametric representation of waveforms. -Note that :class:`~Waveform` object can be generated from an instance of -a :class:`~SymbolicPulse` which will set values for the parameters and -sample the parametric expression to create the :class:`~Waveform`. - - -.. _pulse_models: - -Pulse Models -============ - -.. autosummary:: - :toctree: ../stubs/ - - Waveform - SymbolicPulse - - -.. _symbolic_pulses: - -Parametric Pulse Representation -=============================== - -.. autosummary:: - :toctree: ../stubs/ - - Constant - Drag - Gaussian - GaussianSquare - GaussianSquareDrag - gaussian_square_echo - GaussianDeriv - Sin - Cos - Sawtooth - Triangle - Square - Sech - SechDeriv - -""" - -from .symbolic_pulses import ( - SymbolicPulse, - ScalableSymbolicPulse, - Gaussian, - GaussianSquare, - GaussianSquareDrag, - gaussian_square_echo, - GaussianDeriv, - Drag, - Constant, - Sin, - Cos, - Sawtooth, - Triangle, - Square, - Sech, - SechDeriv, -) -from .pulse import Pulse -from .waveform import Waveform diff --git a/qiskit/pulse/library/continuous.py b/qiskit/pulse/library/continuous.py deleted file mode 100644 index be69ae8c5d02..000000000000 --- a/qiskit/pulse/library/continuous.py +++ /dev/null @@ -1,430 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# 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. - -# pylint: disable=invalid-unary-operand-type - -"""Module for builtin continuous pulse functions.""" -from __future__ import annotations - -import functools - -import numpy as np -from qiskit.pulse.exceptions import PulseError - - -def constant(times: np.ndarray, amp: complex) -> np.ndarray: - """Continuous constant pulse. - - Args: - times: Times to output pulse for. - amp: Complex pulse amplitude. - """ - return np.full(len(times), amp, dtype=np.complex128) - - -def zero(times: np.ndarray) -> np.ndarray: - """Continuous zero pulse. - - Args: - times: Times to output pulse for. - """ - return constant(times, 0) - - -def square(times: np.ndarray, amp: complex, freq: float, phase: float = 0) -> np.ndarray: - """Continuous square wave. - - Args: - times: Times to output wave for. - amp: Pulse amplitude. Wave range is [-amp, amp]. - freq: Pulse frequency. units of 1/dt. - phase: Pulse phase. - """ - x = times * freq + phase / np.pi - return amp * (2 * (2 * np.floor(x) - np.floor(2 * x)) + 1).astype(np.complex128) - - -def sawtooth(times: np.ndarray, amp: complex, freq: float, phase: float = 0) -> np.ndarray: - """Continuous sawtooth wave. - - Args: - times: Times to output wave for. - amp: Pulse amplitude. Wave range is [-amp, amp]. - freq: Pulse frequency. units of 1/dt. - phase: Pulse phase. - """ - x = times * freq + phase / np.pi - return amp * 2 * (x - np.floor(1 / 2 + x)).astype(np.complex128) - - -def triangle(times: np.ndarray, amp: complex, freq: float, phase: float = 0) -> np.ndarray: - """Continuous triangle wave. - - Args: - times: Times to output wave for. - amp: Pulse amplitude. Wave range is [-amp, amp]. - freq: Pulse frequency. units of 1/dt. - phase: Pulse phase. - """ - return amp * (-2 * np.abs(sawtooth(times, 1, freq, phase=(phase - np.pi / 2) / 2)) + 1).astype( - np.complex128 - ) - - -def cos(times: np.ndarray, amp: complex, freq: float, phase: float = 0) -> np.ndarray: - """Continuous cosine wave. - - Args: - times: Times to output wave for. - amp: Pulse amplitude. - freq: Pulse frequency, units of 1/dt. - phase: Pulse phase. - """ - return amp * np.cos(2 * np.pi * freq * times + phase).astype(np.complex128) - - -def sin(times: np.ndarray, amp: complex, freq: float, phase: float = 0) -> np.ndarray: - """Continuous cosine wave. - - Args: - times: Times to output wave for. - amp: Pulse amplitude. - freq: Pulse frequency, units of 1/dt. - phase: Pulse phase. - """ - return amp * np.sin(2 * np.pi * freq * times + phase).astype(np.complex128) - - -def _fix_gaussian_width( - gaussian_samples: np.ndarray, - amp: complex, - center: float, - sigma: float, - zeroed_width: float | None = None, - rescale_amp: bool = False, - ret_scale_factor: bool = False, -) -> np.ndarray | tuple[np.ndarray, float]: - r"""Enforce that the supplied gaussian pulse is zeroed at a specific width. - - This is achieved by subtracting $\Omega_g(center \pm zeroed_width/2)$ from all samples. - - amp: Pulse amplitude at `center`. - center: Center (mean) of pulse. - sigma: Standard deviation of pulse. - zeroed_width: Subtract baseline from gaussian pulses to make sure - $\Omega_g(center \pm zeroed_width/2)=0$ is satisfied. This is used to avoid - large discontinuities at the start of a gaussian pulse. If unsupplied, - defaults to $2*(center + 1)$ such that $\Omega_g(-1)=0$ and $\Omega_g(2*(center + 1))=0$. - rescale_amp: If True the pulse will be rescaled so that $\Omega_g(center)=amp$. - ret_scale_factor: Return amplitude scale factor. - """ - if zeroed_width is None: - zeroed_width = 2 * (center + 1) - - zero_offset = gaussian(np.array([zeroed_width / 2]), amp, 0, sigma) - gaussian_samples -= zero_offset - amp_scale_factor: complex | float | np.ndarray = 1.0 - if rescale_amp: - amp_scale_factor = amp / (amp - zero_offset) if amp - zero_offset != 0 else 1.0 - gaussian_samples *= amp_scale_factor - - if ret_scale_factor: - return gaussian_samples, amp_scale_factor - return gaussian_samples - - -def gaussian( - times: np.ndarray, - amp: complex, - center: float, - sigma: float, - zeroed_width: float | None = None, - rescale_amp: bool = False, - ret_x: bool = False, -) -> np.ndarray | tuple[np.ndarray, np.ndarray]: - r"""Continuous unnormalized gaussian pulse. - - Integrated area under curve is $\Omega_g(amp, sigma) = amp \times np.sqrt(2\pi \sigma^2)$ - - Args: - times: Times to output pulse for. - amp: Pulse amplitude at `center`. If `zeroed_width` is set pulse amplitude at center - will be $amp-\Omega_g(center \pm zeroed_width/2)$ unless `rescale_amp` is set, - in which case all samples will be rescaled such that the center - amplitude will be `amp`. - center: Center (mean) of pulse. - sigma: Width (standard deviation) of pulse. - zeroed_width: Subtract baseline from gaussian pulses to make sure - $\Omega_g(center \pm zeroed_width/2)=0$ is satisfied. This is used to avoid - large discontinuities at the start of a gaussian pulse. - rescale_amp: If `zeroed_width` is not `None` and `rescale_amp=True` the pulse will - be rescaled so that $\Omega_g(center)=amp$. - ret_x: Return centered and standard deviation normalized pulse location. - $x=(times-center)/sigma. - """ - times = np.asarray(times, dtype=np.complex128) - x = (times - center) / sigma - gauss = amp * np.exp(-(x**2) / 2).astype(np.complex128) - - if zeroed_width is not None: - gauss = _fix_gaussian_width( - gauss, - amp=amp, - center=center, - sigma=sigma, - zeroed_width=zeroed_width, - rescale_amp=rescale_amp, - ) - - if ret_x: - return gauss, x - return gauss - - -def gaussian_deriv( - times: np.ndarray, - amp: complex, - center: float, - sigma: float, - ret_gaussian: bool = False, - zeroed_width: float | None = None, - rescale_amp: bool = False, -) -> np.ndarray | tuple[np.ndarray, np.ndarray]: - r"""Continuous unnormalized gaussian derivative pulse. - - Args: - times: Times to output pulse for. - amp: Pulse amplitude at `center`. - center: Center (mean) of pulse. - sigma: Width (standard deviation) of pulse. - ret_gaussian: Return gaussian with which derivative was taken with. - zeroed_width: Subtract baseline of pulse to make sure - $\Omega_g(center \pm zeroed_width/2)=0$ is satisfied. This is used to avoid - large discontinuities at the start of a pulse. - rescale_amp: If `zeroed_width` is not `None` and `rescale_amp=True` the pulse will - be rescaled so that $\Omega_g(center)=amp$. - """ - gauss, x = gaussian( - times, - amp=amp, - center=center, - sigma=sigma, - zeroed_width=zeroed_width, - rescale_amp=rescale_amp, - ret_x=True, - ) - gauss_deriv = -x / sigma * gauss # Note that x is shifted and normalized by sigma - if ret_gaussian: - return gauss_deriv, gauss - return gauss_deriv - - -def _fix_sech_width( - sech_samples: np.ndarray, - amp: complex, - center: float, - sigma: float, - zeroed_width: float | None = None, - rescale_amp: bool = False, - ret_scale_factor: bool = False, -) -> np.ndarray | tuple[np.ndarray, float]: - r"""Enforce that the supplied sech pulse is zeroed at a specific width. - - This is achieved by subtracting $\Omega_g(center \pm zeroed_width/2)$ from all samples. - - amp: Pulse amplitude at `center`. - center: Center (mean) of pulse. - sigma: Standard deviation of pulse. - zeroed_width: Subtract baseline from sech pulses to make sure - $\Omega_g(center \pm zeroed_width/2)=0$ is satisfied. This is used to avoid - large discontinuities at the start of a sech pulse. If unsupplied, - defaults to $2*(center + 1)$ such that $\Omega_g(-1)=0$ and $\Omega_g(2*(center + 1))=0$. - rescale_amp: If True the pulse will be rescaled so that $\Omega_g(center)=amp$. - ret_scale_factor: Return amplitude scale factor. - """ - if zeroed_width is None: - zeroed_width = 2 * (center + 1) - - zero_offset = sech(np.array([zeroed_width / 2]), amp, 0, sigma) - sech_samples -= zero_offset - amp_scale_factor: complex | float | np.ndarray = 1.0 - if rescale_amp: - amp_scale_factor = amp / (amp - zero_offset) if amp - zero_offset != 0 else 1.0 - sech_samples *= amp_scale_factor - - if ret_scale_factor: - return sech_samples, amp_scale_factor - return sech_samples - - -def sech_fn(x, *args, **kwargs): - r"""Hyperbolic secant function""" - return 1.0 / np.cosh(x, *args, **kwargs) - - -def sech( - times: np.ndarray, - amp: complex, - center: float, - sigma: float, - zeroed_width: float | None = None, - rescale_amp: bool = False, - ret_x: bool = False, -) -> np.ndarray | tuple[np.ndarray, np.ndarray]: - r"""Continuous unnormalized sech pulse. - - Args: - times: Times to output pulse for. - amp: Pulse amplitude at `center`. - center: Center (mean) of pulse. - sigma: Width (standard deviation) of pulse. - zeroed_width: Subtract baseline from pulse to make sure - $\Omega_g(center \pm zeroed_width/2)=0$ is satisfied. This is used to avoid - large discontinuities at the start and end of the pulse. - rescale_amp: If `zeroed_width` is not `None` and `rescale_amp=True` the pulse will - be rescaled so that $\Omega_g(center)=amp$. - ret_x: Return centered and standard deviation normalized pulse location. - $x=(times-center)/sigma$. - """ - times = np.asarray(times, dtype=np.complex128) - x = (times - center) / sigma - sech_out = amp * sech_fn(x).astype(np.complex128) - - if zeroed_width is not None: - sech_out = _fix_sech_width( - sech_out, - amp=amp, - center=center, - sigma=sigma, - zeroed_width=zeroed_width, - rescale_amp=rescale_amp, - ) - - if ret_x: - return sech_out, x - return sech_out - - -def sech_deriv( - times: np.ndarray, amp: complex, center: float, sigma: float, ret_sech: bool = False -) -> np.ndarray | tuple[np.ndarray, np.ndarray]: - """Continuous unnormalized sech derivative pulse. - - Args: - times: Times to output pulse for. - amp: Pulse amplitude at `center`. - center: Center (mean) of pulse. - sigma: Width (standard deviation) of pulse. - ret_sech: Return sech with which derivative was taken with. - """ - sech_out, x = sech(times, amp=amp, center=center, sigma=sigma, ret_x=True) - sech_out_deriv = -sech_out * np.tanh(x) / sigma - if ret_sech: - return sech_out_deriv, sech_out - return sech_out_deriv - - -def gaussian_square( - times: np.ndarray, - amp: complex, - center: float, - square_width: float, - sigma: float, - zeroed_width: float | None = None, -) -> np.ndarray: - r"""Continuous gaussian square pulse. - - Args: - times: Times to output pulse for. - amp: Pulse amplitude. - center: Center of the square pulse component. - square_width: Width of the square pulse component. - sigma: Standard deviation of Gaussian rise/fall portion of the pulse. - zeroed_width: Subtract baseline of gaussian square pulse - to enforce $\OmegaSquare(center \pm zeroed_width/2)=0$. - - Raises: - PulseError: if zeroed_width is not compatible with square_width. - """ - square_start = center - square_width / 2 - square_stop = center + square_width / 2 - if zeroed_width: - if zeroed_width < square_width: - raise PulseError("zeroed_width cannot be smaller than square_width.") - gaussian_zeroed_width = zeroed_width - square_width - else: - gaussian_zeroed_width = None - - funclist = [ - functools.partial( - gaussian, - amp=amp, - center=square_start, - sigma=sigma, - zeroed_width=gaussian_zeroed_width, - rescale_amp=True, - ), - functools.partial( - gaussian, - amp=amp, - center=square_stop, - sigma=sigma, - zeroed_width=gaussian_zeroed_width, - rescale_amp=True, - ), - functools.partial(constant, amp=amp), - ] - condlist = [times <= square_start, times >= square_stop] - return np.piecewise(times.astype(np.complex128), condlist, funclist) - - -def drag( - times: np.ndarray, - amp: complex, - center: float, - sigma: float, - beta: float, - zeroed_width: float | None = None, - rescale_amp: bool = False, -) -> np.ndarray: - r"""Continuous Y-only correction DRAG pulse for standard nonlinear oscillator (SNO) [1]. - - [1] Gambetta, J. M., Motzoi, F., Merkel, S. T. & Wilhelm, F. K. - Analytic control methods for high-fidelity unitary operations - in a weakly nonlinear oscillator. Phys. Rev. A 83, 012308 (2011). - - Args: - times: Times to output pulse for. - amp: Pulse amplitude at `center`. - center: Center (mean) of pulse. - sigma: Width (standard deviation) of pulse. - beta: Y correction amplitude. For the SNO this is $\beta=-\frac{\lambda_1^2}{4\Delta_2}$. - Where $\lambds_1$ is the relative coupling strength between the first excited and second - excited states and $\Delta_2$ is the detuning between the respective excited states. - zeroed_width: Subtract baseline of drag pulse to make sure - $\Omega_g(center \pm zeroed_width/2)=0$ is satisfied. This is used to avoid - large discontinuities at the start of a drag pulse. - rescale_amp: If `zeroed_width` is not `None` and `rescale_amp=True` the pulse will - be rescaled so that $\Omega_g(center)=amp$. - - """ - gauss_deriv, gauss = gaussian_deriv( - times, - amp=amp, - center=center, - sigma=sigma, - ret_gaussian=True, - zeroed_width=zeroed_width, - rescale_amp=rescale_amp, - ) - - return gauss + 1j * beta * gauss_deriv diff --git a/qiskit/pulse/library/pulse.py b/qiskit/pulse/library/pulse.py deleted file mode 100644 index 89d67a381ec5..000000000000 --- a/qiskit/pulse/library/pulse.py +++ /dev/null @@ -1,148 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Pulses are descriptions of waveform envelopes. They can be transmitted by control electronics -to the device. -""" -from __future__ import annotations - -import typing -from abc import ABC, abstractmethod -from typing import Any -from qiskit.utils.deprecate_pulse import deprecate_pulse_func - -from qiskit.circuit.parameterexpression import ParameterExpression - - -if typing.TYPE_CHECKING: - from qiskit.providers import Backend # pylint: disable=cyclic-import - - -class Pulse(ABC): - """The abstract superclass for pulses. Pulses are complex-valued waveform envelopes. The - modulation phase and frequency are specified separately from ``Pulse``s. - """ - - __slots__ = ("duration", "name", "_limit_amplitude") - - limit_amplitude = True - - @abstractmethod - @deprecate_pulse_func - def __init__( - self, - duration: int | ParameterExpression, - name: str | None = None, - limit_amplitude: bool | None = None, - ): - """Abstract base class for pulses - Args: - duration: Duration of the pulse - name: Optional name for the pulse - limit_amplitude: If ``True``, then limit the amplitude of the waveform to 1. - The default value of ``None`` causes the flag value to be - derived from :py:attr:`~limit_amplitude` which is ``True`` - by default but may be set by the user to disable amplitude - checks globally. - """ - if limit_amplitude is None: - limit_amplitude = self.__class__.limit_amplitude - - self.duration = duration - self.name = name - self._limit_amplitude = limit_amplitude - - @property - def id(self) -> int: # pylint: disable=invalid-name - """Unique identifier for this pulse.""" - return id(self) - - @property - @abstractmethod - def parameters(self) -> dict[str, typing.Any]: - """Return a dictionary containing the pulse's parameters.""" - pass - - def is_parameterized(self) -> bool: - """Return True iff the instruction is parameterized.""" - raise NotImplementedError - - def draw( - self, - style: dict[str, Any] | None = None, - backend: Backend | None = None, - time_range: tuple[int, int] | None = None, - time_unit: str = "dt", - show_waveform_info: bool = True, - plotter: str = "mpl2d", - axis: Any | None = None, - ): - """Plot the interpolated envelope of pulse. - - Args: - style: Stylesheet options. This can be dictionary or preset stylesheet classes. See - :py:class:`~qiskit.visualization.pulse_v2.stylesheets.IQXStandard`, - :py:class:`~qiskit.visualization.pulse_v2.stylesheets.IQXSimple`, and - :py:class:`~qiskit.visualization.pulse_v2.stylesheets.IQXDebugging` for details of - preset stylesheets. - backend (Optional[BaseBackend]): Backend object to play the input pulse program. - If provided, the plotter may use to make the visualization hardware aware. - time_range: Set horizontal axis limit. Tuple ``(tmin, tmax)``. - time_unit: The unit of specified time range either ``dt`` or ``ns``. - The unit of ``ns`` is available only when ``backend`` object is provided. - show_waveform_info: Show waveform annotations, i.e. name, of waveforms. - Set ``True`` to show additional information about waveforms. - plotter: Name of plotter API to generate an output image. - One of following APIs should be specified:: - - mpl2d: Matplotlib API for 2D image generation. - Matplotlib API to generate 2D image. Charts are placed along y axis with - vertical offset. This API takes matplotlib.axes.Axes as `axis` input. - - `axis` and `style` kwargs may depend on the plotter. - axis: Arbitrary object passed to the plotter. If this object is provided, - the plotters use a given ``axis`` instead of internally initializing - a figure object. This object format depends on the plotter. - See plotter argument for details. - - Returns: - Visualization output data. - The returned data type depends on the ``plotter``. - If matplotlib family is specified, this will be a ``matplotlib.pyplot.Figure`` data. - """ - # pylint: disable=cyclic-import - from qiskit.visualization import pulse_drawer - - return pulse_drawer( - program=self, - style=style, - backend=backend, - time_range=time_range, - time_unit=time_unit, - show_waveform_info=show_waveform_info, - plotter=plotter, - axis=axis, - ) - - @abstractmethod - def __eq__(self, other: object) -> bool: - if not isinstance(other, Pulse): - return NotImplemented - return isinstance(other, type(self)) - - @abstractmethod - def __hash__(self) -> int: - raise NotImplementedError - - @abstractmethod - def __repr__(self) -> str: - raise NotImplementedError diff --git a/qiskit/pulse/library/samplers/__init__.py b/qiskit/pulse/library/samplers/__init__.py deleted file mode 100644 index ea5e2dd5d16a..000000000000 --- a/qiskit/pulse/library/samplers/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# 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. - -"""Module for methods which sample continuous functions.""" - -from .decorators import left, right, midpoint diff --git a/qiskit/pulse/library/samplers/decorators.py b/qiskit/pulse/library/samplers/decorators.py deleted file mode 100644 index db6aabd7b1de..000000000000 --- a/qiskit/pulse/library/samplers/decorators.py +++ /dev/null @@ -1,295 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# 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. - -"""Sampler decorator module for sampling of continuous pulses to discrete pulses to be -exposed to user. - -Some atypical boilerplate has been added to solve the problem of decorators not preserving -their wrapped function signatures. Below we explain the problem that samplers solve and how -we implement this. - -A sampler is a function that takes an continuous pulse function with signature: - ```python - def f(times: np.ndarray, *args, **kwargs) -> np.ndarray: - ... - ``` -and returns a new function: - def f(duration: int, *args, **kwargs) -> Waveform: - ... - -Samplers are used to build up pulse waveforms from continuous pulse functions. - -In Python the creation of a dynamic function that wraps another function will cause -the underlying signature and documentation of the underlying function to be overwritten. -In order to circumvent this issue the Python standard library provides the decorator -`functools.wraps` which allows the programmer to expose the names and signature of the -wrapped function as those of the dynamic function. - -Samplers are implemented by creating a function with signature - @sampler - def left(continuous_pulse: Callable, duration: int, *args, **kwargs) - ... - -This will create a sampler function for `left`. Since it is a dynamic function it would not -have the docstring of `left` available too `help`. This could be fixed by wrapping with -`functools.wraps` in the `sampler`, but this would then cause the signature to be that of the -sampler function which is called on the continuous pulse, below: - `(continuous_pulse: Callable, duration: int, *args, **kwargs)`` -This is not correct for the sampler as the output sampled functions accept only a function. -For the standard sampler we get around this by not using `functools.wraps` and -explicitly defining our samplers such as `left`, `right` and `midpoint` and -calling `sampler` internally on the function that implements the sampling schemes such as -`left_sample`, `right_sample` and `midpoint_sample` respectively. See `left` for an example of this. - - -In this way our standard samplers will expose the proper help signature, but a user can -still create their own sampler with - @sampler - def custom_sampler(time, *args, **kwargs): - ... -However, in this case it will be missing documentation of the underlying sampling methods. -We believe that the definition of custom samplers will be rather infrequent. - -However, users will frequently apply sampler instances too continuous pulses. Therefore, a different -approach was required for sampled continuous functions (the output of an continuous pulse function -decorated by a sampler instance). - -A sampler instance is a decorator that may be used to wrap continuous pulse functions such as -linear below: -```python - @left - def linear(times: np.ndarray, m: float, b: float) -> np.ndarray: - ```Linear test function - Args: - times: Input times. - m: Slope. - b: Intercept - Returns: - np.ndarray - ``` - return m*times+b -``` -Which after decoration may be called with a duration rather than an array of times - ```python - duration = 10 - pulse_envelope = linear(10, 0.1, 0.1) - ``` -If one calls help on `linear` they will find - ``` - linear(duration:int, *args, **kwargs) -> numpy.ndarray - Discretized continuous pulse function: `linear` using - sampler: `_left`. - - The first argument (time) of the continuous pulse function has been replaced with - a discretized `duration` of type (int). - - Args: - duration (int) - *args: Remaining arguments of continuous pulse function. - See continuous pulse function documentation below. - **kwargs: Remaining kwargs of continuous pulse function. - See continuous pulse function documentation below. - - Sampled continuous function: - - function linear in module test.python.pulse.test_samplers - linear(x:numpy.ndarray, m:float, b:float) -> numpy.ndarray - Linear test function - Args: - x: Input times. - m: Slope. - b: Intercept - Returns: - np.ndarray - ``` -This is partly because `functools.wraps` has been used on the underlying function. -This in itself is not sufficient as the signature of the sampled function has -`duration`, whereas the signature of the continuous function is `time`. - -This is achieved by removing `__wrapped__` set by `functools.wraps` in order to preserve -the correct signature and also applying `_update_annotations` and `_update_docstring` -to the generated function which corrects the function annotations and adds an informative -docstring respectively. - -The user therefore has access to the correct sampled function docstring in its entirety, while -still seeing the signature for the continuous pulse function and all of its arguments. -""" -from __future__ import annotations -import functools -import textwrap -import pydoc -from collections.abc import Callable - -import numpy as np - -from ...exceptions import PulseError -from ..waveform import Waveform -from . import strategies - - -def functional_pulse(func: Callable) -> Callable: - """A decorator for generating Waveform from python callable. - - Args: - func: A function describing pulse envelope. - - Raises: - PulseError: when invalid function is specified. - """ - - @functools.wraps(func) - def to_pulse(duration, *args, name=None, **kwargs): - """Return Waveform.""" - if isinstance(duration, (int, np.integer)) and duration > 0: - samples = func(duration, *args, **kwargs) - samples = np.asarray(samples, dtype=np.complex128) - return Waveform(samples=samples, name=name) - raise PulseError("The first argument must be an integer value representing duration.") - - return to_pulse - - -def _update_annotations(discretized_pulse: Callable) -> Callable: - """Update annotations of discretized continuous pulse function with duration. - - Args: - discretized_pulse: Discretized decorated continuous pulse. - """ - undecorated_annotations = list(discretized_pulse.__annotations__.items()) - decorated_annotations = undecorated_annotations[1:] - decorated_annotations.insert(0, ("duration", int)) - discretized_pulse.__annotations__ = dict(decorated_annotations) - return discretized_pulse - - -def _update_docstring(discretized_pulse: Callable, sampler_inst: Callable) -> Callable: - """Update annotations of discretized continuous pulse function. - - Args: - discretized_pulse: Discretized decorated continuous pulse. - sampler_inst: Applied sampler. - """ - wrapped_docstring = pydoc.render_doc(discretized_pulse, "%s") - header, body = wrapped_docstring.split("\n", 1) - body = textwrap.indent(body, " ") - wrapped_docstring = header + body - updated_ds = f""" - Discretized continuous pulse function: `{discretized_pulse.__name__}` using - sampler: `{sampler_inst.__name__}`. - - The first argument (time) of the continuous pulse function has been replaced with - a discretized `duration` of type (int). - - Args: - duration (int) - *args: Remaining arguments of continuous pulse function. - See continuous pulse function documentation below. - **kwargs: Remaining kwargs of continuous pulse function. - See continuous pulse function documentation below. - - Sampled continuous function: - - {wrapped_docstring} - """ - - discretized_pulse.__doc__ = updated_ds - return discretized_pulse - - -def sampler(sample_function: Callable) -> Callable: - """Sampler decorator base method. - - Samplers are used for converting an continuous function to a discretized pulse. - - They operate on a function with the signature: - `def f(times: np.ndarray, *args, **kwargs) -> np.ndarray` - Where `times` is a numpy array of floats with length n_times and the output array - is a complex numpy array with length n_times. The output of the decorator is an - instance of `FunctionalPulse` with signature: - `def g(duration: int, *args, **kwargs) -> Waveform` - - Note if your continuous pulse function outputs a `complex` scalar rather than a - `np.ndarray`, you should first vectorize it before applying a sampler. - - - This class implements the sampler boilerplate for the sampler. - - Args: - sample_function: A sampler function to be decorated. - """ - - def generate_sampler(continuous_pulse: Callable) -> Callable: - """Return a decorated sampler function.""" - - @functools.wraps(continuous_pulse) - def call_sampler(duration: int, *args, **kwargs) -> np.ndarray: - """Replace the call to the continuous function with a call to the sampler applied - to the analytic pulse function.""" - sampled_pulse = sample_function(continuous_pulse, duration, *args, **kwargs) - return np.asarray(sampled_pulse, dtype=np.complex128) - - # Update type annotations for wrapped continuous function to be discrete - call_sampler = _update_annotations(call_sampler) - # Update docstring with that of the sampler and include sampled function documentation. - call_sampler = _update_docstring(call_sampler, sample_function) - # Unset wrapped to return base sampler signature - # but still get rest of benefits of wraps - # such as __name__, __qualname__ - call_sampler.__dict__.pop("__wrapped__") - # wrap with functional pulse - return functional_pulse(call_sampler) - - return generate_sampler - - -def left(continuous_pulse: Callable) -> Callable: - r"""Left sampling strategy decorator. - - See `pulse.samplers.sampler` for more information. - - For `duration`, return: - $$\{f(t) \in \mathbb{C} | t \in \mathbb{Z} \wedge 0<=t<\texttt{duration}\}$$ - - Args: - continuous_pulse: To sample. - """ - - return sampler(strategies.left_sample)(continuous_pulse) - - -def right(continuous_pulse: Callable) -> Callable: - r"""Right sampling strategy decorator. - - See `pulse.samplers.sampler` for more information. - - For `duration`, return: - $$\{f(t) \in \mathbb{C} | t \in \mathbb{Z} \wedge 0 Callable: - r"""Midpoint sampling strategy decorator. - - See `pulse.samplers.sampler` for more information. - - For `duration`, return: - $$\{f(t+0.5) \in \mathbb{C} | t \in \mathbb{Z} \wedge 0<=t<\texttt{duration}\}$$ - - Args: - continuous_pulse: To sample. - """ - return sampler(strategies.midpoint_sample)(continuous_pulse) diff --git a/qiskit/pulse/library/samplers/strategies.py b/qiskit/pulse/library/samplers/strategies.py deleted file mode 100644 index c0886138d910..000000000000 --- a/qiskit/pulse/library/samplers/strategies.py +++ /dev/null @@ -1,71 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# 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. - - -"""Sampler strategy module for sampler functions. - -Sampler functions have signature. - ```python - def sampler_function(continuous_pulse: Callable, duration: int, *args, **kwargs) -> np.ndarray: - ... - ``` -where the supplied `continuous_pulse` is a function with signature: - ```python - def f(times: np.ndarray, *args, **kwargs) -> np.ndarray: - ... - ``` -The sampler will call the `continuous_pulse` function with a set of times it will decide -according to the sampling strategy it implements along with the passed `args` and `kwargs`. -""" - -from typing import Callable - -import numpy as np - - -def left_sample(continuous_pulse: Callable, duration: int, *args, **kwargs) -> np.ndarray: - """Left sample a continuous function. - - Args: - continuous_pulse: Continuous pulse function to sample. - duration: Duration to sample for. - *args: Continuous pulse function args. - **kwargs: Continuous pulse function kwargs. - """ - times = np.arange(duration) - return continuous_pulse(times, *args, **kwargs) - - -def right_sample(continuous_pulse: Callable, duration: int, *args, **kwargs) -> np.ndarray: - """Sampling strategy for decorator. - - Args: - continuous_pulse: Continuous pulse function to sample. - duration: Duration to sample for. - *args: Continuous pulse function args. - **kwargs: Continuous pulse function kwargs. - """ - times = np.arange(1, duration + 1) - return continuous_pulse(times, *args, **kwargs) - - -def midpoint_sample(continuous_pulse: Callable, duration: int, *args, **kwargs) -> np.ndarray: - """Sampling strategy for decorator. - - Args: - continuous_pulse: Continuous pulse function to sample. - duration: Duration to sample for. - *args: Continuous pulse function args. - **kwargs: Continuous pulse function kwargs. - """ - times = np.arange(1 / 2, duration + 1 / 2) - return continuous_pulse(times, *args, **kwargs) diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py deleted file mode 100644 index 560e1d529958..000000000000 --- a/qiskit/pulse/library/symbolic_pulses.py +++ /dev/null @@ -1,1991 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -# pylint: disable=invalid-name - -"""Symbolic waveform module. - -These are pulses which are described by symbolic equations for their envelopes and for their -parameter constraints. -""" -from __future__ import annotations -import functools -import warnings -from collections.abc import Mapping, Callable -from copy import deepcopy -from typing import Any - -import numpy as np -import symengine as sym - -from qiskit.circuit.parameterexpression import ParameterExpression, ParameterValueType -from qiskit.pulse.exceptions import PulseError -from qiskit.pulse.library.pulse import Pulse -from qiskit.pulse.library.waveform import Waveform -from qiskit.utils.deprecate_pulse import deprecate_pulse_func - - -def _lifted_gaussian( - t: sym.Symbol, - center: sym.Symbol | sym.Expr | complex, - t_zero: sym.Symbol | sym.Expr | complex, - sigma: sym.Symbol | sym.Expr | complex, -) -> sym.Expr: - r"""Helper function that returns a lifted Gaussian symbolic equation. - - For :math:`\sigma=` ``sigma`` the symbolic equation will be - - .. math:: - - f(x) = \exp\left(-\frac12 \left(\frac{x - \mu}{\sigma}\right)^2 \right), - - with the center :math:`\mu=` ``duration/2``. - Then, each output sample :math:`y` is modified according to: - - .. math:: - - y \mapsto \frac{y-y^*}{1.0-y^*}, - - where :math:`y^*` is the value of the un-normalized Gaussian at the endpoints of the pulse. - This sets the endpoints to :math:`0` while preserving the amplitude at the center, - i.e. :math:`y` is set to :math:`1.0`. - - Args: - t: Symbol object representing time. - center: Symbol or expression representing the middle point of the samples. - t_zero: The value of t at which the pulse is lowered to 0. - sigma: Symbol or expression representing Gaussian sigma. - - Returns: - Symbolic equation. - """ - # Sympy automatically does expand. - # This causes expression inconsistency after qpy round-trip serializing through sympy. - # See issue for details: https://github.com/symengine/symengine.py/issues/409 - t_shifted = (t - center).expand() - t_offset = (t_zero - center).expand() - - gauss = sym.exp(-((t_shifted / sigma) ** 2) / 2) - offset = sym.exp(-((t_offset / sigma) ** 2) / 2) - - return (gauss - offset) / (1 - offset) - - -@functools.lru_cache(maxsize=None) -def _is_amplitude_valid( - envelope_lam: Callable, time: tuple[float, ...], *fargs: float -) -> bool | np.bool_: - """A helper function to validate maximum amplitude limit. - - Result is cached for better performance. - - Args: - envelope_lam: The SymbolicPulse's lambdified envelope_lam expression. - time: The SymbolicPulse's time array, given as a tuple for hashability. - fargs: The arguments for the lambdified envelope_lam, as given by `_get_expression_args`, - except for the time array. - - Returns: - Return True if no sample point exceeds 1.0 in absolute value. - """ - - time = np.asarray(time, dtype=float) - samples_norm = np.abs(envelope_lam(time, *fargs)) - epsilon = 1e-7 # The value of epsilon mimics that of Waveform._clip() - return np.all(samples_norm < 1.0 + epsilon) - - -def _get_expression_args(expr: sym.Expr, params: dict[str, float]) -> list[np.ndarray | float]: - """A helper function to get argument to evaluate expression. - - Args: - expr: Symbolic expression to evaluate. - params: Dictionary of parameter, which is a superset of expression arguments. - - Returns: - Arguments passed to the lambdified expression. - - Raises: - PulseError: When a free symbol value is not defined in the pulse instance parameters. - """ - args: list[np.ndarray | float] = [] - for symbol in sorted(expr.free_symbols, key=lambda s: s.name): - if symbol.name == "t": - # 't' is a special parameter to represent time vector. - # This should be place at first to broadcast other parameters - # in symengine lambdify function. - times = np.arange(0, params["duration"]) + 1 / 2 - args.insert(0, times) - continue - try: - args.append(params[symbol.name]) - except KeyError as ex: - raise PulseError( - f"Pulse parameter '{symbol.name}' is not defined for this instance. " - "Please check your waveform expression is correct." - ) from ex - return args - - -class LambdifiedExpression: - """Descriptor to lambdify symbolic expression with cache. - - When a new symbolic expression is assigned for the first time, :class:`.LambdifiedExpression` - will internally lambdify the expressions and store the resulting callbacks in its cache. - The next time it encounters the same expression it will return the cached callbacks - thereby increasing the code's speed. - - Note that this class is a python `Descriptor`_, and thus not intended to be - directly called by end-users. This class is designed to be attached to the - :class:`.SymbolicPulse` as attributes for symbolic expressions. - - _`Descriptor`: https://docs.python.org/3/reference/datamodel.html#descriptors - """ - - def __init__(self, attribute: str): - """Create new descriptor. - - Args: - attribute: Name of attribute of :class:`.SymbolicPulse` that returns - the target expression to evaluate. - """ - self.attribute = attribute - self.lambda_funcs: dict[int, Callable] = {} - - def __get__(self, instance, owner) -> Callable: - expr = getattr(instance, self.attribute, None) - if expr is None: - raise PulseError(f"'{self.attribute}' of '{instance.pulse_type}' is not assigned.") - key = hash(expr) - if key not in self.lambda_funcs: - self.__set__(instance, expr) - - return self.lambda_funcs[key] - - def __set__(self, instance, value): - key = hash(value) - if key not in self.lambda_funcs: - params: list[Any] = [] - for p in sorted(value.free_symbols, key=lambda s: s.name): - if p.name == "t": - # Argument "t" must be placed at first. This is a vector. - params.insert(0, p) - continue - params.append(p) - - try: - lamb = sym.lambdify(params, [value], real=False) - - def _wrapped_lamb(*args): - if isinstance(args[0], np.ndarray): - # When the args[0] is a vector ("t"), tile other arguments args[1:] - # to prevent evaluation from looping over each element in t. - t = args[0] - args = np.hstack( - ( - t.reshape(t.size, 1), - np.tile(args[1:], t.size).reshape(t.size, len(args) - 1), - ) - ) - return lamb(args) - - func = _wrapped_lamb - except RuntimeError: - # Currently symengine doesn't support complex_double version for - # several functions such as comparison operator and piecewise. - # If expression contains these function, it fall back to sympy lambdify. - # See https://github.com/symengine/symengine.py/issues/406 for details. - import sympy - - func = sympy.lambdify(params, value) - - self.lambda_funcs[key] = func - - -class SymbolicPulse(Pulse): - r"""The pulse representation model with parameters and symbolic expressions. - - A symbolic pulse instance can be defined with an envelope and parameter constraints. - Envelope and parameter constraints should be provided as symbolic expressions. - Rather than creating a subclass, different pulse shapes can be distinguished by - the instance attributes :attr:`SymbolicPulse.envelope` and :attr:`SymbolicPulse.pulse_type`. - - The symbolic expressions must be defined either with SymPy_ or Symengine_. - Usually Symengine-based expression is much more performant for instantiation - of the :class:`SymbolicPulse`, however, it doesn't support every functions available in SymPy. - You may need to choose proper library depending on how you define your pulses. - Symengine works in the most envelopes and constraints, and thus it is recommended to use - this library especially when your program contains a lot of pulses. - Also note that Symengine has the limited platform support and may not be available - for your local system. Symengine is a required dependency for Qiskit on platforms - that support it will always be installed along with Qiskit on macOS ``x86_64`` and ``arm64``, - and Linux ``x86_64``, ``aarch64``, and ``ppc64le``. - For 64-bit Windows users they will need to manual install it. - For 32-bit platforms such as ``i686`` and ``armv7`` Linux, and on Linux ``s390x`` - there are no pre-compiled packages available and to use symengine you'll need to - compile it from source. If Symengine is not available in your environment SymPy will be used. - - .. _SymPy: https://www.sympy.org/en/index.html - .. _Symengine: https://symengine.org - - .. _symbolic_pulse_envelope: - - .. rubric:: Envelope function - - The waveform at time :math:`t` is generated by the :meth:`get_waveform` according to - - .. math:: - - F(t, \Theta) = \times F(t, {\rm duration}, \overline{\rm params}) - - where :math:`\Theta` is the set of full pulse parameters in the :attr:`SymbolicPulse.parameters` - dictionary which must include the :math:`\rm duration`. - Note that the :math:`F` is an envelope of the waveform, and a programmer must provide this - as a symbolic expression. :math:`\overline{\rm params}` can be arbitrary complex values - as long as they pass :meth:`.validate_parameters` and your quantum backend can accept. - The time :math:`t` and :math:`\rm duration` are in units of dt, i.e. sample time resolution, - and this function is sampled with a discrete time vector in :math:`[0, {\rm duration}]` - sampling the pulse envelope at every 0.5 dt (middle sampling strategy) when - the :meth:`SymbolicPulse.get_waveform` method is called. - The sample data is not generated until this method is called - thus a symbolic pulse instance only stores parameter values and waveform shape, - which greatly reduces memory footprint during the program generation. - - - .. _symbolic_pulse_validation: - - .. rubric:: Pulse validation - - When a symbolic pulse is instantiated, the method :meth:`.validate_parameters` is called, - and performs validation of the pulse. The validation process involves testing the constraint - functions and the maximal amplitude of the pulse (see below). While the validation process - will improve code stability, it will reduce performance and might create - compatibility issues (particularly with JAX). Therefore, it is possible to disable the - validation by setting the class attribute :attr:`.disable_validation` to ``True``. - - .. _symbolic_pulse_constraints: - - .. rubric:: Constraint functions - - Constraints on the parameters are defined with an instance attribute - :attr:`SymbolicPulse.constraints` which can be provided through the constructor. - The constraints value must be a symbolic expression, which is a - function of parameters to be validated and must return a boolean value - being ``True`` when parameters are valid. - If there are multiple conditions to be evaluated, these conditions can be - concatenated with logical expressions such as ``And`` and ``Or`` in SymPy or Symengine. - The symbolic pulse instance can be played only when the constraint function returns ``True``. - The constraint is evaluated when :meth:`.validate_parameters` is called. - - - .. _symbolic_pulse_eval_condition: - - .. rubric:: Maximum amplitude validation - - When you play a pulse in a quantum backend, you might face the restriction on the power - that your waveform generator can handle. Usually, the pulse amplitude is normalized - by this maximum power, namely :math:`\max |F| \leq 1`. This condition is - evaluated along with above constraints when you set ``limit_amplitude = True`` in the constructor. - To evaluate maximum amplitude of the waveform, we need to call :meth:`get_waveform`. - However, this introduces a significant overhead in the validation, and this cannot be ignored - when you repeatedly instantiate symbolic pulse instances. - :attr:`SymbolicPulse.valid_amp_conditions` provides a condition to skip this waveform validation, - and the waveform is not generated as long as this condition returns ``True``, - so that `healthy` symbolic pulses are created very quick. - For example, for a simple pulse shape like ``amp * cos(f * t)``, we know that - pulse amplitude is valid as long as ``amp`` remains less than magnitude 1.0. - So ``abs(amp) <= 1`` could be passed as :attr:`SymbolicPulse.valid_amp_conditions` to skip - doing a full waveform evaluation for amplitude validation. - This expression is provided through the constructor. If this is not provided, - the waveform is generated everytime when :meth:`.validate_parameters` is called. - - - .. rubric:: Examples - - This is how a user can instantiate a symbolic pulse instance. - In this example, we instantiate a custom `Sawtooth` envelope. - - .. code-block:: python - - from qiskit.pulse.library import SymbolicPulse - - my_pulse = SymbolicPulse( - pulse_type="Sawtooth", - duration=100, - parameters={"amp": 0.1, "freq": 0.05}, - name="pulse1", - ) - - Note that :class:`SymbolicPulse` can be instantiated without providing - the envelope and constraints. However, this instance cannot generate waveforms - without knowing the envelope definition. Now you need to provide the envelope. - - .. plot:: - :alt: Output from the previous code. - :include-source: - - import sympy - from qiskit.pulse.library import SymbolicPulse - - t, amp, freq = sympy.symbols("t, amp, freq") - envelope = 2 * amp * (freq * t - sympy.floor(1 / 2 + freq * t)) - - my_pulse = SymbolicPulse( - pulse_type="Sawtooth", - duration=100, - parameters={"amp": 0.1, "freq": 0.05}, - envelope=envelope, - name="pulse1", - ) - - my_pulse.draw() - - Likewise, you can define :attr:`SymbolicPulse.constraints` for ``my_pulse``. - After providing the envelope definition, you can generate the waveform data. - Note that it would be convenient to define a factory function that automatically - accomplishes this procedure. - - .. plot:: - :include-source: - :nofigs: - - def Sawtooth(duration, amp, freq, name): - t, amp, freq = sympy.symbols("t, amp, freq") - - instance = SymbolicPulse( - pulse_type="Sawtooth", - duration=duration, - parameters={"amp": amp, "freq": freq}, - envelope=2 * amp * (freq * t - sympy.floor(1 / 2 + freq * t)), - name=name, - ) - - return instance - - You can also provide a :class:`Parameter` object in the ``parameters`` dictionary, - or define ``duration`` with a :class:`Parameter` object when you instantiate - the symbolic pulse instance. - A waveform cannot be generated until you assign all unbounded parameters. - Note that parameters will be assigned through the schedule playing the pulse. - - - .. _symbolic_pulse_serialize: - - .. rubric:: Serialization - - The :class:`~SymbolicPulse` subclass can be serialized along with the - symbolic expressions through :mod:`qiskit.qpy`. - A user can therefore create a custom pulse subclass with a novel envelope and constraints, - and then one can instantiate the class with certain parameters to run on a backend. - This pulse instance can be saved in the QPY binary, which can be loaded afterwards - even within the environment not having original class definition loaded. - This mechanism also allows us to easily share a pulse program including - custom pulse instructions with collaborators. - """ - - __slots__ = ( - "_pulse_type", - "_params", - "_envelope", - "_constraints", - "_valid_amp_conditions", - ) - - disable_validation = False - - # Lambdify caches keyed on sympy expressions. Returns the corresponding callable. - _envelope_lam = LambdifiedExpression("_envelope") - _constraints_lam = LambdifiedExpression("_constraints") - _valid_amp_conditions_lam = LambdifiedExpression("_valid_amp_conditions") - - @deprecate_pulse_func - def __init__( - self, - pulse_type: str, - duration: ParameterExpression | int, - parameters: Mapping[str, ParameterExpression | complex] | None = None, - name: str | None = None, - limit_amplitude: bool | None = None, - envelope: sym.Expr | None = None, - constraints: sym.Expr | None = None, - valid_amp_conditions: sym.Expr | None = None, - ): - """Create a parametric pulse. - - Args: - pulse_type: Display name of this pulse shape. - duration: Duration of pulse. - parameters: Dictionary of pulse parameters that defines the pulse envelope. - name: Display name for this particular pulse envelope. - limit_amplitude: If ``True``, then limit the absolute value of the amplitude of the - waveform to 1. The default is ``True`` and the amplitude is constrained to 1. - envelope: Pulse envelope expression. - constraints: Pulse parameter constraint expression. - valid_amp_conditions: Extra conditions to skip a full-waveform check for the - amplitude limit. If this condition is not met, then the validation routine - will investigate the full-waveform and raise an error when the amplitude norm - of any data point exceeds 1.0. If not provided, the validation always - creates a full-waveform. - - Raises: - PulseError: When not all parameters are listed in the attribute :attr:`PARAM_DEF`. - """ - super().__init__( - duration=duration, - name=name, - limit_amplitude=limit_amplitude, - ) - if parameters is None: - parameters = {} - - self._pulse_type = pulse_type - self._params = parameters - - self._envelope = envelope - self._constraints = constraints - self._valid_amp_conditions = valid_amp_conditions - if not self.__class__.disable_validation: - self.validate_parameters() - - def __getattr__(self, item): - # Get pulse parameters with attribute-like access. - params = object.__getattribute__(self, "_params") - if item not in params: - raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{item}'") - return params[item] - - @property - def pulse_type(self) -> str: - """Return display name of the pulse shape.""" - return self._pulse_type - - @property - def envelope(self) -> sym.Expr: - """Return symbolic expression for the pulse envelope.""" - return self._envelope - - @property - def constraints(self) -> sym.Expr: - """Return symbolic expression for the pulse parameter constraints.""" - return self._constraints - - @property - def valid_amp_conditions(self) -> sym.Expr: - """Return symbolic expression for the pulse amplitude constraints.""" - return self._valid_amp_conditions - - def get_waveform(self) -> Waveform: - r"""Return a Waveform with samples filled according to the formula that the pulse - represents and the parameter values it contains. - - Since the returned array is a discretized time series of the continuous function, - this method uses a midpoint sampler. For ``duration``, return: - - .. math:: - - \{f(t+0.5) \in \mathbb{C} | t \in \mathbb{Z} \wedge 0<=t<\texttt{duration}\} - - Returns: - A waveform representation of this pulse. - - Raises: - PulseError: When parameters are not assigned. - PulseError: When expression for pulse envelope is not assigned. - """ - if self.is_parameterized(): - raise PulseError("Unassigned parameter exists. All parameters must be assigned.") - - if self._envelope is None: - raise PulseError("Pulse envelope expression is not assigned.") - - fargs = _get_expression_args(self._envelope, self.parameters) - return Waveform(samples=self._envelope_lam(*fargs), name=self.name) - - def validate_parameters(self) -> None: - """Validate parameters. - - Raises: - PulseError: If the parameters passed are not valid. - """ - if self.is_parameterized(): - return - - if self._constraints is not None: - fargs = _get_expression_args(self._constraints, self.parameters) - if not bool(self._constraints_lam(*fargs)): - param_repr = ", ".join(f"{p}={v}" for p, v in self.parameters.items()) - const_repr = str(self._constraints) - raise PulseError( - f"Assigned parameters {param_repr} violate following constraint: {const_repr}." - ) - - if self._limit_amplitude: - if self._valid_amp_conditions is not None: - fargs = _get_expression_args(self._valid_amp_conditions, self.parameters) - check_full_waveform = not bool(self._valid_amp_conditions_lam(*fargs)) - else: - check_full_waveform = True - - if check_full_waveform: - # Check full waveform only when the condition is satisified or - # evaluation condition is not provided. - # This operation is slower due to overhead of 'get_waveform'. - fargs = _get_expression_args(self._envelope, self.parameters) - - if not _is_amplitude_valid(self._envelope_lam, tuple(fargs.pop(0)), *fargs): - param_repr = ", ".join(f"{p}={v}" for p, v in self.parameters.items()) - raise PulseError( - f"Maximum pulse amplitude norm exceeds 1.0 with parameters {param_repr}." - "This can be overruled by setting Pulse.limit_amplitude." - ) - - def is_parameterized(self) -> bool: - """Return True iff the instruction is parameterized.""" - return any(isinstance(val, ParameterExpression) for val in self.parameters.values()) - - @property - def parameters(self) -> dict[str, Any]: - params: dict[str, ParameterExpression | complex | int] = {"duration": self.duration} - params.update(self._params) - return params - - def __eq__(self, other: object) -> bool: - if not isinstance(other, SymbolicPulse): - return NotImplemented - - if self._pulse_type != other._pulse_type: - return False - - if self._envelope != other._envelope: - return False - - if self.parameters != other.parameters: - return False - - return True - - def __repr__(self) -> str: - param_repr = ", ".join(f"{p}={v}" for p, v in self.parameters.items()) - name_repr = f", name='{self.name}'" if self.name is not None else "" - return f"{self._pulse_type}({param_repr}{name_repr})" - - __hash__ = None - - -class ScalableSymbolicPulse(SymbolicPulse): - r"""Subclass of :class:`SymbolicPulse` for pulses with scalable envelope. - - Instance of :class:`ScalableSymbolicPulse` behaves the same as an instance of - :class:`SymbolicPulse`, but its envelope is assumed to have a scalable form - :math:`\text{amp}\times\exp\left(i\times\text{angle}\right)\times\text{F} - \left(t,\text{parameters}\right)`, - where :math:`\text{F}` is some function describing the rest of the envelope, - and both `amp` and `angle` are real (float). Note that both `amp` and `angle` are - stored in the :attr:`parameters` dictionary of the :class:`ScalableSymbolicPulse` - instance. - - When two :class:`ScalableSymbolicPulse` objects are equated, instead of comparing - `amp` and `angle` individually, only the complex amplitude - :math:'\text{amp}\times\exp\left(i\times\text{angle}\right)' is compared. - """ - - def __init__( - self, - pulse_type: str, - duration: ParameterExpression | int, - amp: ParameterValueType, - angle: ParameterValueType, - parameters: dict[str, ParameterExpression | complex] | None = None, - name: str | None = None, - limit_amplitude: bool | None = None, - envelope: sym.Expr | None = None, - constraints: sym.Expr | None = None, - valid_amp_conditions: sym.Expr | None = None, - ): - """Create a scalable symbolic pulse. - - Args: - pulse_type: Display name of this pulse shape. - duration: Duration of pulse. - amp: The magnitude of the complex amplitude of the pulse. - angle: The phase of the complex amplitude of the pulse. - parameters: Dictionary of pulse parameters that defines the pulse envelope. - name: Display name for this particular pulse envelope. - limit_amplitude: If ``True``, then limit the absolute value of the amplitude of the - waveform to 1. The default is ``True`` and the amplitude is constrained to 1. - envelope: Pulse envelope expression. - constraints: Pulse parameter constraint expression. - valid_amp_conditions: Extra conditions to skip a full-waveform check for the - amplitude limit. If this condition is not met, then the validation routine - will investigate the full-waveform and raise an error when the amplitude norm - of any data point exceeds 1.0. If not provided, the validation always - creates a full-waveform. - - Raises: - PulseError: If ``amp`` is complex. - """ - if isinstance(amp, complex): - raise PulseError( - "amp represents the magnitude of the complex amplitude and can't be complex" - ) - - if not isinstance(parameters, dict): - parameters = {"amp": amp, "angle": angle} - else: - parameters = deepcopy(parameters) - parameters["amp"] = amp - parameters["angle"] = angle - - super().__init__( - pulse_type=pulse_type, - duration=duration, - parameters=parameters, - name=name, - limit_amplitude=limit_amplitude, - envelope=envelope, - constraints=constraints, - valid_amp_conditions=valid_amp_conditions, - ) - - # pylint: disable=too-many-return-statements - def __eq__(self, other: object) -> bool: - if not isinstance(other, ScalableSymbolicPulse): - return NotImplemented - - if self._pulse_type != other._pulse_type: - return False - - if self._envelope != other._envelope: - return False - - complex_amp1 = self.amp * np.exp(1j * self.angle) - complex_amp2 = other.amp * np.exp(1j * other.angle) - - if isinstance(complex_amp1, ParameterExpression) or isinstance( - complex_amp2, ParameterExpression - ): - if complex_amp1 != complex_amp2: - return False - else: - if not np.isclose(complex_amp1, complex_amp2): - return False - - for key, value in self.parameters.items(): - if key not in ["amp", "angle"] and value != other.parameters[key]: - return False - - return True - - -class _PulseType(type): - """Metaclass to warn at isinstance check.""" - - def __instancecheck__(cls, instance): - cls_alias = getattr(cls, "alias", None) - - # TODO promote this to Deprecation warning in future. - # Once type information usage is removed from user code, - # we will convert pulse classes into functions. - warnings.warn( - "Typechecking with the symbolic pulse subclass will be deprecated. " - f"'{cls_alias}' subclass instance is turned into SymbolicPulse instance. " - f"Use self.pulse_type == '{cls_alias}' instead.", - PendingDeprecationWarning, - ) - - if not isinstance(instance, SymbolicPulse): - return False - return instance.pulse_type == cls_alias - - def __getattr__(cls, item): - # For pylint. A SymbolicPulse subclass must implement several methods - # such as .get_waveform and .validate_parameters. - # In addition, they conventionally offer attribute-like access to the pulse parameters, - # for example, instance.amp returns instance._params["amp"]. - # If pulse classes are directly instantiated, pylint yells no-member - # since the pulse class itself implements nothing. These classes just - # behave like a factory by internally instantiating the SymbolicPulse and return it. - # It is not realistic to write disable=no-member across qiskit packages. - return NotImplemented - - -class Gaussian(metaclass=_PulseType): - r"""A lifted and truncated pulse envelope shaped according to the Gaussian function whose - mean is centered at the center of the pulse (duration / 2): - - .. math:: - - \begin{aligned} - f'(x) &= \exp\Bigl( -\frac12 \frac{{(x - \text{duration}/2)}^2}{\text{sigma}^2} \Bigr)\\ - f(x) &= \text{A} \times \frac{f'(x) - f'(-1)}{1-f'(-1)}, \quad 0 \le x < \text{duration} - \end{aligned} - - where :math:`f'(x)` is the gaussian waveform without lifting or amplitude scaling, and - :math:`\text{A} = \text{amp} \times \exp\left(i\times\text{angle}\right)`. - """ - - alias = "Gaussian" - - def __new__( - cls, - duration: int | ParameterValueType, - amp: ParameterValueType, - sigma: ParameterValueType, - angle: ParameterValueType = 0.0, - name: str | None = None, - limit_amplitude: bool | None = None, - ) -> ScalableSymbolicPulse: - """Create new pulse instance. - - Args: - duration: Pulse length in terms of the sampling period `dt`. - amp: The magnitude of the amplitude of the Gaussian envelope. - sigma: A measure of how wide or narrow the Gaussian peak is; described mathematically - in the class docstring. - angle: The angle of the complex amplitude of the Gaussian envelope. Default value 0. - name: Display name for this pulse envelope. - limit_amplitude: If ``True``, then limit the amplitude of the - waveform to 1. The default is ``True`` and the amplitude is constrained to 1. - - Returns: - ScalableSymbolicPulse instance. - """ - parameters = {"sigma": sigma} - - # Prepare symbolic expressions - _t, _duration, _amp, _sigma, _angle = sym.symbols("t, duration, amp, sigma, angle") - _center = _duration / 2 - - envelope_expr = ( - _amp * sym.exp(sym.I * _angle) * _lifted_gaussian(_t, _center, _duration + 1, _sigma) - ) - - consts_expr = _sigma > 0 - valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 - - return ScalableSymbolicPulse( - pulse_type=cls.alias, - duration=duration, - amp=amp, - angle=angle, - parameters=parameters, - name=name, - limit_amplitude=limit_amplitude, - envelope=envelope_expr, - constraints=consts_expr, - valid_amp_conditions=valid_amp_conditions_expr, - ) - - @deprecate_pulse_func - def __init__(self): - pass - - -class GaussianSquare(metaclass=_PulseType): - """A square pulse with a Gaussian shaped risefall on both sides lifted such that - its first sample is zero. - - Exactly one of the ``risefall_sigma_ratio`` and ``width`` parameters has to be specified. - - If ``risefall_sigma_ratio`` is not None and ``width`` is None: - - .. math:: - - \\begin{aligned} - \\text{risefall} &= \\text{risefall\\_sigma\\_ratio} \\times \\text{sigma}\\\\ - \\text{width} &= \\text{duration} - 2 \\times \\text{risefall} - \\end{aligned} - - If ``width`` is not None and ``risefall_sigma_ratio`` is None: - - .. math:: \\text{risefall} = \\frac{\\text{duration} - \\text{width}}{2} - - In both cases, the lifted gaussian square pulse :math:`f'(x)` is defined as: - - .. math:: - - \\begin{aligned} - f'(x) &= \\begin{cases}\ - \\exp\\biggl(-\\frac12 \\frac{(x - \\text{risefall})^2}{\\text{sigma}^2}\\biggr)\ - & x < \\text{risefall}\\\\ - 1\ - & \\text{risefall} \\le x < \\text{risefall} + \\text{width}\\\\ - \\exp\\biggl(-\\frac12\ - \\frac{{\\bigl(x - (\\text{risefall} + \\text{width})\\bigr)}^2}\ - {\\text{sigma}^2}\ - \\biggr)\ - & \\text{risefall} + \\text{width} \\le x\ - \\end{cases}\\\\ - f(x) &= \\text{A} \\times \\frac{f'(x) - f'(-1)}{1-f'(-1)},\ - \\quad 0 \\le x < \\text{duration} - \\end{aligned} - - where :math:`f'(x)` is the gaussian square waveform without lifting or amplitude scaling, and - :math:`\\text{A} = \\text{amp} \\times \\exp\\left(i\\times\\text{angle}\\right)`. - """ - - alias = "GaussianSquare" - - def __new__( - cls, - duration: int | ParameterValueType, - amp: ParameterValueType, - sigma: ParameterValueType, - width: ParameterValueType | None = None, - angle: ParameterValueType = 0.0, - risefall_sigma_ratio: ParameterValueType | None = None, - name: str | None = None, - limit_amplitude: bool | None = None, - ) -> ScalableSymbolicPulse: - """Create new pulse instance. - - Args: - duration: Pulse length in terms of the sampling period `dt`. - amp: The magnitude of the amplitude of the Gaussian and square pulse. - sigma: A measure of how wide or narrow the Gaussian risefall is; see the class - docstring for more details. - width: The duration of the embedded square pulse. - angle: The angle of the complex amplitude of the pulse. Default value 0. - risefall_sigma_ratio: The ratio of each risefall duration to sigma. - name: Display name for this pulse envelope. - limit_amplitude: If ``True``, then limit the amplitude of the - waveform to 1. The default is ``True`` and the amplitude is constrained to 1. - - Returns: - ScalableSymbolicPulse instance. - - Raises: - PulseError: When width and risefall_sigma_ratio are both empty or both non-empty. - """ - # Convert risefall_sigma_ratio into width which is defined in OpenPulse spec - if width is None and risefall_sigma_ratio is None: - raise PulseError( - "Either the pulse width or the risefall_sigma_ratio parameter must be specified." - ) - if width is not None and risefall_sigma_ratio is not None: - raise PulseError( - "Either the pulse width or the risefall_sigma_ratio parameter can be specified" - " but not both." - ) - if width is None and risefall_sigma_ratio is not None: - width = duration - 2.0 * risefall_sigma_ratio * sigma - - parameters = {"sigma": sigma, "width": width} - - # Prepare symbolic expressions - _t, _duration, _amp, _sigma, _width, _angle = sym.symbols( - "t, duration, amp, sigma, width, angle" - ) - _center = _duration / 2 - - _sq_t0 = _center - _width / 2 - _sq_t1 = _center + _width / 2 - - _gaussian_ledge = _lifted_gaussian(_t, _sq_t0, -1, _sigma) - _gaussian_redge = _lifted_gaussian(_t, _sq_t1, _duration + 1, _sigma) - - envelope_expr = ( - _amp - * sym.exp(sym.I * _angle) - * sym.Piecewise( - (_gaussian_ledge, _t <= _sq_t0), (_gaussian_redge, _t >= _sq_t1), (1, True) - ) - ) - - consts_expr = sym.And(_sigma > 0, _width >= 0, _duration >= _width) - valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 - - return ScalableSymbolicPulse( - pulse_type=cls.alias, - duration=duration, - amp=amp, - angle=angle, - parameters=parameters, - name=name, - limit_amplitude=limit_amplitude, - envelope=envelope_expr, - constraints=consts_expr, - valid_amp_conditions=valid_amp_conditions_expr, - ) - - @deprecate_pulse_func - def __init__(self): - pass - - -@deprecate_pulse_func -def GaussianSquareDrag( - duration: int | ParameterExpression, - amp: float | ParameterExpression, - sigma: float | ParameterExpression, - beta: float | ParameterExpression, - width: float | ParameterExpression | None = None, - angle: float | ParameterExpression | None = 0.0, - risefall_sigma_ratio: float | ParameterExpression | None = None, - name: str | None = None, - limit_amplitude: bool | None = None, -) -> ScalableSymbolicPulse: - """A square pulse with a Drag shaped rise and fall - - This pulse shape is similar to :class:`~.GaussianSquare` but uses - :class:`~.Drag` for its rise and fall instead of :class:`~.Gaussian`. The - addition of the DRAG component of the rise and fall is sometimes helpful in - suppressing the spectral content of the pulse at frequencies near to, but - slightly offset from, the fundamental frequency of the drive. When there is - a spectator qubit close in frequency to the fundamental frequency, - suppressing the drive at the spectator's frequency can help avoid unwanted - excitation of the spectator. - - Exactly one of the ``risefall_sigma_ratio`` and ``width`` parameters has to be specified. - - If ``risefall_sigma_ratio`` is not ``None`` and ``width`` is ``None``: - - .. math:: - - \\begin{aligned} - \\text{risefall} &= \\text{risefall\\_sigma\\_ratio} \\times \\text{sigma}\\\\ - \\text{width} &= \\text{duration} - 2 \\times \\text{risefall} - \\end{aligned} - - If ``width`` is not None and ``risefall_sigma_ratio`` is None: - - .. math:: \\text{risefall} = \\frac{\\text{duration} - \\text{width}}{2} - - Gaussian :math:`g(x, c, σ)` and lifted gaussian :math:`g'(x, c, σ)` curves - can be written as: - - .. math:: - - \\begin{aligned} - g(x, c, σ) &= \\exp\\Bigl(-\\frac12 \\frac{(x - c)^2}{σ^2}\\Bigr)\\\\ - g'(x, c, σ) &= \\frac{g(x, c, σ)-g(-1, c, σ)}{1-g(-1, c, σ)} - \\end{aligned} - - From these, the lifted DRAG curve :math:`d'(x, c, σ, β)` can be written as - - .. math:: - - d'(x, c, σ, β) = g'(x, c, σ) \\times \\Bigl(1 + 1j \\times β \\times\ - \\Bigl(-\\frac{x - c}{σ^2}\\Bigr)\\Bigr) - - The lifted gaussian square drag pulse :math:`f'(x)` is defined as: - - .. math:: - - \\begin{aligned} - f'(x) &= \\begin{cases}\ - \\text{A} \\times d'(x, \\text{risefall}, \\text{sigma}, \\text{beta})\ - & x < \\text{risefall}\\\\ - \\text{A}\ - & \\text{risefall} \\le x < \\text{risefall} + \\text{width}\\\\ - \\text{A} \\times \\times d'(\ - x - (\\text{risefall} + \\text{width}),\ - \\text{risefall},\ - \\text{sigma},\ - \\text{beta}\ - )\ - & \\text{risefall} + \\text{width} \\le x\ - \\end{cases}\\\\ - \\end{aligned} - - where :math:`\\text{A} = \\text{amp} \\times - \\exp\\left(i\\times\\text{angle}\\right)`. - - Args: - duration: Pulse length in terms of the sampling period `dt`. - amp: The amplitude of the DRAG rise and fall and of the square pulse. - sigma: A measure of how wide or narrow the DRAG risefall is; see the class - docstring for more details. - beta: The DRAG correction amplitude. - width: The duration of the embedded square pulse. - angle: The angle in radians of the complex phase factor uniformly - scaling the pulse. Default value 0. - risefall_sigma_ratio: The ratio of each risefall duration to sigma. - name: Display name for this pulse envelope. - limit_amplitude: If ``True``, then limit the amplitude of the - waveform to 1. The default is ``True`` and the amplitude is constrained to 1. - - Returns: - ScalableSymbolicPulse instance. - - Raises: - PulseError: When width and risefall_sigma_ratio are both empty or both non-empty. - """ - # Convert risefall_sigma_ratio into width which is defined in OpenPulse spec - if width is None and risefall_sigma_ratio is None: - raise PulseError( - "Either the pulse width or the risefall_sigma_ratio parameter must be specified." - ) - if width is not None and risefall_sigma_ratio is not None: - raise PulseError( - "Either the pulse width or the risefall_sigma_ratio parameter can be specified" - " but not both." - ) - if width is None and risefall_sigma_ratio is not None: - width = duration - 2.0 * risefall_sigma_ratio * sigma - - parameters = {"sigma": sigma, "width": width, "beta": beta} - - # Prepare symbolic expressions - _t, _duration, _amp, _sigma, _beta, _width, _angle = sym.symbols( - "t, duration, amp, sigma, beta, width, angle" - ) - _center = _duration / 2 - - _sq_t0 = _center - _width / 2 - _sq_t1 = _center + _width / 2 - - _gaussian_ledge = _lifted_gaussian(_t, _sq_t0, -1, _sigma) - _gaussian_redge = _lifted_gaussian(_t, _sq_t1, _duration + 1, _sigma) - _deriv_ledge = -(_t - _sq_t0) / (_sigma**2) * _gaussian_ledge - _deriv_redge = -(_t - _sq_t1) / (_sigma**2) * _gaussian_redge - - envelope_expr = ( - _amp - * sym.exp(sym.I * _angle) - * sym.Piecewise( - (_gaussian_ledge + sym.I * _beta * _deriv_ledge, _t <= _sq_t0), - (_gaussian_redge + sym.I * _beta * _deriv_redge, _t >= _sq_t1), - (1, True), - ) - ) - consts_expr = sym.And(_sigma > 0, _width >= 0, _duration >= _width) - valid_amp_conditions_expr = sym.And(sym.Abs(_amp) <= 1.0, sym.Abs(_beta) < _sigma) - - return ScalableSymbolicPulse( - pulse_type="GaussianSquareDrag", - duration=duration, - amp=amp, - angle=angle, - parameters=parameters, - name=name, - limit_amplitude=limit_amplitude, - envelope=envelope_expr, - constraints=consts_expr, - valid_amp_conditions=valid_amp_conditions_expr, - ) - - -@deprecate_pulse_func -def gaussian_square_echo( - duration: int | ParameterValueType, - amp: float | ParameterExpression, - sigma: float | ParameterExpression, - width: float | ParameterExpression | None = None, - angle: float | ParameterExpression | None = 0.0, - active_amp: float | ParameterExpression | None = 0.0, - active_angle: float | ParameterExpression | None = 0.0, - risefall_sigma_ratio: float | ParameterExpression | None = None, - name: str | None = None, - limit_amplitude: bool | None = None, -) -> SymbolicPulse: - """An echoed Gaussian square pulse with an active tone overlaid on it. - - The Gaussian Square Echo pulse is composed of three pulses. First, a Gaussian Square pulse - :math:`f_{echo}(x)` with amplitude ``amp`` and phase ``angle`` playing for half duration, - followed by a second Gaussian Square pulse :math:`-f_{echo}(x)` with opposite amplitude - and same phase playing for the rest of the duration. Third a Gaussian Square pulse - :math:`f_{active}(x)` with amplitude ``active_amp`` and phase ``active_angle`` - playing for the entire duration. The Gaussian Square Echo pulse :math:`g_e()` - can be written as: - - .. math:: - - \\begin{aligned} - g_e(x) &= \\begin{cases}\ - f_{\\text{active}} + f_{\\text{echo}}(x)\ - & x < \\frac{\\text{duration}}{2}\\\\ - f_{\\text{active}} - f_{\\text{echo}}(x)\ - & \\frac{\\text{duration}}{2} < x\ - \\end{cases}\\\\ - \\end{aligned} - - One case where this pulse can be used is when implementing a direct CNOT gate with - a cross-resonance superconducting qubit architecture. When applying this pulse to - the target qubit, the active portion can be used to cancel IX terms from the - cross-resonance drive while the echo portion can reduce the impact of a static ZZ coupling. - - Exactly one of the ``risefall_sigma_ratio`` and ``width`` parameters has to be specified. - - If ``risefall_sigma_ratio`` is not ``None`` and ``width`` is ``None``: - - .. math:: - - \\begin{aligned} - \\text{risefall} &= \\text{risefall\\_sigma\\_ratio} \\times \\text{sigma}\\\\ - \\text{width} &= \\text{duration} - 2 \\times \\text{risefall} - \\end{aligned} - - If ``width`` is not None and ``risefall_sigma_ratio`` is None: - - .. math:: \\text{risefall} = \\frac{\\text{duration} - \\text{width}}{2} - - References: - 1. |citation1|_ - - .. _citation1: https://iopscience.iop.org/article/10.1088/2058-9565/abe519 - - .. |citation1| replace:: *Jurcevic, P., Javadi-Abhari, A., Bishop, L. S., - Lauer, I., Bogorin, D. F., Brink, M., Capelluto, L., G{\"u}nl{\"u}k, O., - Itoko, T., Kanazawa, N. & others - Demonstration of quantum volume 64 on a superconducting quantum - computing system. (Section V)* - Args: - duration: Pulse length in terms of the sampling period `dt`. - amp: The amplitude of the rise and fall and of the echoed pulse. - sigma: A measure of how wide or narrow the risefall is; see the class - docstring for more details. - width: The duration of the embedded square pulse. - angle: The angle in radians of the complex phase factor uniformly - scaling the echoed pulse. Default value 0. - active_amp: The amplitude of the active pulse. - active_angle: The angle in radian of the complex phase factor uniformly - scaling the active pulse. Default value 0. - risefall_sigma_ratio: The ratio of each risefall duration to sigma. - name: Display name for this pulse envelope. - limit_amplitude: If ``True``, then limit the amplitude of the - waveform to 1. The default is ``True`` and the amplitude is constrained to 1. - - Returns: - ScalableSymbolicPulse instance. - Raises: - PulseError: When width and risefall_sigma_ratio are both empty or both non-empty. - """ - # Convert risefall_sigma_ratio into width which is defined in OpenPulse spec - if width is None and risefall_sigma_ratio is None: - raise PulseError( - "Either the pulse width or the risefall_sigma_ratio parameter must be specified." - ) - if width is not None and risefall_sigma_ratio is not None: - raise PulseError( - "Either the pulse width or the risefall_sigma_ratio parameter can be specified" - " but not both." - ) - - if width is None and risefall_sigma_ratio is not None: - width = duration - 2.0 * risefall_sigma_ratio * sigma - - parameters = { - "amp": amp, - "angle": angle, - "sigma": sigma, - "width": width, - "active_amp": active_amp, - "active_angle": active_angle, - } - - # Prepare symbolic expressions - ( - _t, - _duration, - _amp, - _sigma, - _active_amp, - _width, - _angle, - _active_angle, - ) = sym.symbols("t, duration, amp, sigma, active_amp, width, angle, active_angle") - - # gaussian square echo for rotary tone - _center = _duration / 4 - - _width_echo = (_duration - 2 * (_duration - _width)) / 2 - - _sq_t0 = _center - _width_echo / 2 - _sq_t1 = _center + _width_echo / 2 - - _gaussian_ledge = _lifted_gaussian(_t, _sq_t0, -1, _sigma) - _gaussian_redge = _lifted_gaussian(_t, _sq_t1, _duration / 2 + 1, _sigma) - - envelope_expr_p = ( - _amp - * sym.exp(sym.I * _angle) - * sym.Piecewise( - (_gaussian_ledge, _t <= _sq_t0), - (_gaussian_redge, _t >= _sq_t1), - (1, True), - ) - ) - - _center_echo = _duration / 2 + _duration / 4 - - _sq_t0_echo = _center_echo - _width_echo / 2 - _sq_t1_echo = _center_echo + _width_echo / 2 - - _gaussian_ledge_echo = _lifted_gaussian(_t, _sq_t0_echo, _duration / 2 - 1, _sigma) - _gaussian_redge_echo = _lifted_gaussian(_t, _sq_t1_echo, _duration + 1, _sigma) - - envelope_expr_echo = ( - -1 - * _amp - * sym.exp(sym.I * _angle) - * sym.Piecewise( - (_gaussian_ledge_echo, _t <= _sq_t0_echo), - (_gaussian_redge_echo, _t >= _sq_t1_echo), - (1, True), - ) - ) - - envelope_expr = sym.Piecewise( - (envelope_expr_p, _t <= _duration / 2), (envelope_expr_echo, _t >= _duration / 2), (0, True) - ) - - # gaussian square for active cancellation tone - _center_active = _duration / 2 - - _sq_t0_active = _center_active - _width / 2 - _sq_t1_active = _center_active + _width / 2 - - _gaussian_ledge_active = _lifted_gaussian(_t, _sq_t0_active, -1, _sigma) - _gaussian_redge_active = _lifted_gaussian(_t, _sq_t1_active, _duration + 1, _sigma) - - envelope_expr_active = ( - _active_amp - * sym.exp(sym.I * _active_angle) - * sym.Piecewise( - (_gaussian_ledge_active, _t <= _sq_t0_active), - (_gaussian_redge_active, _t >= _sq_t1_active), - (1, True), - ) - ) - - envelop_expr_total = envelope_expr + envelope_expr_active - - consts_expr = sym.And( - _sigma > 0, _width >= 0, _duration >= _width, _duration / 2 >= _width_echo - ) - - # Check validity of amplitudes - valid_amp_conditions_expr = sym.And(sym.Abs(_amp) + sym.Abs(_active_amp) <= 1.0) - - return SymbolicPulse( - pulse_type="gaussian_square_echo", - duration=duration, - parameters=parameters, - name=name, - limit_amplitude=limit_amplitude, - envelope=envelop_expr_total, - constraints=consts_expr, - valid_amp_conditions=valid_amp_conditions_expr, - ) - - -@deprecate_pulse_func -def GaussianDeriv( - duration: int | ParameterValueType, - amp: float | ParameterExpression, - sigma: float | ParameterExpression, - angle: float | ParameterExpression | None = 0.0, - name: str | None = None, - limit_amplitude: bool | None = None, -) -> ScalableSymbolicPulse: - """An unnormalized Gaussian derivative pulse. - - The Gaussian function is centered around the halfway point of the pulse, - and the envelope of the pulse is given by: - - .. math:: - - f(x) = -\\text{A}\\frac{x-\\mu}{\\text{sigma}^{2}}\\exp - \\left[-\\left(\\frac{x-\\mu}{2\\text{sigma}}\\right)^{2}\\right] , 0 <= x < duration - - where :math:`\\text{A} = \\text{amp} \\times\\exp\\left(i\\times\\text{angle}\\right)`, - and :math:`\\mu=\\text{duration}/2`. - - Args: - duration: Pulse length in terms of the sampling period `dt`. - amp: The magnitude of the amplitude of the pulse - (the value of the corresponding Gaussian at the midpoint `duration`/2). - sigma: A measure of how wide or narrow the corresponding Gaussian peak is in terms of `dt`; - described mathematically in the class docstring. - angle: The angle in radians of the complex phase factor uniformly - scaling the pulse. Default value 0. - name: Display name for this pulse envelope. - limit_amplitude: If ``True``, then limit the amplitude of the - waveform to 1. The default is ``True`` and the amplitude is constrained to 1. - - Returns: - ScalableSymbolicPulse instance. - """ - parameters = {"sigma": sigma} - - # Prepare symbolic expressions - _t, _duration, _amp, _angle, _sigma = sym.symbols("t, duration, amp, angle, sigma") - envelope_expr = ( - -_amp - * sym.exp(sym.I * _angle) - * ((_t - (_duration / 2)) / _sigma**2) - * sym.exp(-(1 / 2) * ((_t - (_duration / 2)) / _sigma) ** 2) - ) - consts_expr = _sigma > 0 - valid_amp_conditions_expr = sym.Abs(_amp / _sigma) <= sym.exp(1 / 2) - - return ScalableSymbolicPulse( - pulse_type="GaussianDeriv", - duration=duration, - amp=amp, - angle=angle, - parameters=parameters, - name=name, - limit_amplitude=limit_amplitude, - envelope=envelope_expr, - constraints=consts_expr, - valid_amp_conditions=valid_amp_conditions_expr, - ) - - -class Drag(metaclass=_PulseType): - """The Derivative Removal by Adiabatic Gate (DRAG) pulse is a standard Gaussian pulse - with an additional Gaussian derivative component and lifting applied. - - It can be calibrated either to reduce the phase error due to virtual population of the - :math:`|2\\rangle` state during the pulse or to reduce the frequency spectrum of a - standard Gaussian pulse near the :math:`|1\\rangle\\leftrightarrow|2\\rangle` transition, - reducing the chance of leakage to the :math:`|2\\rangle` state. - - .. math:: - - \\begin{aligned} - g(x) &= \\exp\\Bigl(-\\frac12 \\frac{(x - \\text{duration}/2)^2}{\\text{sigma}^2}\\Bigr)\\\\ - g'(x) &= \\text{A}\\times\\frac{g(x)-g(-1)}{1-g(-1)}\\\\ - f(x) &= g'(x) \\times \\Bigl(1 + 1j \\times \\text{beta} \\times\ - \\Bigl(-\\frac{x - \\text{duration}/2}{\\text{sigma}^2}\\Bigr) \\Bigr), - \\quad 0 \\le x < \\text{duration} - \\end{aligned} - - where :math:`g(x)` is a standard unlifted Gaussian waveform, :math:`g'(x)` is the lifted - :class:`~qiskit.pulse.library.Gaussian` waveform, and - :math:`\\text{A} = \\text{amp} \\times \\exp\\left(i\\times\\text{angle}\\right)`. - - References: - 1. |citation1|_ - - .. _citation1: https://link.aps.org/doi/10.1103/PhysRevA.83.012308 - - .. |citation1| replace:: *Gambetta, J. M., Motzoi, F., Merkel, S. T. & Wilhelm, F. K. - Analytic control methods for high-fidelity unitary operations - in a weakly nonlinear oscillator. Phys. Rev. A 83, 012308 (2011).* - - 2. |citation2|_ - - .. _citation2: https://link.aps.org/doi/10.1103/PhysRevLett.103.110501 - - .. |citation2| replace:: *F. Motzoi, J. M. Gambetta, P. Rebentrost, and F. K. Wilhelm - Phys. Rev. Lett. 103, 110501 – Published 8 September 2009.* - """ - - alias = "Drag" - - def __new__( - cls, - duration: int | ParameterValueType, - amp: ParameterValueType, - sigma: ParameterValueType, - beta: ParameterValueType, - angle: ParameterValueType = 0.0, - name: str | None = None, - limit_amplitude: bool | None = None, - ) -> ScalableSymbolicPulse: - """Create new pulse instance. - - Args: - duration: Pulse length in terms of the sampling period `dt`. - amp: The magnitude of the amplitude of the DRAG envelope. - sigma: A measure of how wide or narrow the Gaussian peak is; described mathematically - in the class docstring. - beta: The correction amplitude. - angle: The angle of the complex amplitude of the DRAG envelope. Default value 0. - name: Display name for this pulse envelope. - limit_amplitude: If ``True``, then limit the amplitude of the - waveform to 1. The default is ``True`` and the amplitude is constrained to 1. - - Returns: - ScalableSymbolicPulse instance. - """ - parameters = {"sigma": sigma, "beta": beta} - - # Prepare symbolic expressions - _t, _duration, _amp, _sigma, _beta, _angle = sym.symbols( - "t, duration, amp, sigma, beta, angle" - ) - _center = _duration / 2 - - _gauss = _lifted_gaussian(_t, _center, _duration + 1, _sigma) - _deriv = -(_t - _center) / (_sigma**2) * _gauss - - envelope_expr = _amp * sym.exp(sym.I * _angle) * (_gauss + sym.I * _beta * _deriv) - - consts_expr = _sigma > 0 - valid_amp_conditions_expr = sym.And(sym.Abs(_amp) <= 1.0, sym.Abs(_beta) < _sigma) - - return ScalableSymbolicPulse( - pulse_type="Drag", - duration=duration, - amp=amp, - angle=angle, - parameters=parameters, - name=name, - limit_amplitude=limit_amplitude, - envelope=envelope_expr, - constraints=consts_expr, - valid_amp_conditions=valid_amp_conditions_expr, - ) - - @deprecate_pulse_func - def __init__(self): - pass - - -class Constant(metaclass=_PulseType): - """A simple constant pulse, with an amplitude value and a duration: - - .. math:: - - f(x) = \\text{amp}\\times\\exp\\left(i\\text{angle}\\right) , 0 <= x < duration - f(x) = 0 , elsewhere - """ - - alias = "Constant" - - def __new__( - cls, - duration: int | ParameterValueType, - amp: ParameterValueType, - angle: ParameterValueType = 0.0, - name: str | None = None, - limit_amplitude: bool | None = None, - ) -> ScalableSymbolicPulse: - """Create new pulse instance. - - Args: - duration: Pulse length in terms of the sampling period `dt`. - amp: The magnitude of the amplitude of the square envelope. - angle: The angle of the complex amplitude of the square envelope. Default value 0. - name: Display name for this pulse envelope. - limit_amplitude: If ``True``, then limit the amplitude of the - waveform to 1. The default is ``True`` and the amplitude is constrained to 1. - - Returns: - ScalableSymbolicPulse instance. - """ - # Prepare symbolic expressions - _t, _amp, _duration, _angle = sym.symbols("t, amp, duration, angle") - - # Note this is implemented using Piecewise instead of just returning amp - # directly because otherwise the expression has no t dependence and sympy's - # lambdify will produce a function f that for an array t returns amp - # instead of amp * np.ones(t.shape). - # - # See: https://github.com/sympy/sympy/issues/5642 - envelope_expr = ( - _amp - * sym.exp(sym.I * _angle) - * sym.Piecewise((1, sym.And(_t >= 0, _t <= _duration)), (0, True)) - ) - - valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 - - return ScalableSymbolicPulse( - pulse_type="Constant", - duration=duration, - amp=amp, - angle=angle, - name=name, - limit_amplitude=limit_amplitude, - envelope=envelope_expr, - valid_amp_conditions=valid_amp_conditions_expr, - ) - - @deprecate_pulse_func - def __init__(self): - pass - - -@deprecate_pulse_func -def Sin( - duration: int | ParameterExpression, - amp: float | ParameterExpression, - phase: float | ParameterExpression, - freq: float | ParameterExpression | None = None, - angle: float | ParameterExpression | None = 0.0, - name: str | None = None, - limit_amplitude: bool | None = None, -) -> ScalableSymbolicPulse: - """A sinusoidal pulse. - - The envelope of the pulse is given by: - - .. math:: - - f(x) = \\text{A}\\sin\\left(2\\pi\\text{freq}x+\\text{phase}\\right) , 0 <= x < duration - - where :math:`\\text{A} = \\text{amp} \\times\\exp\\left(i\\times\\text{angle}\\right)`. - - Args: - duration: Pulse length in terms of the sampling period `dt`. - amp: The magnitude of the amplitude of the sinusoidal wave. Wave range is [-`amp`,`amp`]. - phase: The phase of the sinusoidal wave (note that this is not equivalent to the angle of - the complex amplitude) - freq: The frequency of the sinusoidal wave, in terms of 1 over sampling period. - If not provided defaults to a single cycle (i.e :math:'\\frac{1}{\\text{duration}}'). - The frequency is limited to the range :math:`\\left(0,0.5\\right]` (the Nyquist frequency). - angle: The angle in radians of the complex phase factor uniformly - scaling the pulse. Default value 0. - name: Display name for this pulse envelope. - limit_amplitude: If ``True``, then limit the amplitude of the - waveform to 1. The default is ``True`` and the amplitude is constrained to 1. - - Returns: - ScalableSymbolicPulse instance. - """ - if freq is None: - freq = 1 / duration - parameters = {"freq": freq, "phase": phase} - - # Prepare symbolic expressions - _t, _duration, _amp, _angle, _freq, _phase = sym.symbols("t, duration, amp, angle, freq, phase") - - envelope_expr = _amp * sym.exp(sym.I * _angle) * sym.sin(2 * sym.pi * _freq * _t + _phase) - - consts_expr = sym.And(_freq > 0, _freq < 0.5) - - # This might fail for waves shorter than a single cycle - valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 - - return ScalableSymbolicPulse( - pulse_type="Sin", - duration=duration, - amp=amp, - angle=angle, - parameters=parameters, - name=name, - limit_amplitude=limit_amplitude, - envelope=envelope_expr, - constraints=consts_expr, - valid_amp_conditions=valid_amp_conditions_expr, - ) - - -@deprecate_pulse_func -def Cos( - duration: int | ParameterExpression, - amp: float | ParameterExpression, - phase: float | ParameterExpression, - freq: float | ParameterExpression | None = None, - angle: float | ParameterExpression | None = 0.0, - name: str | None = None, - limit_amplitude: bool | None = None, -) -> ScalableSymbolicPulse: - """A cosine pulse. - - The envelope of the pulse is given by: - - .. math:: - - f(x) = \\text{A}\\cos\\left(2\\pi\\text{freq}x+\\text{phase}\\right) , 0 <= x < duration - - where :math:`\\text{A} = \\text{amp} \\times\\exp\\left(i\\times\\text{angle}\\right)`. - - Args: - duration: Pulse length in terms of the sampling period `dt`. - amp: The magnitude of the amplitude of the cosine wave. Wave range is [-`amp`,`amp`]. - phase: The phase of the cosine wave (note that this is not equivalent to the angle - of the complex amplitude). - freq: The frequency of the cosine wave, in terms of 1 over sampling period. - If not provided defaults to a single cycle (i.e :math:'\\frac{1}{\\text{duration}}'). - The frequency is limited to the range :math:`\\left(0,0.5\\right]` (the Nyquist frequency). - angle: The angle in radians of the complex phase factor uniformly - scaling the pulse. Default value 0. - name: Display name for this pulse envelope. - limit_amplitude: If ``True``, then limit the amplitude of the - waveform to 1. The default is ``True`` and the amplitude is constrained to 1. - - Returns: - ScalableSymbolicPulse instance. - """ - if freq is None: - freq = 1 / duration - parameters = {"freq": freq, "phase": phase} - - # Prepare symbolic expressions - _t, _duration, _amp, _angle, _freq, _phase = sym.symbols("t, duration, amp, angle, freq, phase") - - envelope_expr = _amp * sym.exp(sym.I * _angle) * sym.cos(2 * sym.pi * _freq * _t + _phase) - - consts_expr = sym.And(_freq > 0, _freq < 0.5) - - # This might fail for waves shorter than a single cycle - valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 - - return ScalableSymbolicPulse( - pulse_type="Cos", - duration=duration, - amp=amp, - angle=angle, - parameters=parameters, - name=name, - limit_amplitude=limit_amplitude, - envelope=envelope_expr, - constraints=consts_expr, - valid_amp_conditions=valid_amp_conditions_expr, - ) - - -@deprecate_pulse_func -def Sawtooth( - duration: int | ParameterExpression, - amp: float | ParameterExpression, - phase: float | ParameterExpression, - freq: float | ParameterExpression | None = None, - angle: float | ParameterExpression | None = 0.0, - name: str | None = None, - limit_amplitude: bool | None = None, -) -> ScalableSymbolicPulse: - """A sawtooth pulse. - - The envelope of the pulse is given by: - - .. math:: - - f(x) = 2\\text{A}\\left[g\\left(x\\right)- - \\lfloor g\\left(x\\right)+\\frac{1}{2}\\rfloor\\right] - - where :math:`\\text{A} = \\text{amp} \\times\\exp\\left(i\\times\\text{angle}\\right)`, - :math:`g\\left(x\\right)=x\\times\\text{freq}+\\frac{\\text{phase}}{2\\pi}`, - and :math:`\\lfloor ...\\rfloor` is the floor operation. - - Args: - duration: Pulse length in terms of the sampling period `dt`. - amp: The magnitude of the amplitude of the sawtooth wave. Wave range is [-`amp`,`amp`]. - phase: The phase of the sawtooth wave (note that this is not equivalent to the angle - of the complex amplitude) - freq: The frequency of the sawtooth wave, in terms of 1 over sampling period. - If not provided defaults to a single cycle (i.e :math:'\\frac{1}{\\text{duration}}'). - The frequency is limited to the range :math:`\\left(0,0.5\\right]` (the Nyquist frequency). - angle: The angle in radians of the complex phase factor uniformly - scaling the pulse. Default value 0. - name: Display name for this pulse envelope. - limit_amplitude: If ``True``, then limit the amplitude of the - waveform to 1. The default is ``True`` and the amplitude is constrained to 1. - - Returns: - ScalableSymbolicPulse instance. - """ - if freq is None: - freq = 1 / duration - parameters = {"freq": freq, "phase": phase} - - # Prepare symbolic expressions - _t, _duration, _amp, _angle, _freq, _phase = sym.symbols("t, duration, amp, angle, freq, phase") - lin_expr = _t * _freq + _phase / (2 * sym.pi) - - envelope_expr = 2 * _amp * sym.exp(sym.I * _angle) * (lin_expr - sym.floor(lin_expr + 1 / 2)) - - consts_expr = sym.And(_freq > 0, _freq < 0.5) - - # This might fail for waves shorter than a single cycle - valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 - - return ScalableSymbolicPulse( - pulse_type="Sawtooth", - duration=duration, - amp=amp, - angle=angle, - parameters=parameters, - name=name, - limit_amplitude=limit_amplitude, - envelope=envelope_expr, - constraints=consts_expr, - valid_amp_conditions=valid_amp_conditions_expr, - ) - - -@deprecate_pulse_func -def Triangle( - duration: int | ParameterExpression, - amp: float | ParameterExpression, - phase: float | ParameterExpression, - freq: float | ParameterExpression | None = None, - angle: float | ParameterExpression | None = 0.0, - name: str | None = None, - limit_amplitude: bool | None = None, -) -> ScalableSymbolicPulse: - """A triangle wave pulse. - - The envelope of the pulse is given by: - - .. math:: - - f(x) = \\text{A}\\left[\\text{sawtooth}\\left(x\\right)\\right] , 0 <= x < duration - - where :math:`\\text{A} = \\text{amp} \\times\\exp\\left(i\\times\\text{angle}\\right)`, - and :math:`\\text{sawtooth}\\left(x\\right)` is a sawtooth wave with the same frequency - as the triangle wave, but a phase shifted by :math:`\\frac{\\pi}{2}`. - - Args: - duration: Pulse length in terms of the sampling period `dt`. - amp: The magnitude of the amplitude of the triangle wave. Wave range is [-`amp`,`amp`]. - phase: The phase of the triangle wave (note that this is not equivalent to the angle - of the complex amplitude) - freq: The frequency of the triangle wave, in terms of 1 over sampling period. - If not provided defaults to a single cycle (i.e :math:'\\frac{1}{\\text{duration}}'). - The frequency is limited to the range :math:`\\left(0,0.5\\right]` (the Nyquist frequency). - angle: The angle in radians of the complex phase factor uniformly - scaling the pulse. Default value 0. - name: Display name for this pulse envelope. - limit_amplitude: If ``True``, then limit the amplitude of the - waveform to 1. The default is ``True`` and the amplitude is constrained to 1. - - Returns: - ScalableSymbolicPulse instance. - """ - if freq is None: - freq = 1 / duration - parameters = {"freq": freq, "phase": phase} - - # Prepare symbolic expressions - _t, _duration, _amp, _angle, _freq, _phase = sym.symbols("t, duration, amp, angle, freq, phase") - lin_expr = _t * _freq + _phase / (2 * sym.pi) - 0.25 - sawtooth_expr = 2 * (lin_expr - sym.floor(lin_expr + 1 / 2)) - - envelope_expr = _amp * sym.exp(sym.I * _angle) * (-2 * sym.Abs(sawtooth_expr) + 1) - - consts_expr = sym.And(_freq > 0, _freq < 0.5) - - # This might fail for waves shorter than a single cycle - valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 - - return ScalableSymbolicPulse( - pulse_type="Triangle", - duration=duration, - amp=amp, - angle=angle, - parameters=parameters, - name=name, - limit_amplitude=limit_amplitude, - envelope=envelope_expr, - constraints=consts_expr, - valid_amp_conditions=valid_amp_conditions_expr, - ) - - -@deprecate_pulse_func -def Square( - duration: int | ParameterValueType, - amp: float | ParameterExpression, - phase: float | ParameterExpression, - freq: float | ParameterExpression | None = None, - angle: float | ParameterExpression | None = 0.0, - name: str | None = None, - limit_amplitude: bool | None = None, -) -> ScalableSymbolicPulse: - """A square wave pulse. - - The envelope of the pulse is given by: - - .. math:: - - f(x) = \\text{A}\\text{sign}\\left[\\sin - \\left(2\\pi x\\times\\text{freq}+\\text{phase}\\right)\\right] , 0 <= x < duration - - where :math:`\\text{A} = \\text{amp} \\times\\exp\\left(i\\times\\text{angle}\\right)`, - and :math:`\\text{sign}` - is the sign function with the convention :math:`\\text{sign}\\left(0\\right)=1`. - - Args: - duration: Pulse length in terms of the sampling period ``dt``. - amp: The magnitude of the amplitude of the square wave. Wave range is - :math:`\\left[-\\texttt{amp},\\texttt{amp}\\right]`. - phase: The phase of the square wave (note that this is not equivalent to the angle of - the complex amplitude). - freq: The frequency of the square wave, in terms of 1 over sampling period. - If not provided defaults to a single cycle (i.e :math:`\\frac{1}{\\text{duration}}`). - The frequency is limited to the range :math:`\\left(0,0.5\\right]` (the Nyquist frequency). - angle: The angle in radians of the complex phase factor uniformly - scaling the pulse. Default value 0. - name: Display name for this pulse envelope. - limit_amplitude: If ``True``, then limit the amplitude of the - waveform to 1. The default is ``True`` and the amplitude is constrained to 1. - - Returns: - ScalableSymbolicPulse instance. - """ - if freq is None: - freq = 1 / duration - parameters = {"freq": freq, "phase": phase} - - # Prepare symbolic expressions - _t, _duration, _amp, _angle, _freq, _phase = sym.symbols("t, duration, amp, angle, freq, phase") - _x = _freq * _t + _phase / (2 * sym.pi) - - envelope_expr = ( - _amp * sym.exp(sym.I * _angle) * (2 * (2 * sym.floor(_x) - sym.floor(2 * _x)) + 1) - ) - - consts_expr = sym.And(_freq > 0, _freq < 0.5) - - # This might fail for waves shorter than a single cycle - valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 - - return ScalableSymbolicPulse( - pulse_type="Square", - duration=duration, - amp=amp, - angle=angle, - parameters=parameters, - name=name, - limit_amplitude=limit_amplitude, - envelope=envelope_expr, - constraints=consts_expr, - valid_amp_conditions=valid_amp_conditions_expr, - ) - - -@deprecate_pulse_func -def Sech( - duration: int | ParameterValueType, - amp: float | ParameterExpression, - sigma: float | ParameterExpression, - angle: float | ParameterExpression | None = 0.0, - name: str | None = None, - zero_ends: bool | None = True, - limit_amplitude: bool | None = None, -) -> ScalableSymbolicPulse: - """An unnormalized sech pulse. - - The sech function is centered around the halfway point of the pulse, - and the envelope of the pulse is given by: - - .. math:: - - f(x) = \\text{A}\\text{sech}\\left( - \\frac{x-\\mu}{\\text{sigma}}\\right) , 0 <= x < duration - - where :math:`\\text{A} = \\text{amp} \\times\\exp\\left(i\\times\\text{angle}\\right)`, - and :math:`\\mu=\\text{duration}/2`. - - If `zero_ends` is set to `True`, the output `y` is modified: - .. math:: - - y\\left(x\\right) \\mapsto \\text{A}\\frac{y-y^{*}}{\\text{A}-y^{*}}, - - where :math:`y^{*}` is the value of :math:`y` at the endpoints (at :math:`x=-1 - and :math:`x=\\text{duration}+1`). This shifts the endpoints value to zero, while also - rescaling to preserve the amplitude at `:math:`\\text{duration}/2``. - - Args: - duration: Pulse length in terms of the sampling period `dt`. - amp: The magnitude of the amplitude of the pulse (the value at the midpoint `duration`/2). - sigma: A measure of how wide or narrow the sech peak is in terms of `dt`; - described mathematically in the class docstring. - angle: The angle in radians of the complex phase factor uniformly - scaling the pulse. Default value 0. - name: Display name for this pulse envelope. - zero_ends: If True, zeros the ends at x = -1, x = `duration` + 1, - but rescales to preserve `amp`. Default value True. - limit_amplitude: If ``True``, then limit the amplitude of the - waveform to 1. The default is ``True`` and the amplitude is constrained to 1. - - Returns: - ScalableSymbolicPulse instance. - """ - parameters = {"sigma": sigma} - - # Prepare symbolic expressions - _t, _duration, _amp, _angle, _sigma = sym.symbols("t, duration, amp, angle, sigma") - complex_amp = _amp * sym.exp(sym.I * _angle) - envelope_expr = complex_amp * sym.sech((_t - (_duration / 2)) / _sigma) - - if zero_ends: - shift_val = complex_amp * sym.sech((-1 - (_duration / 2)) / _sigma) - envelope_expr = complex_amp * (envelope_expr - shift_val) / (complex_amp - shift_val) - - consts_expr = _sigma > 0 - - valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 - - return ScalableSymbolicPulse( - pulse_type="Sech", - duration=duration, - amp=amp, - angle=angle, - parameters=parameters, - name=name, - limit_amplitude=limit_amplitude, - envelope=envelope_expr, - constraints=consts_expr, - valid_amp_conditions=valid_amp_conditions_expr, - ) - - -@deprecate_pulse_func -def SechDeriv( - duration: int | ParameterValueType, - amp: float | ParameterExpression, - sigma: float | ParameterExpression, - angle: float | ParameterExpression | None = 0.0, - name: str | None = None, - limit_amplitude: bool | None = None, -) -> ScalableSymbolicPulse: - """An unnormalized sech derivative pulse. - - The sech function is centered around the halfway point of the pulse, and the envelope of the - pulse is given by: - - .. math:: - - f(x) = \\text{A}\\frac{d}{dx}\\left[\\text{sech} - \\left(\\frac{x-\\mu}{\\text{sigma}}\\right)\\right] , 0 <= x < duration - - where :math:`\\text{A} = \\text{amp} \\times\\exp\\left(i\\times\\text{angle}\\right)`, - :math:`\\mu=\\text{duration}/2`, and :math:`d/dx` is a derivative with respect to `x`. - - Args: - duration: Pulse length in terms of the sampling period `dt`. - amp: The magnitude of the amplitude of the pulse (the value of the corresponding sech - function at the midpoint `duration`/2). - sigma: A measure of how wide or narrow the corresponding sech peak is, in terms of `dt`; - described mathematically in the class docstring. - angle: The angle in radians of the complex phase factor uniformly - scaling the pulse. Default value 0. - name: Display name for this pulse envelope. - limit_amplitude: If ``True``, then limit the amplitude of the - waveform to 1. The default is ``True`` and the amplitude is constrained to 1. - - Returns: - ScalableSymbolicPulse instance. - """ - parameters = {"sigma": sigma} - - # Prepare symbolic expressions - _t, _duration, _amp, _angle, _sigma = sym.symbols("t, duration, amp, angle, sigma") - time_argument = (_t - (_duration / 2)) / _sigma - sech_deriv = -sym.tanh(time_argument) * sym.sech(time_argument) / _sigma - - envelope_expr = _amp * sym.exp(sym.I * _angle) * sech_deriv - - consts_expr = _sigma > 0 - - valid_amp_conditions_expr = sym.Abs(_amp) / _sigma <= 2.0 - - return ScalableSymbolicPulse( - pulse_type="SechDeriv", - duration=duration, - amp=amp, - angle=angle, - parameters=parameters, - name=name, - limit_amplitude=limit_amplitude, - envelope=envelope_expr, - constraints=consts_expr, - valid_amp_conditions=valid_amp_conditions_expr, - ) diff --git a/qiskit/pulse/library/waveform.py b/qiskit/pulse/library/waveform.py deleted file mode 100644 index 0a31ac11a8d8..000000000000 --- a/qiskit/pulse/library/waveform.py +++ /dev/null @@ -1,136 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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 pulse that is described by complex-valued sample points.""" -from __future__ import annotations -from typing import Any - -import numpy as np - -from qiskit.pulse.exceptions import PulseError -from qiskit.pulse.library.pulse import Pulse -from qiskit.utils.deprecate_pulse import deprecate_pulse_func - - -class Waveform(Pulse): - """A pulse specified completely by complex-valued samples; each sample is played for the - duration of the backend cycle-time, dt. - """ - - @deprecate_pulse_func - def __init__( - self, - samples: np.ndarray | list[complex], - name: str | None = None, - epsilon: float = 1e-7, - limit_amplitude: bool | None = None, - ): - """Create new sample pulse command. - - Args: - samples: Complex array of the samples in the pulse envelope. - name: Unique name to identify the pulse. - epsilon: Pulse sample norm tolerance for clipping. - If any sample's norm exceeds unity by less than or equal to epsilon - it will be clipped to unit norm. If the sample - norm is greater than 1+epsilon an error will be raised. - limit_amplitude: Passed to parent Pulse - """ - - super().__init__(duration=len(samples), name=name, limit_amplitude=limit_amplitude) - samples = np.asarray(samples, dtype=np.complex128) - self.epsilon = epsilon - self._samples = self._clip(samples, epsilon=epsilon) - - @property - def samples(self) -> np.ndarray: - """Return sample values.""" - return self._samples - - def _clip(self, samples: np.ndarray, epsilon: float = 1e-7) -> np.ndarray: - """If samples are within epsilon of unit norm, clip sample by reducing norm by (1-epsilon). - - If difference is greater than epsilon error is raised. - - Args: - samples: Complex array of the samples in the pulse envelope. - epsilon: Pulse sample norm tolerance for clipping. - If any sample's norm exceeds unity by less than or equal to epsilon - it will be clipped to unit norm. If the sample - norm is greater than 1+epsilon an error will be raised. - - Returns: - Clipped pulse samples. - - Raises: - PulseError: If there exists a pulse sample with a norm greater than 1+epsilon. - """ - samples_norm = np.abs(samples) - to_clip = (samples_norm > 1.0) & (samples_norm <= 1.0 + epsilon) - - if np.any(to_clip): - # first try normalizing by the abs value - clip_where = np.argwhere(to_clip) - clip_angle = np.angle(samples[clip_where]) - clipped_samples = np.exp(1j * clip_angle, dtype=np.complex128) - - # if norm still exceed one subtract epsilon - # required for some platforms - clipped_sample_norms = np.abs(clipped_samples) - to_clip_epsilon = clipped_sample_norms > 1.0 - if np.any(to_clip_epsilon): - clip_where_epsilon = np.argwhere(to_clip_epsilon) - clipped_samples_epsilon = (1 - epsilon) * np.exp( - 1j * clip_angle[clip_where_epsilon], dtype=np.complex128 - ) - clipped_samples[clip_where_epsilon] = clipped_samples_epsilon - - # update samples with clipped values - samples[clip_where] = clipped_samples - samples_norm[clip_where] = np.abs(clipped_samples) - - if np.any(samples_norm > 1.0) and self._limit_amplitude: - amp = np.max(samples_norm) - raise PulseError( - f"Pulse contains sample with norm {amp} greater than 1+epsilon." - " This can be overruled by setting Pulse.limit_amplitude." - ) - - return samples - - def is_parameterized(self) -> bool: - """Return True iff the instruction is parameterized.""" - return False - - @property - def parameters(self) -> dict[str, Any]: - """Return a dictionary containing the pulse's parameters.""" - return {} - - def __eq__(self, other: object) -> bool: - if not isinstance(other, Waveform): - return NotImplemented - return ( - super().__eq__(other) - and self.samples.shape == other.samples.shape - and np.allclose(self.samples, other.samples, rtol=0, atol=self.epsilon) - ) - - def __hash__(self) -> int: - return hash(self.samples.tobytes()) - - def __repr__(self) -> str: - opt = np.get_printoptions() - np.set_printoptions(threshold=50) - np.set_printoptions(**opt) - name_repr = f", name='{self.name}'" if self.name is not None else "" - return f"{self.__class__.__name__}({repr(self.samples)}{name_repr})" diff --git a/qiskit/pulse/macros.py b/qiskit/pulse/macros.py deleted file mode 100644 index 2d73c27e54b7..000000000000 --- a/qiskit/pulse/macros.py +++ /dev/null @@ -1,260 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Module for common pulse programming macros.""" -from __future__ import annotations - -from collections.abc import Sequence -from typing import TYPE_CHECKING - -from qiskit.pulse import channels, exceptions, instructions, utils -from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap -from qiskit.pulse.schedule import Schedule -from qiskit.providers.backend import BackendV2 - - -if TYPE_CHECKING: - from qiskit.transpiler import Target - - -def measure( - qubits: Sequence[int], - backend=None, - inst_map: InstructionScheduleMap | None = None, - meas_map: list[list[int]] | dict[int, list[int]] | None = None, - qubit_mem_slots: dict[int, int] | None = None, - measure_name: str = "measure", -) -> Schedule: - """Return a schedule which measures the requested qubits according to the given - instruction mapping and measure map, or by using the defaults provided by the backend. - - .. note:: - This function internally dispatches schedule generation logic depending on input backend model. - For the :class:`.BackendV2`, it assembles calibrations of single qubit measurement - defined in the backend target to build a composite measurement schedule for `qubits`. - - By default, the measurement results for each qubit are trivially mapped to the qubit - index. This behavior is overridden by qubit_mem_slots. For instance, to measure - qubit 0 into MemorySlot(1), qubit_mem_slots can be provided as {0: 1}. - - Args: - qubits: List of qubits to be measured. - backend (Union[Backend, BaseBackend]): A backend instance, which contains - hardware-specific data required for scheduling. - inst_map: Mapping of circuit operations to pulse schedules. If None, defaults to the - ``instruction_schedule_map`` of ``backend``. - meas_map: List of sets of qubits that must be measured together. If None, defaults to - the ``meas_map`` of ``backend``. - qubit_mem_slots: Mapping of measured qubit index to classical bit index. - measure_name: Name of the measurement schedule. - - Returns: - A measurement schedule corresponding to the inputs provided. - """ - - # backend is V2. - if isinstance(backend, BackendV2): - - return _measure_v2( - qubits=qubits, - target=backend.target, - meas_map=meas_map or backend.meas_map, - qubit_mem_slots=qubit_mem_slots or dict(zip(qubits, range(len(qubits)))), - measure_name=measure_name, - ) - # backend is V1 or backend is None. - else: - try: - return _measure_v1( - qubits=qubits, - inst_map=inst_map or backend.defaults().instruction_schedule_map, - meas_map=meas_map or backend.configuration().meas_map, - qubit_mem_slots=qubit_mem_slots, - measure_name=measure_name, - ) - except AttributeError as ex: - raise exceptions.PulseError( - "inst_map or meas_map, and backend cannot be None simultaneously" - ) from ex - - -def _measure_v1( - qubits: Sequence[int], - inst_map: InstructionScheduleMap, - meas_map: list[list[int]] | dict[int, list[int]], - qubit_mem_slots: dict[int, int] | None = None, - measure_name: str = "measure", -) -> Schedule: - """Return a schedule which measures the requested qubits according to the given - instruction mapping and measure map, or by using the defaults provided by the backendV1. - - Args: - qubits: List of qubits to be measured. - backend (Union[Backend, BaseBackend]): A backend instance, which contains - hardware-specific data required for scheduling. - inst_map: Mapping of circuit operations to pulse schedules. If None, defaults to the - ``instruction_schedule_map`` of ``backend``. - meas_map: List of sets of qubits that must be measured together. If None, defaults to - the ``meas_map`` of ``backend``. - qubit_mem_slots: Mapping of measured qubit index to classical bit index. - measure_name: Name of the measurement schedule. - Returns: - A measurement schedule corresponding to the inputs provided. - Raises: - PulseError: If both ``inst_map`` or ``meas_map``, and ``backend`` is None. - """ - - schedule = Schedule(name=f"Default measurement schedule for qubits {qubits}") - - if isinstance(meas_map, list): - meas_map = utils.format_meas_map(meas_map) - - measure_groups = set() - for qubit in qubits: - measure_groups.add(tuple(meas_map[qubit])) - for measure_group_qubits in measure_groups: - - unused_mem_slots = ( - set() - if qubit_mem_slots is None - else set(measure_group_qubits) - set(qubit_mem_slots.values()) - ) - - try: - default_sched = inst_map.get(measure_name, measure_group_qubits) - except exceptions.PulseError as ex: - raise exceptions.PulseError( - f"We could not find a default measurement schedule called '{measure_name}'. " - "Please provide another name using the 'measure_name' keyword " - "argument. For assistance, the instructions which are defined are: " - f"{inst_map.instructions}" - ) from ex - for time, inst in default_sched.instructions: - if inst.channel.index not in qubits: - continue - if qubit_mem_slots and isinstance(inst, instructions.Acquire): - if inst.channel.index in qubit_mem_slots: - mem_slot = channels.MemorySlot(qubit_mem_slots[inst.channel.index]) - else: - mem_slot = channels.MemorySlot(unused_mem_slots.pop()) - inst = instructions.Acquire(inst.duration, inst.channel, mem_slot=mem_slot) - # Measurement pulses should only be added if its qubit was measured by the user - schedule = schedule.insert(time, inst) - - return schedule - - -def _measure_v2( - qubits: Sequence[int], - target: Target, - meas_map: list[list[int]] | dict[int, list[int]], - qubit_mem_slots: dict[int, int], - measure_name: str = "measure", -) -> Schedule: - """Return a schedule which measures the requested qubits according to the given - target and measure map, or by using the defaults provided by the backendV2. - - Args: - qubits: List of qubits to be measured. - target: The :class:`~.Target` representing the target backend. - meas_map: List of sets of qubits that must be measured together. - qubit_mem_slots: Mapping of measured qubit index to classical bit index. - measure_name: Name of the measurement schedule. - - Returns: - A measurement schedule corresponding to the inputs provided. - """ - schedule = Schedule(name=f"Default measurement schedule for qubits {qubits}") - - if isinstance(meas_map, list): - meas_map = utils.format_meas_map(meas_map) - meas_group = set() - for qubit in qubits: - meas_group |= set(meas_map[qubit]) - meas_group = sorted(meas_group) - - meas_group_set = set(range(max(meas_group) + 1)) - unassigned_qubit_indices = sorted(set(meas_group) - qubit_mem_slots.keys()) - unassigned_reg_indices = sorted(meas_group_set - set(qubit_mem_slots.values()), reverse=True) - if set(qubit_mem_slots.values()).issubset(meas_group_set): - for qubit in unassigned_qubit_indices: - qubit_mem_slots[qubit] = unassigned_reg_indices.pop() - - for measure_qubit in meas_group: - try: - if measure_qubit in qubits: - default_sched = target._get_calibration(measure_name, (measure_qubit,)).filter( - channels=[ - channels.MeasureChannel(measure_qubit), - channels.AcquireChannel(measure_qubit), - ] - ) - schedule += _schedule_remapping_memory_slot(default_sched, qubit_mem_slots) - except KeyError as ex: - raise exceptions.PulseError( - f"We could not find a default measurement schedule called '{measure_name}'. " - "Please provide another name using the 'measure_name' keyword " - "argument. For assistance, the instructions which are defined are: " - f"{target.instructions}" - ) from ex - return schedule - - -def measure_all(backend) -> Schedule: - """ - Return a Schedule which measures all qubits of the given backend. - - Args: - backend (Union[Backend, BaseBackend]): A backend instance, which contains - hardware-specific data required for scheduling. - - Returns: - A schedule corresponding to the inputs provided. - """ - # backend is V2. - if isinstance(backend, BackendV2): - qubits = list(range(backend.num_qubits)) - else: - qubits = list(range(backend.configuration().n_qubits)) - return measure(qubits=qubits, backend=backend) - - -def _schedule_remapping_memory_slot( - schedule: Schedule, qubit_mem_slots: dict[int, int] -) -> Schedule: - """ - A helper function to overwrite MemorySlot index of :class:`.Acquire` instruction. - - Args: - schedule: A measurement schedule. - qubit_mem_slots: Mapping of measured qubit index to classical bit index. - - Returns: - A measurement schedule with new memory slot index. - """ - new_schedule = Schedule() - for t0, inst in schedule.instructions: - if isinstance(inst, instructions.Acquire): - qubit_index = inst.channel.index - reg_index = qubit_mem_slots.get(qubit_index, qubit_index) - new_schedule.insert( - t0, - instructions.Acquire( - inst.duration, - channels.AcquireChannel(qubit_index), - mem_slot=channels.MemorySlot(reg_index), - ), - inplace=True, - ) - else: - new_schedule.insert(t0, inst, inplace=True) - return new_schedule diff --git a/qiskit/pulse/parameter_manager.py b/qiskit/pulse/parameter_manager.py deleted file mode 100644 index e5a4a1a1d2bd..000000000000 --- a/qiskit/pulse/parameter_manager.py +++ /dev/null @@ -1,445 +0,0 @@ -# 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. - -# pylint: disable=invalid-name - -""""Management of pulse program parameters. - -Background -========== - -In contrast to ``QuantumCircuit``, in pulse programs, parameter objects can be stored in -multiple places at different layers, for example - -- program variables: ``ScheduleBlock.alignment_context._context_params`` - -- instruction operands: ``ShiftPhase.phase``, ... - -- operand parameters: ``pulse.parameters``, ``channel.index`` ... - -This complexity is due to the tight coupling of the program to an underlying device Hamiltonian, -i.e. the variance of physical parameters between qubits and their couplings. -If we want to define a program that can be used with arbitrary qubits, -we should be able to parametrize every control parameter in the program. - -Implementation -============== - -Managing parameters in each object within a program, i.e. the ``ParameterTable`` model, -makes the framework quite complicated. With the ``ParameterManager`` class within this module, -the parameter assignment operation is performed by a visitor instance. - -The visitor pattern is a way of separating data processing from the object on which it operates. -This removes the overhead of parameter management from each piece of the program. -The computational complexity of the parameter assignment operation may be increased -from the parameter table model of ~O(1), however, usually, this calculation occurs -only once before the program is executed. Thus this doesn't hurt user experience during -pulse programming. On the contrary, it removes parameter table object and associated logic -from each object, yielding smaller object creation cost and higher performance -as the data amount scales. - -Note that we don't need to write any parameter management logic for each object, -and thus this parameter framework gives greater scalability to the pulse module. -""" -from __future__ import annotations -from copy import copy -from typing import Any, Mapping, Sequence - -from qiskit.circuit.parametervector import ParameterVector, ParameterVectorElement -from qiskit.circuit.parameter import Parameter -from qiskit.circuit.parameterexpression import ParameterExpression, ParameterValueType -from qiskit.pulse import instructions, channels -from qiskit.pulse.exceptions import PulseError -from qiskit.pulse.library import SymbolicPulse, Waveform -from qiskit.pulse.schedule import Schedule, ScheduleBlock -from qiskit.pulse.transforms.alignments import AlignmentKind -from qiskit.pulse.utils import ( - format_parameter_value, - _validate_parameter_vector, - _validate_parameter_value, -) - - -class NodeVisitor: - """A node visitor base class that walks instruction data in a pulse program and calls - visitor functions for every node. - - Though this class implementation is based on Python AST, each node doesn't have - a dedicated node class due to the lack of an abstract syntax tree for pulse programs in - Qiskit. Instead of parsing pulse programs, this visitor class finds the associated visitor - function based on class name of the instruction node, i.e. ``Play``, ``Call``, etc... - The `.visit` method recursively checks superclass of given node since some parametrized - components such as ``DriveChannel`` may share a common superclass with other subclasses. - In this example, we can just define ``visit_Channel`` method instead of defining - the same visitor function for every subclasses. - - Some instructions may have special logic or data structure to store parameter objects, - and visitor functions for these nodes should be individually defined. - - Because pulse programs can be nested into another pulse program, - the visitor function should be able to recursively call proper visitor functions. - If visitor function is not defined for a given node, ``generic_visit`` - method is called. Usually, this method is provided for operating on object defined - outside of the Qiskit Pulse module. - """ - - def visit(self, node: Any): - """Visit a node.""" - visitor = self._get_visitor(type(node)) - return visitor(node) - - def _get_visitor(self, node_class): - """A helper function to recursively investigate superclass visitor method.""" - if node_class == object: - return self.generic_visit - - try: - return getattr(self, f"visit_{node_class.__name__}") - except AttributeError: - # check super class - return self._get_visitor(node_class.__base__) - - def visit_ScheduleBlock(self, node: ScheduleBlock): - """Visit ``ScheduleBlock``. Recursively visit context blocks and overwrite. - - .. note:: ``ScheduleBlock`` can have parameters in blocks and its alignment. - """ - raise NotImplementedError - - def visit_Schedule(self, node: Schedule): - """Visit ``Schedule``. Recursively visit schedule children and overwrite.""" - raise NotImplementedError - - def generic_visit(self, node: Any): - """Called if no explicit visitor function exists for a node.""" - raise NotImplementedError - - -class ParameterSetter(NodeVisitor): - """Node visitor for parameter binding. - - This visitor is initialized with a dictionary of parameters to be assigned, - and assign values to operands of nodes found. - """ - - def __init__(self, param_map: dict[ParameterExpression, ParameterValueType]): - self._param_map = param_map - - # Top layer: Assign parameters to programs - - def visit_ScheduleBlock(self, node: ScheduleBlock): - """Visit ``ScheduleBlock``. Recursively visit context blocks and overwrite. - - .. note:: ``ScheduleBlock`` can have parameters in blocks and its alignment. - """ - node._alignment_context = self.visit_AlignmentKind(node.alignment_context) - for elm in node._blocks: - self.visit(elm) - - self._update_parameter_manager(node) - return node - - def visit_Schedule(self, node: Schedule): - """Visit ``Schedule``. Recursively visit schedule children and overwrite.""" - # accessing to private member - # TODO: consider updating Schedule to handle this more gracefully - node._Schedule__children = [(t0, self.visit(sched)) for t0, sched in node.instructions] - node._renew_timeslots() - - self._update_parameter_manager(node) - return node - - def visit_AlignmentKind(self, node: AlignmentKind): - """Assign parameters to block's ``AlignmentKind`` specification.""" - new_parameters = tuple(self.visit(param) for param in node._context_params) - node._context_params = new_parameters - - return node - - # Mid layer: Assign parameters to instructions - - def visit_Instruction(self, node: instructions.Instruction): - """Assign parameters to general pulse instruction. - - .. note:: All parametrized object should be stored in the operands. - Otherwise parameter cannot be detected. - """ - if node.is_parameterized(): - node._operands = tuple(self.visit(op) for op in node.operands) - - return node - - # Lower layer: Assign parameters to operands - - def visit_Channel(self, node: channels.Channel): - """Assign parameters to ``Channel`` object.""" - if node.is_parameterized(): - new_index = self._assign_parameter_expression(node.index) - - # validate - if not isinstance(new_index, ParameterExpression): - if not isinstance(new_index, int) or new_index < 0: - raise PulseError("Channel index must be a nonnegative integer") - - # return new instance to prevent accidentally override timeslots without evaluation - return node.__class__(index=new_index) - - return node - - def visit_SymbolicPulse(self, node: SymbolicPulse): - """Assign parameters to ``SymbolicPulse`` object.""" - if node.is_parameterized(): - # Assign duration - if isinstance(node.duration, ParameterExpression): - node.duration = self._assign_parameter_expression(node.duration) - # Assign other parameters - for name in node._params: - pval = node._params[name] - if isinstance(pval, ParameterExpression): - new_val = self._assign_parameter_expression(pval) - node._params[name] = new_val - if not node.disable_validation: - node.validate_parameters() - - return node - - def visit_Waveform(self, node: Waveform): - """Assign parameters to ``Waveform`` object. - - .. node:: No parameter can be assigned to ``Waveform`` object. - """ - return node - - def generic_visit(self, node: Any): - """Assign parameters to object that doesn't belong to Qiskit Pulse module.""" - if isinstance(node, ParameterExpression): - return self._assign_parameter_expression(node) - else: - return node - - def _assign_parameter_expression(self, param_expr: ParameterExpression): - """A helper function to assign parameter value to parameter expression.""" - new_value = copy(param_expr) - updated = param_expr.parameters & self._param_map.keys() - for param in updated: - new_value = new_value.assign(param, self._param_map[param]) - new_value = format_parameter_value(new_value) - return new_value - - def _update_parameter_manager(self, node: Schedule | ScheduleBlock): - """A helper function to update parameter manager of pulse program.""" - if not hasattr(node, "_parameter_manager"): - raise PulseError(f"Node type {node.__class__.__name__} has no parameter manager.") - - param_manager = node._parameter_manager - updated = param_manager.parameters & self._param_map.keys() - - new_parameters = set() - for param in param_manager.parameters: - if param not in updated: - new_parameters.add(param) - continue - new_value = self._param_map[param] - if isinstance(new_value, ParameterExpression): - new_parameters |= new_value.parameters - param_manager._parameters = new_parameters - - -class ParameterGetter(NodeVisitor): - """Node visitor for parameter finding. - - This visitor initializes empty parameter array, and recursively visits nodes - and add parameters found to the array. - """ - - def __init__(self): - self.parameters = set() - - # Top layer: Get parameters from programs - - def visit_ScheduleBlock(self, node: ScheduleBlock): - """Visit ``ScheduleBlock``. Recursively visit context blocks and search parameters. - - .. note:: ``ScheduleBlock`` can have parameters in blocks and its alignment. - """ - # Note that node.parameters returns parameters of main program with subroutines. - # The manager of main program is not aware of parameters in subroutines. - self.parameters |= node._parameter_manager.parameters - - def visit_Schedule(self, node: Schedule): - """Visit ``Schedule``. Recursively visit schedule children and search parameters.""" - self.parameters |= node.parameters - - def visit_AlignmentKind(self, node: AlignmentKind): - """Get parameters from block's ``AlignmentKind`` specification.""" - for param in node._context_params: - if isinstance(param, ParameterExpression): - self.parameters |= param.parameters - - # Mid layer: Get parameters from instructions - - def visit_Instruction(self, node: instructions.Instruction): - """Get parameters from general pulse instruction. - - .. note:: All parametrized object should be stored in the operands. - Otherwise, parameter cannot be detected. - """ - for op in node.operands: - self.visit(op) - - # Lower layer: Get parameters from operands - - def visit_Channel(self, node: channels.Channel): - """Get parameters from ``Channel`` object.""" - self.parameters |= node.parameters - - def visit_SymbolicPulse(self, node: SymbolicPulse): - """Get parameters from ``SymbolicPulse`` object.""" - for op_value in node.parameters.values(): - if isinstance(op_value, ParameterExpression): - self.parameters |= op_value.parameters - - def visit_Waveform(self, node: Waveform): - """Get parameters from ``Waveform`` object. - - .. node:: No parameter can be assigned to ``Waveform`` object. - """ - pass - - def generic_visit(self, node: Any): - """Get parameters from object that doesn't belong to Qiskit Pulse module.""" - if isinstance(node, ParameterExpression): - self.parameters |= node.parameters - - -class ParameterManager: - """Helper class to manage parameter objects associated with arbitrary pulse programs. - - This object is implicitly initialized with the parameter object storage - that stores parameter objects added to the parent pulse program. - - Parameter assignment logic is implemented based on the visitor pattern. - Instruction data and its location are not directly associated with this object. - """ - - def __init__(self): - """Create new parameter table for pulse programs.""" - self._parameters = set() - - @property - def parameters(self) -> set[Parameter]: - """Parameters which determine the schedule behavior.""" - return self._parameters - - def clear(self): - """Remove the parameters linked to this manager.""" - self._parameters.clear() - - def is_parameterized(self) -> bool: - """Return True iff the instruction is parameterized.""" - return bool(self.parameters) - - def get_parameters(self, parameter_name: str) -> list[Parameter]: - """Get parameter object bound to this schedule by string name. - - Because different ``Parameter`` objects can have the same name, - this method returns a list of ``Parameter`` s for the provided name. - - Args: - parameter_name: Name of parameter. - - Returns: - Parameter objects that have corresponding name. - """ - return [param for param in self.parameters if param.name == parameter_name] - - def assign_parameters( - self, - pulse_program: Any, - value_dict: dict[ - ParameterExpression | ParameterVector | str, - ParameterValueType | Sequence[ParameterValueType], - ], - ) -> Any: - """Modify and return program data with parameters assigned according to the input. - - Args: - pulse_program: Arbitrary pulse program associated with this manager instance. - value_dict: A mapping from Parameters to either numeric values or another - Parameter expression. - - Returns: - Updated program data. - """ - unrolled_value_dict = self._unroll_param_dict(value_dict) - valid_map = { - k: unrolled_value_dict[k] for k in unrolled_value_dict.keys() & self._parameters - } - if valid_map: - visitor = ParameterSetter(param_map=valid_map) - return visitor.visit(pulse_program) - return pulse_program - - def update_parameter_table(self, new_node: Any): - """A helper function to update parameter table with given data node. - - Args: - new_node: A new data node to be added. - """ - visitor = ParameterGetter() - visitor.visit(new_node) - self._parameters |= visitor.parameters - - def _unroll_param_dict( - self, - parameter_binds: Mapping[ - Parameter | ParameterVector | str, ParameterValueType | Sequence[ParameterValueType] - ], - ) -> Mapping[Parameter, ParameterValueType]: - """ - Unroll parameter dictionary to a map from parameter to value. - - Args: - parameter_binds: A dictionary from parameter to value or a list of values. - - Returns: - A dictionary from parameter to value. - """ - out = {} - param_name_dict = {param.name: [] for param in self.parameters} - for param in self.parameters: - param_name_dict[param.name].append(param) - param_vec_dict = { - param.vector.name: param.vector - for param in self.parameters - if isinstance(param, ParameterVectorElement) - } - for name in param_vec_dict.keys(): - if name in param_name_dict: - param_name_dict[name].append(param_vec_dict[name]) - else: - param_name_dict[name] = [param_vec_dict[name]] - - for parameter, value in parameter_binds.items(): - if isinstance(parameter, ParameterVector): - _validate_parameter_vector(parameter, value) - out.update(zip(parameter, value)) - elif isinstance(parameter, str): - for param in param_name_dict[parameter]: - is_vec = _validate_parameter_value(param, value) - if is_vec: - out.update(zip(param, value)) - else: - out[param] = value - else: - out[parameter] = value - return out diff --git a/qiskit/pulse/parser.py b/qiskit/pulse/parser.py deleted file mode 100644 index e9cd4917a7ce..000000000000 --- a/qiskit/pulse/parser.py +++ /dev/null @@ -1,314 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# 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. - -# pylint: disable=invalid-name - -"""Parser for mathematical string expressions returned by backends.""" -from __future__ import annotations -import ast -import copy -import operator - -import cmath -from collections.abc import Callable -from typing import Any - -from qiskit.pulse.exceptions import PulseError -from qiskit.circuit import ParameterExpression - - -class PulseExpression(ast.NodeTransformer): - """Expression parser to evaluate parameter values.""" - - _math_ops: dict[str, Callable | float] = { - "acos": cmath.acos, - "acosh": cmath.acosh, - "asin": cmath.asin, - "asinh": cmath.asinh, - "atan": cmath.atan, - "atanh": cmath.atanh, - "cos": cmath.cos, - "cosh": cmath.cosh, - "exp": cmath.exp, - "log": cmath.log, - "log10": cmath.log10, - "sin": cmath.sin, - "sinh": cmath.sinh, - "sqrt": cmath.sqrt, - "tan": cmath.tan, - "tanh": cmath.tanh, - "pi": cmath.pi, - "e": cmath.e, - } - """Valid math functions.""" - - _binary_ops = { - ast.Add: operator.add, - ast.Sub: operator.sub, - ast.Mult: operator.mul, - ast.Div: operator.truediv, - ast.Pow: operator.pow, - } - """Valid binary operations.""" - - _unary_ops = {ast.UAdd: operator.pos, ast.USub: operator.neg} - """Valid unary operations.""" - - def __init__(self, source: str | ast.Expression, partial_binding: bool = False): - """Create new evaluator. - - Args: - source: Expression of equation to evaluate. - partial_binding: Allow partial bind of parameters. - - Raises: - PulseError: When invalid string is specified. - """ - self._partial_binding = partial_binding - self._locals_dict: dict[str, Any] = {} - self._params: set[str] = set() - - if isinstance(source, ast.Expression): - self._tree = source - else: - try: - self._tree = ast.parse(source, mode="eval") - except SyntaxError as ex: - raise PulseError(f"{source} is invalid expression.") from ex - - # parse parameters - self.visit(self._tree) - - @property - def params(self) -> list[str]: - """Get parameters. - - Returns: - A list of parameters in sorted order. - """ - return sorted(self._params.copy()) - - def __call__(self, *args, **kwargs) -> complex | ast.Expression | PulseExpression: - """Evaluate the expression with the given values of the expression's parameters. - - Args: - *args: Variable length parameter list. - **kwargs: Arbitrary parameters. - - Returns: - Evaluated value. - - Raises: - PulseError: When parameters are not bound. - """ - if isinstance(self._tree.body, ast.Constant): - return self._tree.body.value - - self._locals_dict.clear() - if args: - for key, val in zip(self.params, args): - self._locals_dict[key] = val - if kwargs: - for key, val in kwargs.items(): - if key in self.params: - if key not in self._locals_dict: - self._locals_dict[key] = val - else: - raise PulseError( - f"{self.__class__.__name__} got multiple values for argument '{key}'" - ) - else: - raise PulseError( - f"{self.__class__.__name__} got an unexpected keyword argument '{key}'" - ) - - expr = self.visit(self._tree) - - if not isinstance(expr.body, ast.Constant): - if self._partial_binding: - return PulseExpression(expr, self._partial_binding) - else: - raise PulseError(f"Parameters {self.params} are not all bound.") - return expr.body.value - - @staticmethod - def _match_ops(opr: ast.AST, opr_dict: dict, *args) -> complex: - """Helper method to apply operators. - - Args: - opr: Operator of node. - opr_dict: Mapper from ast to operator. - *args: Arguments supplied to operator. - - Returns: - Evaluated value. - - Raises: - PulseError: When unsupported operation is specified. - """ - for op_type, op_func in opr_dict.items(): - if isinstance(opr, op_type): - return op_func(*args) - raise PulseError(f"Operator {opr.__class__.__name__} is not supported.") - - def visit_Expression(self, node: ast.Expression) -> ast.Expression: - """Evaluate children nodes of expression. - - Args: - node: Expression to evaluate. - - Returns: - Evaluated value. - """ - tmp_node = copy.copy(node) - tmp_node.body = self.visit(tmp_node.body) - - return tmp_node - - def visit_Constant(self, node: ast.Constant) -> ast.Constant: - """Return constant value as it is. - - Args: - node: Constant. - - Returns: - Input node. - """ - return node - - def visit_Name(self, node: ast.Name) -> ast.Name | ast.Constant: - """Evaluate name and return ast.Constant if it is bound. - - Args: - node: Name to evaluate. - - Returns: - Evaluated value. - - Raises: - PulseError: When parameter value is not a number. - """ - if node.id in self._math_ops: - val = ast.Constant(self._math_ops[node.id]) - return ast.copy_location(val, node) - elif node.id in self._locals_dict: - _val = self._locals_dict[node.id] - if not isinstance(_val, ParameterExpression): - # check value type - try: - _val = complex(_val) - if not _val.imag: - _val = _val.real - except ValueError as ex: - raise PulseError( - f"Invalid parameter value {node.id} = {self._locals_dict[node.id]} is " - "specified." - ) from ex - val = ast.Constant(_val) - return ast.copy_location(val, node) - self._params.add(node.id) - return node - - def visit_UnaryOp(self, node: ast.UnaryOp) -> ast.UnaryOp | ast.Constant: - """Evaluate unary operation and return ast.Constant if operand is bound. - - Args: - node: Unary operation to evaluate. - - Returns: - Evaluated value. - """ - node = copy.copy(node) - node.operand = self.visit(node.operand) - if isinstance(node.operand, ast.Constant): - val = ast.Constant(self._match_ops(node.op, self._unary_ops, node.operand.value)) - return ast.copy_location(val, node) - return node - - def visit_BinOp(self, node: ast.BinOp) -> ast.BinOp | ast.Constant: - """Evaluate binary operation and return ast.Constant if operands are bound. - - Args: - node: Binary operation to evaluate. - - Returns: - Evaluated value. - """ - node = copy.copy(node) - node.left = self.visit(node.left) - node.right = self.visit(node.right) - if isinstance(node.left, ast.Constant) and isinstance(node.right, ast.Constant): - val = ast.Constant( - self._match_ops(node.op, self._binary_ops, node.left.value, node.right.value) - ) - return ast.copy_location(val, node) - return node - - def visit_Call(self, node: ast.Call) -> ast.Call | ast.Constant: - """Evaluate function and return ast.Constant if all arguments are bound. - - Args: - node: Function to evaluate. - - Returns: - Evaluated value. - - Raises: - PulseError: When unsupported or unsafe function is specified. - """ - if not isinstance(node.func, ast.Name): - raise PulseError("Unsafe expression is detected.") - node = copy.copy(node) - node.args = [self.visit(arg) for arg in node.args] - if all(isinstance(arg, ast.Constant) for arg in node.args): - if node.func.id not in self._math_ops: - raise PulseError(f"Function {node.func.id} is not supported.") - _args = [arg.value for arg in node.args] - _val = self._math_ops[node.func.id](*_args) - if not _val.imag: - _val = _val.real - val = ast.Constant(_val) - return ast.copy_location(val, node) - return node - - def generic_visit(self, node): - raise PulseError(f"Unsupported node: {node.__class__.__name__}") - - -def parse_string_expr(source: str, partial_binding: bool = False) -> PulseExpression: - """Safe parsing of string expression. - - Args: - source: String expression to parse. - partial_binding: Allow partial bind of parameters. - - Returns: - PulseExpression: Returns a expression object. - - Example: - - expr = 'P1 + P2 + P3' - parsed_expr = parse_string_expr(expr, partial_binding=True) - - # create new PulseExpression - bound_two = parsed_expr(P1=1, P2=2) - # evaluate expression - value1 = bound_two(P3=3) - value2 = bound_two(P3=4) - value3 = bound_two(P3=5) - - """ - subs = [("numpy.", ""), ("np.", ""), ("math.", ""), ("cmath.", "")] - for match, sub in subs: - source = source.replace(match, sub) - - return PulseExpression(source, partial_binding) diff --git a/qiskit/pulse/reference_manager.py b/qiskit/pulse/reference_manager.py deleted file mode 100644 index 9016fa7efaec..000000000000 --- a/qiskit/pulse/reference_manager.py +++ /dev/null @@ -1,58 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2022. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Management of schedule block references.""" - -from typing import Tuple -from collections import UserDict -from qiskit.pulse.exceptions import PulseError - - -class ReferenceManager(UserDict): - """Dictionary wrapper to manage pulse schedule references.""" - - def unassigned(self) -> Tuple[Tuple[str, ...], ...]: - """Get the keys of unassigned references. - - Returns: - Tuple of reference keys. - """ - keys = [] - for key, value in self.items(): - if value is None: - keys.append(key) - return tuple(keys) - - def __setitem__(self, key, value): - if key in self and self[key] is not None: - # Check subroutine conflict. - if self[key] != value: - raise PulseError( - f"Subroutine {key} is already assigned to the reference of the current scope, " - "however, the newly assigned schedule conflicts with the existing schedule. " - "This operation was not successfully done." - ) - return - super().__setitem__(key, value) - - def __repr__(self): - keys = ", ".join(map(repr, self.keys())) - return f"{self.__class__.__name__}(references=[{keys}])" - - def __str__(self): - out = f"{self.__class__.__name__}:" - for key, reference in self.items(): - prog_repr = repr(reference) - if len(prog_repr) > 50: - prog_repr = prog_repr[:50] + "..." - out += f"\n - {repr(key)}: {prog_repr}" - return out diff --git a/qiskit/pulse/schedule.py b/qiskit/pulse/schedule.py deleted file mode 100644 index 6d7db303d297..000000000000 --- a/qiskit/pulse/schedule.py +++ /dev/null @@ -1,1909 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019. -# -# 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. - -# pylint: disable=cyclic-import - -""" -========= -Schedules -========= - -.. currentmodule:: qiskit.pulse - -Schedules are Pulse programs. They describe instruction sequences for the control hardware. -The Schedule is one of the most fundamental objects to this pulse-level programming module. -A ``Schedule`` is a representation of a *program* in Pulse. Each schedule tracks the time of each -instruction occuring in parallel over multiple signal *channels*. - -.. autosummary:: - :toctree: ../stubs/ - - Schedule - ScheduleBlock -""" -from __future__ import annotations -import abc -import copy -import functools -import itertools -import multiprocessing as mp -import sys -import warnings -from collections.abc import Callable, Iterable -from typing import List, Tuple, Union, Dict, Any, Sequence - -import numpy as np -import rustworkx as rx - -from qiskit.circuit import ParameterVector -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, UnassignedReferenceError -from qiskit.pulse.instructions import Instruction, Reference -from qiskit.pulse.utils import instruction_duration_validation -from qiskit.pulse.reference_manager import ReferenceManager -from qiskit.utils import deprecate_arg -from qiskit.utils.deprecate_pulse import deprecate_pulse_func - - -Interval = Tuple[int, int] -"""An interval type is a tuple of a start time (inclusive) and an end time (exclusive).""" - -TimeSlots = Dict[Channel, List[Interval]] -"""List of timeslots occupied by instructions for each channel.""" - - -class Schedule: - """A quantum program *schedule* with exact time constraints for its instructions, operating - over all input signal *channels* and supporting special syntaxes for building. - - Pulse program representation for the original Qiskit Pulse model [1]. - Instructions are not allowed to overlap in time - on the same channel. This overlap constraint is immediately - evaluated when a new instruction is added to the ``Schedule`` object. - - It is necessary to specify the absolute start time and duration - for each instruction so as to deterministically fix its execution time. - - The ``Schedule`` program supports some syntax sugar for easier programming. - - - Appending an instruction to the end of a channel - - .. plot:: - :include-source: - :nofigs: - :context: reset - - from qiskit.pulse import Schedule, Gaussian, DriveChannel, Play - sched = Schedule() - sched += Play(Gaussian(160, 0.1, 40), DriveChannel(0)) - - - Appending an instruction shifted in time by a given amount - - .. plot:: - :include-source: - :nofigs: - :context: - - sched = Schedule() - sched += Play(Gaussian(160, 0.1, 40), DriveChannel(0)) << 30 - - - Merge two schedules - - .. plot:: - :include-source: - :nofigs: - :context: - - sched1 = Schedule() - sched1 += Play(Gaussian(160, 0.1, 40), DriveChannel(0)) - - sched2 = Schedule() - sched2 += Play(Gaussian(160, 0.1, 40), DriveChannel(1)) - sched2 = sched1 | sched2 - - A :obj:`.PulseError` is immediately raised when the overlap constraint is violated. - - In the schedule representation, we cannot parametrize the duration of instructions. - Thus, we need to create a new schedule object for each duration. - To parametrize an instruction's duration, the :class:`~qiskit.pulse.ScheduleBlock` - representation may be used instead. - - References: - [1]: https://arxiv.org/abs/2004.06755 - - """ - - # Prefix to use for auto naming. - prefix = "sched" - - # Counter to count instance number. - instances_counter = itertools.count() - - @deprecate_pulse_func - def __init__( - self, - *schedules: "ScheduleComponent" | tuple[int, "ScheduleComponent"], - name: str | None = None, - metadata: dict | None = None, - ): - """Create an empty schedule. - - Args: - *schedules: Child Schedules of this parent Schedule. May either be passed as - the list of schedules, or a list of ``(start_time, schedule)`` pairs. - name: Name of this schedule. Defaults to an autogenerated string if not provided. - metadata: Arbitrary key value metadata to associate with the schedule. This gets - stored as free-form data in a dict in the - :attr:`~qiskit.pulse.Schedule.metadata` attribute. It will not be directly - used in the schedule. - Raises: - TypeError: if metadata is not a dict. - """ - from qiskit.pulse.parameter_manager import ParameterManager - - if name is None: - name = self.prefix + str(next(self.instances_counter)) - if sys.platform != "win32" and mp.parent_process() is not None: - name += f"-{mp.current_process().pid}" - - self._name = name - self._parameter_manager = ParameterManager() - - if not isinstance(metadata, dict) and metadata is not None: - raise TypeError("Only a dictionary or None is accepted for schedule metadata") - self._metadata = metadata or {} - - self._duration = 0 - - # These attributes are populated by ``_mutable_insert`` - self._timeslots: TimeSlots = {} - self._children: list[tuple[int, "ScheduleComponent"]] = [] - for sched_pair in schedules: - try: - time, sched = sched_pair - except TypeError: - # recreate as sequence starting at 0. - time, sched = 0, sched_pair - self._mutable_insert(time, sched) - - @classmethod - def initialize_from(cls, other_program: Any, name: str | None = None) -> "Schedule": - """Create new schedule object with metadata of another schedule object. - - Args: - other_program: Qiskit program that provides metadata to new object. - name: Name of new schedule. Name of ``schedule`` is used by default. - - Returns: - New schedule object with name and metadata. - - Raises: - PulseError: When `other_program` does not provide necessary information. - """ - try: - name = name or other_program.name - - if other_program.metadata: - metadata = other_program.metadata.copy() - else: - metadata = None - - return cls(name=name, metadata=metadata) - except AttributeError as ex: - raise PulseError( - f"{cls.__name__} cannot be initialized from the program data " - f"{other_program.__class__.__name__}." - ) from ex - - @property - def name(self) -> str: - """Name of this Schedule""" - return self._name - - @property - def metadata(self) -> dict[str, Any]: - """The user provided metadata associated with the schedule. - - User provided ``dict`` of metadata for the schedule. - The metadata contents do not affect the semantics of the program - but are used to influence the execution of the schedule. It is expected - to be passed between all transforms of the schedule and that providers - will associate any schedule metadata with the results it returns from the - execution of that schedule. - """ - return self._metadata - - @metadata.setter - def metadata(self, metadata): - """Update the schedule metadata""" - if not isinstance(metadata, dict) and metadata is not None: - raise TypeError("Only a dictionary or None is accepted for schedule metadata") - self._metadata = metadata or {} - - @property - def timeslots(self) -> TimeSlots: - """Time keeping attribute.""" - return self._timeslots - - @property - def duration(self) -> int: - """Duration of this schedule.""" - return self._duration - - @property - def start_time(self) -> int: - """Starting time of this schedule.""" - return self.ch_start_time(*self.channels) - - @property - def stop_time(self) -> int: - """Stopping time of this schedule.""" - return self.duration - - @property - def channels(self) -> tuple[Channel, ...]: - """Returns channels that this schedule uses.""" - return tuple(self._timeslots.keys()) - - @property - def children(self) -> tuple[tuple[int, "ScheduleComponent"], ...]: - """Return the child schedule components of this ``Schedule`` in the - order they were added to the schedule. - - Notes: - Nested schedules are returned as-is. If you want to collect only instructions, - use :py:meth:`~Schedule.instructions` instead. - - Returns: - A tuple, where each element is a two-tuple containing the initial - scheduled time of each ``NamedValue`` and the component - itself. - """ - return tuple(self._children) - - @property - def instructions(self) -> tuple[tuple[int, Instruction], ...]: - """Get the time-ordered instructions from self.""" - - def key(time_inst_pair): - inst = time_inst_pair[1] - return time_inst_pair[0], inst.duration, sorted(chan.name for chan in inst.channels) - - return tuple(sorted(self._instructions(), key=key)) - - @property - def parameters(self) -> set[Parameter]: - """Parameters which determine the schedule behavior.""" - return self._parameter_manager.parameters - - def ch_duration(self, *channels: Channel) -> int: - """Return the time of the end of the last instruction over the supplied channels. - - Args: - *channels: Channels within ``self`` to include. - """ - return self.ch_stop_time(*channels) - - def ch_start_time(self, *channels: Channel) -> int: - """Return the time of the start of the first instruction over the supplied channels. - - Args: - *channels: Channels within ``self`` to include. - """ - try: - chan_intervals = (self._timeslots[chan] for chan in channels if chan in self._timeslots) - return min(intervals[0][0] for intervals in chan_intervals) - except ValueError: - # If there are no instructions over channels - return 0 - - def ch_stop_time(self, *channels: Channel) -> int: - """Return maximum start time over supplied channels. - - Args: - *channels: Channels within ``self`` to include. - """ - try: - chan_intervals = (self._timeslots[chan] for chan in channels if chan in self._timeslots) - return max(intervals[-1][1] for intervals in chan_intervals) - except ValueError: - # If there are no instructions over channels - return 0 - - def _instructions(self, time: int = 0): - """Iterable for flattening Schedule tree. - - Args: - time: Shifted time due to parent. - - Yields: - Iterable[Tuple[int, Instruction]]: Tuple containing the time each - :class:`~qiskit.pulse.Instruction` - starts at and the flattened :class:`~qiskit.pulse.Instruction` s. - """ - for insert_time, child_sched in self.children: - yield from child_sched._instructions(time + insert_time) - - def shift(self, time: int, name: str | None = None, inplace: bool = False) -> "Schedule": - """Return a schedule shifted forward by ``time``. - - Args: - time: Time to shift by. - name: Name of the new schedule. Defaults to the name of self. - inplace: Perform operation inplace on this schedule. Otherwise - return a new ``Schedule``. - """ - if inplace: - return self._mutable_shift(time) - return self._immutable_shift(time, name=name) - - def _immutable_shift(self, time: int, name: str | None = None) -> "Schedule": - """Return a new schedule shifted forward by `time`. - - Args: - time: Time to shift by - name: Name of the new schedule if call was mutable. Defaults to name of self - """ - shift_sched = Schedule.initialize_from(self, name) - shift_sched.insert(time, self, inplace=True) - - return shift_sched - - def _mutable_shift(self, time: int) -> "Schedule": - """Return this schedule shifted forward by `time`. - - Args: - time: Time to shift by - - Raises: - PulseError: if ``time`` is not an integer. - """ - if not isinstance(time, int): - raise PulseError("Schedule start time must be an integer.") - - timeslots = {} - for chan, ch_timeslots in self._timeslots.items(): - timeslots[chan] = [(ts[0] + time, ts[1] + time) for ts in ch_timeslots] - - _check_nonnegative_timeslot(timeslots) - - self._duration = self._duration + time - self._timeslots = timeslots - self._children = [(orig_time + time, child) for orig_time, child in self.children] - return self - - def insert( - self, - start_time: int, - schedule: "ScheduleComponent", - name: str | None = None, - inplace: bool = False, - ) -> "Schedule": - """Return a new schedule with ``schedule`` inserted into ``self`` at ``start_time``. - - Args: - start_time: Time to insert the schedule. - schedule: Schedule to insert. - name: Name of the new schedule. Defaults to the name of self. - inplace: Perform operation inplace on this schedule. Otherwise - return a new ``Schedule``. - """ - if inplace: - return self._mutable_insert(start_time, schedule) - return self._immutable_insert(start_time, schedule, name=name) - - def _mutable_insert(self, start_time: int, schedule: "ScheduleComponent") -> "Schedule": - """Mutably insert `schedule` into `self` at `start_time`. - - Args: - start_time: Time to insert the second schedule. - schedule: Schedule to mutably insert. - """ - self._add_timeslots(start_time, schedule) - self._children.append((start_time, schedule)) - self._parameter_manager.update_parameter_table(schedule) - return self - - def _immutable_insert( - self, - start_time: int, - schedule: "ScheduleComponent", - name: str | None = None, - ) -> "Schedule": - """Return a new schedule with ``schedule`` inserted into ``self`` at ``start_time``. - Args: - start_time: Time to insert the schedule. - schedule: Schedule to insert. - name: Name of the new ``Schedule``. Defaults to name of ``self``. - """ - new_sched = Schedule.initialize_from(self, name) - new_sched._mutable_insert(0, self) - new_sched._mutable_insert(start_time, schedule) - return new_sched - - def append( - self, schedule: "ScheduleComponent", name: str | None = None, inplace: bool = False - ) -> "Schedule": - r"""Return a new schedule with ``schedule`` inserted at the maximum time over - all channels shared between ``self`` and ``schedule``. - - .. math:: - - t = \textrm{max}(\texttt{x.stop_time} |\texttt{x} \in - \texttt{self.channels} \cap \texttt{schedule.channels}) - - Args: - schedule: Schedule to be appended. - name: Name of the new ``Schedule``. Defaults to name of ``self``. - inplace: Perform operation inplace on this schedule. Otherwise - return a new ``Schedule``. - """ - common_channels = set(self.channels) & set(schedule.channels) - time = self.ch_stop_time(*common_channels) - return self.insert(time, schedule, name=name, inplace=inplace) - - def filter( - self, - *filter_funcs: Callable, - channels: Iterable[Channel] | None = None, - instruction_types: Iterable[abc.ABCMeta] | abc.ABCMeta = None, - time_ranges: Iterable[tuple[int, int]] | None = None, - intervals: Iterable[Interval] | None = None, - check_subroutine: bool = True, - ) -> "Schedule": - """Return a new ``Schedule`` with only the instructions from this ``Schedule`` which pass - though the provided filters; i.e. an instruction will be retained iff every function in - ``filter_funcs`` returns ``True``, the instruction occurs on a channel type contained in - ``channels``, the instruction type is contained in ``instruction_types``, and the period - over which the instruction operates is *fully* contained in one specified in - ``time_ranges`` or ``intervals``. - - If no arguments are provided, ``self`` is returned. - - Args: - filter_funcs: A list of Callables which take a (int, Union['Schedule', Instruction]) - tuple and return a bool. - channels: For example, ``[DriveChannel(0), AcquireChannel(0)]``. - instruction_types: For example, ``[PulseInstruction, AcquireInstruction]``. - time_ranges: For example, ``[(0, 5), (6, 10)]``. - intervals: For example, ``[(0, 5), (6, 10)]``. - check_subroutine: Set `True` to individually filter instructions inside of a subroutine - defined by the :py:class:`~qiskit.pulse.instructions.Call` instruction. - """ - from qiskit.pulse.filters import composite_filter, filter_instructions - - filters = composite_filter(channels, instruction_types, time_ranges, intervals) - filters.extend(filter_funcs) - - return filter_instructions( - self, filters=filters, negate=False, recurse_subroutines=check_subroutine - ) - - def exclude( - self, - *filter_funcs: Callable, - channels: Iterable[Channel] | None = None, - instruction_types: Iterable[abc.ABCMeta] | abc.ABCMeta = None, - time_ranges: Iterable[tuple[int, int]] | None = None, - intervals: Iterable[Interval] | None = None, - check_subroutine: bool = True, - ) -> "Schedule": - """Return a ``Schedule`` with only the instructions from this Schedule *failing* - at least one of the provided filters. - This method is the complement of :py:meth:`~Schedule.filter`, so that:: - - self.filter(args) | self.exclude(args) == self - - Args: - filter_funcs: A list of Callables which take a (int, Union['Schedule', Instruction]) - tuple and return a bool. - channels: For example, ``[DriveChannel(0), AcquireChannel(0)]``. - instruction_types: For example, ``[PulseInstruction, AcquireInstruction]``. - time_ranges: For example, ``[(0, 5), (6, 10)]``. - intervals: For example, ``[(0, 5), (6, 10)]``. - check_subroutine: Set `True` to individually filter instructions inside of a subroutine - defined by the :py:class:`~qiskit.pulse.instructions.Call` instruction. - """ - from qiskit.pulse.filters import composite_filter, filter_instructions - - filters = composite_filter(channels, instruction_types, time_ranges, intervals) - filters.extend(filter_funcs) - - return filter_instructions( - self, filters=filters, negate=True, recurse_subroutines=check_subroutine - ) - - def _add_timeslots(self, time: int, schedule: "ScheduleComponent") -> None: - """Update all time tracking within this schedule based on the given schedule. - - Args: - time: The time to insert the schedule into self. - schedule: The schedule to insert into self. - - Raises: - PulseError: If timeslots overlap or an invalid start time is provided. - """ - if not np.issubdtype(type(time), np.integer): - raise PulseError("Schedule start time must be an integer.") - - other_timeslots = _get_timeslots(schedule) - self._duration = max(self._duration, time + schedule.duration) - - for channel in schedule.channels: - if channel not in self._timeslots: - if time == 0: - self._timeslots[channel] = copy.copy(other_timeslots[channel]) - else: - self._timeslots[channel] = [ - (i[0] + time, i[1] + time) for i in other_timeslots[channel] - ] - continue - - for idx, interval in enumerate(other_timeslots[channel]): - if interval[0] + time >= self._timeslots[channel][-1][1]: - # Can append the remaining intervals - self._timeslots[channel].extend( - [(i[0] + time, i[1] + time) for i in other_timeslots[channel][idx:]] - ) - break - - try: - interval = (interval[0] + time, interval[1] + time) - index = _find_insertion_index(self._timeslots[channel], interval) - self._timeslots[channel].insert(index, interval) - except PulseError as ex: - raise PulseError( - f"Schedule(name='{schedule.name or ''}') cannot be inserted into " - f"Schedule(name='{self.name or ''}') at " - f"time {time} because its instruction on channel {channel} scheduled from time " - f"{interval[0]} to {interval[1]} overlaps with an existing instruction." - ) from ex - - _check_nonnegative_timeslot(self._timeslots) - - def _remove_timeslots(self, time: int, schedule: "ScheduleComponent"): - """Delete the timeslots if present for the respective schedule component. - - Args: - time: The time to remove the timeslots for the ``schedule`` component. - schedule: The schedule to insert into self. - - Raises: - PulseError: If timeslots overlap or an invalid start time is provided. - """ - if not isinstance(time, int): - raise PulseError("Schedule start time must be an integer.") - - for channel in schedule.channels: - - if channel not in self._timeslots: - raise PulseError(f"The channel {channel} is not present in the schedule") - - channel_timeslots = self._timeslots[channel] - other_timeslots = _get_timeslots(schedule) - - for interval in other_timeslots[channel]: - if channel_timeslots: - interval = (interval[0] + time, interval[1] + time) - index = _interval_index(channel_timeslots, interval) - if channel_timeslots[index] == interval: - channel_timeslots.pop(index) - continue - - raise PulseError( - f"Cannot find interval ({interval[0]}, {interval[1]}) to remove from " - f"channel {channel} in Schedule(name='{schedule.name}')." - ) - - if not channel_timeslots: - self._timeslots.pop(channel) - - def _replace_timeslots(self, time: int, old: "ScheduleComponent", new: "ScheduleComponent"): - """Replace the timeslots of ``old`` if present with the timeslots of ``new``. - - Args: - time: The time to remove the timeslots for the ``schedule`` component. - old: Instruction to replace. - new: Instruction to replace with. - """ - self._remove_timeslots(time, old) - self._add_timeslots(time, new) - - def _renew_timeslots(self): - """Regenerate timeslots based on current instructions.""" - self._timeslots.clear() - for t0, inst in self.instructions: - self._add_timeslots(t0, inst) - - def replace( - self, - old: "ScheduleComponent", - new: "ScheduleComponent", - inplace: bool = False, - ) -> "Schedule": - """Return a ``Schedule`` with the ``old`` instruction replaced with a ``new`` - instruction. - - The replacement matching is based on an instruction equality check. - - .. plot:: - :include-source: - :nofigs: - :context: reset - - from qiskit import pulse - - d0 = pulse.DriveChannel(0) - - sched = pulse.Schedule() - - old = pulse.Play(pulse.Constant(100, 1.0), d0) - new = pulse.Play(pulse.Constant(100, 0.1), d0) - - sched += old - - sched = sched.replace(old, new) - - assert sched == pulse.Schedule(new) - - Only matches at the top-level of the schedule tree. If you wish to - perform this replacement over all instructions in the schedule tree. - Flatten the schedule prior to running: - - .. plot:: - :include-source: - :nofigs: - :context: - - sched = pulse.Schedule() - - sched += pulse.Schedule(old) - - sched = sched.replace(old, new) - - assert sched == pulse.Schedule(new) - - Args: - old: Instruction to replace. - new: Instruction to replace with. - inplace: Replace instruction by mutably modifying this ``Schedule``. - - Returns: - The modified schedule with ``old`` replaced by ``new``. - - Raises: - PulseError: If the ``Schedule`` after replacements will has a timing overlap. - """ - from qiskit.pulse.parameter_manager import ParameterManager - - new_children = [] - new_parameters = ParameterManager() - - for time, child in self.children: - if child == old: - new_children.append((time, new)) - new_parameters.update_parameter_table(new) - else: - new_children.append((time, child)) - new_parameters.update_parameter_table(child) - - if inplace: - self._children = new_children - self._parameter_manager = new_parameters - self._renew_timeslots() - return self - else: - try: - new_sched = Schedule.initialize_from(self) - for time, inst in new_children: - new_sched.insert(time, inst, inplace=True) - return new_sched - except PulseError as err: - raise PulseError( - f"Replacement of {old} with {new} results in overlapping instructions." - ) from err - - def is_parameterized(self) -> bool: - """Return True iff the instruction is parameterized.""" - return self._parameter_manager.is_parameterized() - - def assign_parameters( - self, - value_dict: dict[ - ParameterExpression | ParameterVector | str, - ParameterValueType | Sequence[ParameterValueType], - ], - inplace: bool = True, - ) -> "Schedule": - """Assign the parameters in this schedule according to the input. - - Args: - value_dict: A mapping from parameters or parameter names (parameter vector - or parameter vector name) to either numeric values (list of numeric values) - or another parameter expression (list of parameter expressions). - inplace: Set ``True`` to override this instance with new parameter. - - Returns: - Schedule with updated parameters. - """ - if not inplace: - new_schedule = copy.deepcopy(self) - return new_schedule.assign_parameters(value_dict, inplace=True) - - return self._parameter_manager.assign_parameters(pulse_program=self, value_dict=value_dict) - - def get_parameters(self, parameter_name: str) -> list[Parameter]: - """Get parameter object bound to this schedule by string name. - - Because different ``Parameter`` objects can have the same name, - this method returns a list of ``Parameter`` s for the provided name. - - Args: - parameter_name: Name of parameter. - - Returns: - Parameter objects that have corresponding name. - """ - return self._parameter_manager.get_parameters(parameter_name) - - def __len__(self) -> int: - """Return number of instructions in the schedule.""" - return len(self.instructions) - - def __add__(self, other: "ScheduleComponent") -> "Schedule": - """Return a new schedule with ``other`` inserted within ``self`` at ``start_time``.""" - return self.append(other) - - def __or__(self, other: "ScheduleComponent") -> "Schedule": - """Return a new schedule which is the union of `self` and `other`.""" - return self.insert(0, other) - - def __lshift__(self, time: int) -> "Schedule": - """Return a new schedule which is shifted forward by ``time``.""" - return self.shift(time) - - def __eq__(self, other: object) -> bool: - """Test if two Schedule are equal. - - Equality is checked by verifying there is an equal instruction at every time - in ``other`` for every instruction in this ``Schedule``. - - .. warning:: - - This does not check for logical equivalency. Ie., - - ```python - >>> Delay(10, DriveChannel(0)) + Delay(10, DriveChannel(0)) - == Delay(20, DriveChannel(0)) - False - ``` - """ - # 0. type check, we consider Instruction is a subtype of schedule - if not isinstance(other, (type(self), Instruction)): - return False - - # 1. channel check - if set(self.channels) != set(other.channels): - return False - - # 2. size check - if len(self.instructions) != len(other.instructions): - return False - - # 3. instruction check - return all( - self_inst == other_inst - for self_inst, other_inst in zip(self.instructions, other.instructions) - ) - - 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 += ", ..." - return f'{self.__class__.__name__}({instructions}, name="{name}")' - - -def _require_schedule_conversion(function: Callable) -> Callable: - """A method decorator to convert schedule block to pulse schedule. - - This conversation is performed for backward compatibility only if all durations are assigned. - """ - - @functools.wraps(function) - def wrapper(self, *args, **kwargs): - from qiskit.pulse.transforms import block_to_schedule - - return function(block_to_schedule(self), *args, **kwargs) - - return wrapper - - -class ScheduleBlock: - """Time-ordered sequence of instructions with alignment context. - - :class:`.ScheduleBlock` supports lazy scheduling of context instructions, - i.e. their timeslots is always generated at runtime. - This indicates we can parametrize instruction durations as well as - other parameters. In contrast to :class:`.Schedule` being somewhat static, - :class:`.ScheduleBlock` is a dynamic representation of a pulse program. - - .. rubric:: Pulse Builder - - The Qiskit pulse builder is a domain specific language that is developed on top of - the schedule block. Use of the builder syntax will improve the workflow of - pulse programming. See :ref:`pulse_builder` for a user guide. - - .. rubric:: Alignment contexts - - A schedule block is always relatively scheduled. - Instead of taking individual instructions with absolute execution time ``t0``, - the schedule block defines a context of scheduling and instructions - under the same context are scheduled in the same manner (alignment). - Several contexts are available in :ref:`pulse_alignments`. - A schedule block is instantiated with one of these alignment contexts. - The default context is :class:`AlignLeft`, for which all instructions are left-justified, - in other words, meaning they use as-soon-as-possible scheduling. - - If you need an absolute-time interval in between instructions, you can explicitly - insert :class:`~qiskit.pulse.instructions.Delay` instructions. - - .. rubric:: Nested blocks - - A schedule block can contain other nested blocks with different alignment contexts. - This enables advanced scheduling, where a subset of instructions is - locally scheduled in a different manner. - Note that a :class:`.Schedule` instance cannot be directly added to a schedule block. - To add a :class:`.Schedule` instance, wrap it in a :class:`.Call` instruction. - This is implicitly performed when a schedule is added through the :ref:`pulse_builder`. - - .. rubric:: Unsupported operations - - Because the schedule block representation lacks timeslots, it cannot - perform particular :class:`.Schedule` operations such as :meth:`insert` or :meth:`shift` that - require instruction start time ``t0``. - In addition, :meth:`exclude` and :meth:`filter` methods are not supported - because these operations may identify the target instruction with ``t0``. - Except for these operations, :class:`.ScheduleBlock` provides full compatibility - with :class:`.Schedule`. - - .. rubric:: Subroutine - - The timeslots-free representation offers much greater flexibility for writing pulse programs. - Because :class:`.ScheduleBlock` only cares about the ordering of the child blocks - we can add an undefined pulse sequence as a subroutine of the main program. - If your program contains the same sequence multiple times, this representation may - reduce the memory footprint required by the program construction. - Such a subroutine is realized by the special compiler directive - :class:`~qiskit.pulse.instructions.Reference` that is defined by - a unique set of reference key strings to the subroutine. - The (executable) subroutine is separately stored in the main program. - Appended reference directives are resolved when the main program is executed. - Subroutines must be assigned through :meth:`assign_references` before execution. - - One way to reference a subroutine in a schedule is to use the pulse - builder's :func:`~qiskit.pulse.builder.reference` function to declare an - unassigned reference. In this example, the program is called with the - reference key "grand_child". You can call a subroutine without specifying - a substantial program. - - .. plot:: - :include-source: - :nofigs: - :context: reset - - from qiskit import pulse - from qiskit.circuit.parameter import Parameter - - amp1 = Parameter("amp1") - amp2 = Parameter("amp2") - - with pulse.build() as sched_inner: - pulse.play(pulse.Constant(100, amp1), pulse.DriveChannel(0)) - - with pulse.build() as sched_outer: - with pulse.align_right(): - pulse.reference("grand_child") - pulse.play(pulse.Constant(200, amp2), pulse.DriveChannel(0)) - - # Now assign the inner pulse program to this reference - sched_outer.assign_references({("grand_child",): sched_inner}) - print(sched_outer.parameters) - - .. code-block:: text - - {Parameter(amp1), Parameter(amp2)} - - The outer program now has the parameter ``amp2`` from the inner program, - indicating that the inner program's data has been made available to the - outer program. - The program calling the "grand_child" has a reference program description - which is accessed through :attr:`ScheduleBlock.references`. - - .. plot:: - :include-source: - :nofigs: - :context: - - print(sched_outer.references) - - .. code-block:: text - - ReferenceManager: - - ('grand_child',): ScheduleBlock(Play(Constant(duration=100, amp=amp1,... - - Finally, you may want to call this program from another program. - Here we try a different approach to define subroutine. Namely, we call - a subroutine from the root program with the actual program ``sched2``. - - .. plot:: - :include-source: - :nofigs: - :context: - - amp3 = Parameter("amp3") - - with pulse.build() as main: - pulse.play(pulse.Constant(300, amp3), pulse.DriveChannel(0)) - pulse.call(sched_outer, name="child") - - print(main.parameters) - - .. code-block:: text - - {Parameter(amp1), Parameter(amp2), Parameter(amp3} - - This implicitly creates a reference named "child" within - the root program and assigns ``sched_outer`` to it. - - Note that the root program is only aware of its direct references. - - .. plot:: - :include-source: - :nofigs: - :context: - - print(main.references) - - .. code-block:: text - - ReferenceManager: - - ('child',): ScheduleBlock(ScheduleBlock(ScheduleBlock(Play(Con... - - As you can see the main program cannot directly assign a subroutine to the "grand_child" because - this subroutine is not called within the root program, i.e. it is indirectly called by "child". - However, the returned :class:`.ReferenceManager` is a dict-like object, and you can still - reach to "grand_child" via the "child" program with the following chained dict access. - - .. plot:: - :include-source: - :nofigs: - :context: - - main.references[("child", )].references[("grand_child", )] - - Note that :attr:`ScheduleBlock.parameters` still collects all parameters - also from the subroutine once it's assigned. - """ - - __slots__ = ( - "_parent", - "_name", - "_reference_manager", - "_parameter_manager", - "_alignment_context", - "_blocks", - "_metadata", - ) - - # Prefix to use for auto naming. - prefix = "block" - - # Counter to count instance number. - instances_counter = itertools.count() - - @deprecate_pulse_func - def __init__( - self, name: str | None = None, metadata: dict | None = None, alignment_context=None - ): - """Create an empty schedule block. - - Args: - name: Name of this schedule. Defaults to an autogenerated string if not provided. - metadata: Arbitrary key value metadata to associate with the schedule. This gets - stored as free-form data in a dict in the - :attr:`~qiskit.pulse.ScheduleBlock.metadata` attribute. It will not be directly - used in the schedule. - alignment_context (AlignmentKind): ``AlignmentKind`` instance that manages - scheduling of instructions in this block. - - Raises: - TypeError: if metadata is not a dict. - """ - from qiskit.pulse.parameter_manager import ParameterManager - from qiskit.pulse.transforms import AlignLeft - - if name is None: - name = self.prefix + str(next(self.instances_counter)) - if sys.platform != "win32" and mp.parent_process() is not None: - name += f"-{mp.current_process().pid}" - - # This points to the parent schedule object in the current scope. - # Note that schedule block can be nested without referencing, e.g. .append(child_block), - # and parent=None indicates the root program of the current scope. - # The nested schedule block objects should not have _reference_manager and - # should refer to the one of the root program. - # This also means referenced program should be assigned to the root program, not to child. - self._parent: ScheduleBlock | None = None - - self._name = name - self._parameter_manager = ParameterManager() - self._reference_manager = ReferenceManager() - self._alignment_context = alignment_context or AlignLeft() - self._blocks: list["BlockComponent"] = [] - - # get parameters from context - self._parameter_manager.update_parameter_table(self._alignment_context) - - if not isinstance(metadata, dict) and metadata is not None: - raise TypeError("Only a dictionary or None is accepted for schedule metadata") - self._metadata = metadata or {} - - @classmethod - def initialize_from(cls, other_program: Any, name: str | None = None) -> "ScheduleBlock": - """Create new schedule object with metadata of another schedule object. - - Args: - other_program: Qiskit program that provides metadata to new object. - name: Name of new schedule. Name of ``block`` is used by default. - - Returns: - New block object with name and metadata. - - Raises: - PulseError: When ``other_program`` does not provide necessary information. - """ - try: - name = name or other_program.name - - if other_program.metadata: - metadata = other_program.metadata.copy() - else: - metadata = None - - try: - alignment_context = other_program.alignment_context - except AttributeError: - alignment_context = None - - return cls(name=name, metadata=metadata, alignment_context=alignment_context) - except AttributeError as ex: - raise PulseError( - f"{cls.__name__} cannot be initialized from the program data " - f"{other_program.__class__.__name__}." - ) from ex - - @property - def name(self) -> str: - """Return name of this schedule""" - return self._name - - @property - def metadata(self) -> dict[str, Any]: - """The user provided metadata associated with the schedule. - - User provided ``dict`` of metadata for the schedule. - The metadata contents do not affect the semantics of the program - but are used to influence the execution of the schedule. It is expected - to be passed between all transforms of the schedule and that providers - will associate any schedule metadata with the results it returns from the - execution of that schedule. - """ - return self._metadata - - @metadata.setter - def metadata(self, metadata): - """Update the schedule metadata""" - if not isinstance(metadata, dict) and metadata is not None: - raise TypeError("Only a dictionary or None is accepted for schedule metadata") - self._metadata = metadata or {} - - @property - def alignment_context(self): - """Return alignment instance that allocates block component to generate schedule.""" - return self._alignment_context - - def is_schedulable(self) -> bool: - """Return ``True`` if all durations are assigned.""" - # check context assignment - for context_param in self._alignment_context._context_params: - if isinstance(context_param, ParameterExpression): - return False - - # check duration assignment - for elm in self.blocks: - if isinstance(elm, ScheduleBlock): - if not elm.is_schedulable(): - return False - else: - try: - if not isinstance(elm.duration, int): - return False - except UnassignedReferenceError: - return False - return True - - @property - @_require_schedule_conversion - def duration(self) -> int: - """Duration of this schedule block.""" - return self.duration - - @property - def channels(self) -> tuple[Channel, ...]: - """Returns channels that this schedule block uses.""" - chans: set[Channel] = set() - for elm in self.blocks: - if isinstance(elm, Reference): - raise UnassignedReferenceError( - f"This schedule contains unassigned reference {elm.ref_keys} " - "and channels are ambiguous. Please assign the subroutine first." - ) - chans = chans | set(elm.channels) - return tuple(chans) - - @property - @_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 block elements added to self. - - .. note:: - - The sequence of elements is returned in order of addition. Because the first element is - schedule first, e.g. FIFO, the returned sequence is roughly time-ordered. - However, in the parallel alignment context, especially in - the as-late-as-possible scheduling, or :class:`.AlignRight` context, - the actual timing of when the instructions are issued is unknown until - the :class:`.ScheduleBlock` is scheduled and converted into a :class:`.Schedule`. - """ - blocks = [] - for elm in self._blocks: - if isinstance(elm, Reference): - elm = self.references.get(elm.ref_keys, None) or elm - blocks.append(elm) - return tuple(blocks) - - @property - def parameters(self) -> set[Parameter]: - """Return unassigned parameters with raw names.""" - # Need new object not to mutate parameter_manager.parameters - out_params = set() - - out_params |= self._parameter_manager.parameters - for subroutine in self.references.values(): - if subroutine is None: - continue - out_params |= subroutine.parameters - - return out_params - - @property - def references(self) -> ReferenceManager: - """Return a reference manager of the current scope.""" - if self._parent is not None: - return self._parent.references - return self._reference_manager - - @_require_schedule_conversion - def ch_duration(self, *channels: Channel) -> int: - """Return the time of the end of the last instruction over the supplied channels. - - Args: - *channels: Channels within ``self`` to include. - """ - return self.ch_duration(*channels) - - def append( - self, block: "BlockComponent", name: str | None = None, inplace: bool = True - ) -> "ScheduleBlock": - """Return a new schedule block with ``block`` appended to the context block. - The execution time is automatically assigned when the block is converted into schedule. - - Args: - block: ScheduleBlock to be appended. - name: Name of the new ``Schedule``. Defaults to name of ``self``. - inplace: Perform operation inplace on this schedule. Otherwise, - return a new ``Schedule``. - - Returns: - Schedule block with appended schedule. - - Raises: - PulseError: When invalid schedule type is specified. - """ - if not isinstance(block, (ScheduleBlock, Instruction)): - raise PulseError( - f"Appended `schedule` {block.__class__.__name__} is invalid type. " - "Only `Instruction` and `ScheduleBlock` can be accepted." - ) - - if not inplace: - schedule = copy.deepcopy(self) - schedule._name = name or self.name - schedule.append(block, inplace=True) - return schedule - - if isinstance(block, Reference) and block.ref_keys not in self.references: - self.references[block.ref_keys] = None - - elif isinstance(block, ScheduleBlock): - block = copy.deepcopy(block) - # Expose subroutines to the current main scope. - # Note that this 'block' is not called. - # The block is just directly appended to the current scope. - if block.is_referenced(): - if block._parent is not None: - # This is an edge case: - # If this is not a parent, block.references points to the parent's reference - # where subroutine not referred within the 'block' may exist. - # Move only references existing in the 'block'. - # See 'test.python.pulse.test_reference.TestReference.test_appending_child_block' - for ref in _get_references(block._blocks): - self.references[ref.ref_keys] = block.references[ref.ref_keys] - else: - # Avoid using dict.update and explicitly call __set_item__ for validation. - # Reference manager of appended block is cleared because of data reduction. - for ref_keys, ref in block._reference_manager.items(): - self.references[ref_keys] = ref - block._reference_manager.clear() - # Now switch the parent because block is appended to self. - block._parent = self - - self._blocks.append(block) - self._parameter_manager.update_parameter_table(block) - - return self - - def filter( - self, - *filter_funcs: Callable[..., bool], - channels: Iterable[Channel] | None = None, - instruction_types: Iterable[abc.ABCMeta] | abc.ABCMeta = None, - check_subroutine: bool = True, - ): - """Return a new ``ScheduleBlock`` with only the instructions from this ``ScheduleBlock`` - which pass though the provided filters; i.e. an instruction will be retained if - every function in ``filter_funcs`` returns ``True``, the instruction occurs on - a channel type contained in ``channels``, and the instruction type is contained - in ``instruction_types``. - - .. warning:: - Because ``ScheduleBlock`` is not aware of the execution time of - the context instructions, filtering out some instructions may - change the execution time of the remaining instructions. - - If no arguments are provided, ``self`` is returned. - - Args: - filter_funcs: A list of Callables which take a ``Instruction`` and return a bool. - channels: For example, ``[DriveChannel(0), AcquireChannel(0)]``. - instruction_types: For example, ``[PulseInstruction, AcquireInstruction]``. - check_subroutine: Set `True` to individually filter instructions inside a subroutine - defined by the :py:class:`~qiskit.pulse.instructions.Call` instruction. - - Returns: - ``ScheduleBlock`` consisting of instructions that matches with filtering condition. - """ - from qiskit.pulse.filters import composite_filter, filter_instructions - - filters = composite_filter(channels, instruction_types) - filters.extend(filter_funcs) - - return filter_instructions( - self, filters=filters, negate=False, recurse_subroutines=check_subroutine - ) - - def exclude( - self, - *filter_funcs: Callable[..., bool], - channels: Iterable[Channel] | None = None, - instruction_types: Iterable[abc.ABCMeta] | abc.ABCMeta = None, - check_subroutine: bool = True, - ): - """Return a new ``ScheduleBlock`` with only the instructions from this ``ScheduleBlock`` - *failing* at least one of the provided filters. - This method is the complement of :py:meth:`~ScheduleBlock.filter`, so that:: - - self.filter(args) + self.exclude(args) == self in terms of instructions included. - - .. warning:: - Because ``ScheduleBlock`` is not aware of the execution time of - the context instructions, excluding some instructions may - change the execution time of the remaining instructions. - - Args: - filter_funcs: A list of Callables which take a ``Instruction`` and return a bool. - channels: For example, ``[DriveChannel(0), AcquireChannel(0)]``. - instruction_types: For example, ``[PulseInstruction, AcquireInstruction]``. - check_subroutine: Set `True` to individually filter instructions inside of a subroutine - defined by the :py:class:`~qiskit.pulse.instructions.Call` instruction. - - Returns: - ``ScheduleBlock`` consisting of instructions that do not match with - at least one of filtering conditions. - """ - from qiskit.pulse.filters import composite_filter, filter_instructions - - filters = composite_filter(channels, instruction_types) - filters.extend(filter_funcs) - - return filter_instructions( - self, filters=filters, negate=True, recurse_subroutines=check_subroutine - ) - - def replace( - self, - old: "BlockComponent", - new: "BlockComponent", - inplace: bool = True, - ) -> "ScheduleBlock": - """Return a ``ScheduleBlock`` with the ``old`` component replaced with a ``new`` - component. - - Args: - old: Schedule block component to replace. - new: Schedule block component to replace with. - inplace: Replace instruction by mutably modifying this ``ScheduleBlock``. - - Returns: - The modified schedule block with ``old`` replaced by ``new``. - """ - if not inplace: - schedule = copy.deepcopy(self) - return schedule.replace(old, new, inplace=True) - - if old not in self._blocks: - # Avoid unnecessary update of reference and parameter manager - return self - - # Temporarily copies references - all_references = ReferenceManager() - if isinstance(new, ScheduleBlock): - new = copy.deepcopy(new) - all_references.update(new.references) - new._reference_manager.clear() - new._parent = self - for ref_key, subroutine in self.references.items(): - if ref_key in all_references: - warnings.warn( - f"Reference {ref_key} conflicts with substituted program {new.name}. " - "Existing reference has been replaced with new reference.", - UserWarning, - ) - continue - all_references[ref_key] = subroutine - - # Regenerate parameter table by regenerating elements. - # Note that removal of parameters in old is not sufficient, - # because corresponding parameters might be also used in another block element. - self._parameter_manager.clear() - self._parameter_manager.update_parameter_table(self._alignment_context) - - new_elms = [] - for elm in self._blocks: - if elm == old: - elm = new - self._parameter_manager.update_parameter_table(elm) - new_elms.append(elm) - self._blocks = new_elms - - # Regenerate reference table - # Note that reference is attached to the outer schedule if nested. - # Thus, this investigates all references within the scope. - self.references.clear() - root = self - while root._parent is not None: - root = root._parent - for ref in _get_references(root._blocks): - self.references[ref.ref_keys] = all_references[ref.ref_keys] - - return self - - def is_parameterized(self) -> bool: - """Return True iff the instruction is parameterized.""" - return any(self.parameters) - - def is_referenced(self) -> bool: - """Return True iff the current schedule block contains reference to subroutine.""" - return len(self.references) > 0 - - def assign_parameters( - self, - value_dict: dict[ - ParameterExpression | ParameterVector | str, - ParameterValueType | Sequence[ParameterValueType], - ], - inplace: bool = True, - ) -> "ScheduleBlock": - """Assign the parameters in this schedule according to the input. - - Args: - value_dict: A mapping from parameters or parameter names (parameter vector - or parameter vector name) to either numeric values (list of numeric values) - or another parameter expression (list of parameter expressions). - inplace: Set ``True`` to override this instance with new parameter. - - Returns: - Schedule with updated parameters. - - Raises: - PulseError: When the block is nested into another block. - """ - if not inplace: - new_schedule = copy.deepcopy(self) - return new_schedule.assign_parameters(value_dict, inplace=True) - - # Update parameters in the current scope - self._parameter_manager.assign_parameters(pulse_program=self, value_dict=value_dict) - - for subroutine in self._reference_manager.values(): - # Also assigning parameters to the references associated with self. - # Note that references are always stored in the root program. - # So calling assign_parameters from nested block doesn't update references. - if subroutine is None: - continue - subroutine.assign_parameters(value_dict=value_dict, inplace=True) - - return self - - def assign_references( - self, - subroutine_dict: dict[str | tuple[str, ...], "ScheduleBlock"], - inplace: bool = True, - ) -> "ScheduleBlock": - """Assign schedules to references. - - It is only capable of assigning a schedule block to immediate references - which are directly referred within the current scope. - Let's see following example: - - .. plot:: - :include-source: - :nofigs: - :context: reset - - from qiskit import pulse - - with pulse.build() as nested_prog: - pulse.delay(10, pulse.DriveChannel(0)) - - with pulse.build() as sub_prog: - pulse.reference("A") - - with pulse.build() as main_prog: - pulse.reference("B") - - In above example, the ``main_prog`` can refer to the subroutine "root::B" and the - reference of "B" to program "A", i.e., "B::A", is not defined in the root namespace. - This prevents breaking the reference "root::B::A" by the assignment of "root::B". - For example, if a user could indirectly assign "root::B::A" from the root program, - one can later assign another program to "root::B" that doesn't contain "A" within it. - In this situation, a reference "root::B::A" would still live in - the reference manager of the root. - However, the subroutine "root::B::A" would no longer be used in the actual pulse program. - To assign subroutine "A" to ``nested_prog`` as a nested subprogram of ``main_prog``, - you must first assign "A" of the ``sub_prog``, - and then assign the ``sub_prog`` to the ``main_prog``. - - .. plot:: - :include-source: - :nofigs: - :context: - - sub_prog.assign_references({("A", ): nested_prog}, inplace=True) - main_prog.assign_references({("B", ): sub_prog}, inplace=True) - - Alternatively, you can also write - - .. plot:: - :nofigs: - :context: reset - - # This code is hidden from readers - # It resets the variables so the following code example runs correctly - from qiskit import pulse - with pulse.build() as nested_prog: - pulse.delay(10, pulse.DriveChannel(0)) - with pulse.build() as sub_prog: - pulse.reference("A") - with pulse.build() as main_prog: - pulse.reference("B") - - - .. plot:: - :include-source: - :nofigs: - :context: - - main_prog.assign_references({("B", ): sub_prog}, inplace=True) - main_prog.references[("B", )].assign_references({("A", ): nested_prog}, inplace=True) - - Here :attr:`.references` returns a dict-like object, and you can - mutably update the nested reference of the particular subroutine. - - .. note:: - - Assigned programs are deep-copied to prevent an unexpected update. - - Args: - subroutine_dict: A mapping from reference key to schedule block of the subroutine. - inplace: Set ``True`` to override this instance with new subroutine. - - Returns: - Schedule block with assigned subroutine. - - Raises: - PulseError: When reference key is not defined in the current scope. - """ - if not inplace: - new_schedule = copy.deepcopy(self) - return new_schedule.assign_references(subroutine_dict, inplace=True) - - for key, subroutine in subroutine_dict.items(): - if key not in self.references: - unassigned_keys = ", ".join(map(repr, self.references.unassigned())) - raise PulseError( - f"Reference instruction with {key} doesn't exist " - f"in the current scope: {unassigned_keys}" - ) - self.references[key] = copy.deepcopy(subroutine) - - return self - - def get_parameters(self, parameter_name: str) -> list[Parameter]: - """Get parameter object bound to this schedule by string name. - - Note that we can define different parameter objects with the same name, - because these different objects are identified by their unique uuid. - For example, - - .. plot:: - :include-source: - :nofigs: - - from qiskit import pulse, circuit - - amp1 = circuit.Parameter("amp") - amp2 = circuit.Parameter("amp") - - with pulse.build() as sub_prog: - pulse.play(pulse.Constant(100, amp1), pulse.DriveChannel(0)) - - with pulse.build() as main_prog: - pulse.call(sub_prog, name="sub") - pulse.play(pulse.Constant(100, amp2), pulse.DriveChannel(0)) - - main_prog.get_parameters("amp") - - This returns a list of two parameters ``amp1`` and ``amp2``. - - Args: - parameter_name: Name of parameter. - - Returns: - Parameter objects that have corresponding name. - """ - matched = [p for p in self.parameters if p.name == parameter_name] - return matched - - def __len__(self) -> int: - """Return number of instructions in the schedule.""" - return len(self.blocks) - - def __eq__(self, other: object) -> bool: - """Test if two ScheduleBlocks are equal. - - Equality is checked by verifying there is an equal instruction at every time - in ``other`` for every instruction in this ``ScheduleBlock``. This check is - performed by converting the instruction representation into directed acyclic graph, - in which execution order of every instruction is evaluated correctly across all channels. - Also ``self`` and ``other`` should have the same alignment context. - - .. warning:: - - This does not check for logical equivalency. Ie., - - ```python - >>> Delay(10, DriveChannel(0)) + Delay(10, DriveChannel(0)) - == Delay(20, DriveChannel(0)) - False - ``` - """ - # 0. type check - if not isinstance(other, type(self)): - return False - - # 1. transformation check - if self.alignment_context != other.alignment_context: - return False - - # 2. size check - if len(self) != len(other): - return False - - # 3. instruction check with alignment - from qiskit.pulse.transforms.dag import block_to_dag as dag - - if not rx.is_isomorphic_node_match(dag(self), dag(other), lambda x, y: x == y): - return False - - return True - - def __repr__(self) -> str: - name = format(self._name) if self._name else "" - blocks = ", ".join([repr(instr) for instr in self.blocks[:50]]) - if len(self.blocks) > 25: - blocks += ", ..." - return ( - f'{self.__class__.__name__}({blocks}, name="{name}",' - f" transform={repr(self.alignment_context)})" - ) - - def __add__(self, other: "BlockComponent") -> "ScheduleBlock": - """Return a new schedule with ``other`` inserted within ``self`` at ``start_time``.""" - return self.append(other) - - -def _common_method(*classes): - """A function decorator to attach the function to specified classes as a method. - - .. note:: For developer: A method attached through this decorator may hurt readability - of the codebase, because the method may not be detected by a code editor. - Thus, this decorator should be used to a limited extent, i.e. huge helper method. - By using this decorator wisely, we can reduce code maintenance overhead without - losing readability of the codebase. - """ - - def decorator(method): - @functools.wraps(method) - def wrapper(*args, **kwargs): - return method(*args, **kwargs) - - for cls in classes: - setattr(cls, method.__name__, wrapper) - return method - - return decorator - - -@deprecate_arg("show_barriers", new_alias="plot_barriers", since="1.1.0", pending=True) -@_common_method(Schedule, ScheduleBlock) -def draw( - self, - style: dict[str, Any] | None = None, - backend=None, # importing backend causes cyclic import - time_range: tuple[int, int] | None = None, - time_unit: str = "dt", - disable_channels: list[Channel] | None = None, - show_snapshot: bool = True, - show_framechange: bool = True, - show_waveform_info: bool = True, - plot_barrier: bool = True, - plotter: str = "mpl2d", - axis: Any | None = None, - show_barrier: bool = True, -): - """Plot the schedule. - - Args: - style: Stylesheet options. This can be dictionary or preset stylesheet classes. See - :py:class:`~qiskit.visualization.pulse_v2.stylesheets.IQXStandard`, - :py:class:`~qiskit.visualization.pulse_v2.stylesheets.IQXSimple`, and - :py:class:`~qiskit.visualization.pulse_v2.stylesheets.IQXDebugging` for details of - preset stylesheets. - backend (Optional[BaseBackend]): Backend object to play the input pulse program. - If provided, the plotter may use to make the visualization hardware aware. - time_range: Set horizontal axis limit. Tuple ``(tmin, tmax)``. - time_unit: The unit of specified time range either ``dt`` or ``ns``. - The unit of `ns` is available only when ``backend`` object is provided. - disable_channels: A control property to show specific pulse channel. - Pulse channel instances provided as a list are not shown in the output image. - show_snapshot: Show snapshot instructions. - show_framechange: Show frame change instructions. The frame change represents - instructions that modulate phase or frequency of pulse channels. - show_waveform_info: Show additional information about waveforms such as their name. - plot_barrier: Show barrier lines. - plotter: Name of plotter API to generate an output image. - One of following APIs should be specified:: - - mpl2d: Matplotlib API for 2D image generation. - Matplotlib API to generate 2D image. Charts are placed along y axis with - vertical offset. This API takes matplotlib.axes.Axes as ``axis`` input. - - ``axis`` and ``style`` kwargs may depend on the plotter. - axis: Arbitrary object passed to the plotter. If this object is provided, - the plotters use a given ``axis`` instead of internally initializing - a figure object. This object format depends on the plotter. - See plotter argument for details. - show_barrier: DEPRECATED. Show barrier lines. - - Returns: - Visualization output data. - The returned data type depends on the ``plotter``. - If matplotlib family is specified, this will be a ``matplotlib.pyplot.Figure`` data. - """ - # pylint: disable=cyclic-import - from qiskit.visualization import pulse_drawer - - del show_barrier - return pulse_drawer( - program=self, - style=style, - backend=backend, - time_range=time_range, - time_unit=time_unit, - disable_channels=disable_channels, - show_snapshot=show_snapshot, - show_framechange=show_framechange, - show_waveform_info=show_waveform_info, - plot_barrier=plot_barrier, - plotter=plotter, - axis=axis, - ) - - -def _interval_index(intervals: list[Interval], interval: Interval) -> int: - """Find the index of an interval. - - Args: - intervals: A sorted list of non-overlapping Intervals. - interval: The interval for which the index into intervals will be found. - - Returns: - The index of the interval. - - Raises: - PulseError: If the interval does not exist. - """ - index = _locate_interval_index(intervals, interval) - found_interval = intervals[index] - if found_interval != interval: - raise PulseError(f"The interval: {interval} does not exist in intervals: {intervals}") - return index - - -def _locate_interval_index(intervals: list[Interval], interval: Interval, index: int = 0) -> int: - """Using binary search on start times, find an interval. - - Args: - intervals: A sorted list of non-overlapping Intervals. - interval: The interval for which the index into intervals will be found. - index: A running tally of the index, for recursion. The user should not pass a value. - - Returns: - The index into intervals that new_interval would be inserted to maintain - a sorted list of intervals. - """ - if not intervals or len(intervals) == 1: - return index - - mid_idx = len(intervals) // 2 - mid = intervals[mid_idx] - if interval[1] <= mid[0] and (interval != mid): - return _locate_interval_index(intervals[:mid_idx], interval, index=index) - else: - return _locate_interval_index(intervals[mid_idx:], interval, index=index + mid_idx) - - -def _find_insertion_index(intervals: list[Interval], new_interval: Interval) -> int: - """Using binary search on start times, return the index into `intervals` where the new interval - belongs, or raise an error if the new interval overlaps with any existing ones. - Args: - intervals: A sorted list of non-overlapping Intervals. - new_interval: The interval for which the index into intervals will be found. - Returns: - The index into intervals that new_interval should be inserted to maintain a sorted list - of intervals. - Raises: - PulseError: If new_interval overlaps with the given intervals. - """ - index = _locate_interval_index(intervals, new_interval) - if index < len(intervals): - if _overlaps(intervals[index], new_interval): - raise PulseError("New interval overlaps with existing.") - return index if new_interval[1] <= intervals[index][0] else index + 1 - return index - - -def _overlaps(first: Interval, second: Interval) -> bool: - """Return True iff first and second overlap. - Note: first.stop may equal second.start, since Interval stop times are exclusive. - """ - if first[0] == second[0] == second[1]: - # They fail to overlap if one of the intervals has duration 0 - return False - if first[0] > second[0]: - first, second = second, first - return second[0] < first[1] - - -def _check_nonnegative_timeslot(timeslots: TimeSlots): - """Test that a channel has no negative timeslots. - - Raises: - PulseError: If a channel timeslot is negative. - """ - for chan, chan_timeslots in timeslots.items(): - if chan_timeslots: - if chan_timeslots[0][0] < 0: - raise PulseError(f"An instruction on {chan} has a negative starting time.") - - -def _get_timeslots(schedule: "ScheduleComponent") -> TimeSlots: - """Generate timeslots from given schedule component. - - Args: - schedule: Input schedule component. - - Raises: - PulseError: When invalid schedule type is specified. - """ - if isinstance(schedule, Instruction): - duration = schedule.duration - instruction_duration_validation(duration) - timeslots = {channel: [(0, duration)] for channel in schedule.channels} - elif isinstance(schedule, Schedule): - timeslots = schedule.timeslots - else: - raise PulseError(f"Invalid schedule type {type(schedule)} is specified.") - - return timeslots - - -def _get_references(block_elms: list["BlockComponent"]) -> set[Reference]: - """Recursively get reference instructions in the current scope. - - Args: - block_elms: List of schedule block elements to investigate. - - Returns: - A set of unique reference instructions. - """ - references = set() - for elm in block_elms: - if isinstance(elm, ScheduleBlock): - references |= _get_references(elm._blocks) - elif isinstance(elm, Reference): - references.add(elm) - return references - - -# These type aliases are defined at the bottom of the file, because as of 2022-01-18 they are -# imported into other parts of Terra. Previously, the aliases were at the top of the file and used -# forwards references within themselves. This was fine within the same file, but causes scoping -# issues when the aliases are imported into different scopes, in which the `ForwardRef` instances -# would no longer resolve. Instead, we only use forward references in the annotations of _this_ -# file to reference the aliases, which are guaranteed to resolve in scope, so the aliases can all be -# concrete. - -ScheduleComponent = Union[Schedule, Instruction] -"""An element that composes a pulse schedule.""" - -BlockComponent = Union[ScheduleBlock, Instruction] -"""An element that composes a pulse schedule block.""" diff --git a/qiskit/pulse/transforms/__init__.py b/qiskit/pulse/transforms/__init__.py deleted file mode 100644 index 4fb4bf40e9ef..000000000000 --- a/qiskit/pulse/transforms/__init__.py +++ /dev/null @@ -1,106 +0,0 @@ -# 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. -r""" -================================================= -Pulse Transforms (:mod:`qiskit.pulse.transforms`) -================================================= - -The pulse transforms provide transformation routines to reallocate and optimize -pulse programs for backends. - -.. _pulse_alignments: - -Alignments -========== - -The alignment transforms define alignment policies of instructions in :obj:`.ScheduleBlock`. -These transformations are called to create :obj:`.Schedule`\ s from :obj:`.ScheduleBlock`\ s. - -.. autosummary:: - :toctree: ../stubs/ - - AlignEquispaced - AlignFunc - AlignLeft - AlignRight - AlignSequential - -These are all subtypes of the abstract base class :class:`AlignmentKind`. - -.. autoclass:: AlignmentKind - - -.. _pulse_canonical_transform: - -Canonicalization -================ - -The canonicalization transforms convert schedules to a form amenable for execution on -OpenPulse backends. - -.. autofunction:: add_implicit_acquires -.. autofunction:: align_measures -.. autofunction:: block_to_schedule -.. autofunction:: compress_pulses -.. autofunction:: flatten -.. autofunction:: inline_subroutines -.. autofunction:: pad -.. autofunction:: remove_directives -.. autofunction:: remove_trivial_barriers - - -.. _pulse_dag: - -DAG -=== - -The DAG transforms create DAG representation of input program. This can be used for -optimization of instructions and equality checks. - -.. autofunction:: block_to_dag - - -.. _pulse_transform_chain: - -Composite transform -=================== - -A sequence of transformations to generate a target code. - -.. autofunction:: target_qobj_transform - -""" - -from .alignments import ( - AlignEquispaced, - AlignFunc, - AlignLeft, - AlignRight, - AlignSequential, - AlignmentKind, -) - -from .base_transforms import target_qobj_transform - -from .canonicalization import ( - add_implicit_acquires, - align_measures, - block_to_schedule, - compress_pulses, - flatten, - inline_subroutines, - pad, - remove_directives, - remove_trivial_barriers, -) - -from .dag import block_to_dag diff --git a/qiskit/pulse/transforms/alignments.py b/qiskit/pulse/transforms/alignments.py deleted file mode 100644 index b0983ea80c4e..000000000000 --- a/qiskit/pulse/transforms/alignments.py +++ /dev/null @@ -1,408 +0,0 @@ -# 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 passes to reallocate the timeslots of instructions according to context.""" -from __future__ import annotations -import abc -from typing import Callable, Tuple - -import numpy as np - -from qiskit.circuit.parameterexpression import ParameterExpression, ParameterValueType -from qiskit.pulse.exceptions import PulseError -from qiskit.pulse.schedule import Schedule, ScheduleComponent -from qiskit.pulse.utils import instruction_duration_validation - - -class AlignmentKind(abc.ABC): - """An abstract class for schedule alignment.""" - - def __init__(self, context_params: Tuple[ParameterValueType, ...]): - """Create new context.""" - self._context_params = tuple(context_params) - - @abc.abstractmethod - def align(self, schedule: Schedule) -> Schedule: - """Reallocate instructions according to the policy. - - Only top-level sub-schedules are aligned. If sub-schedules are nested, - nested schedules are not recursively aligned. - - Args: - schedule: Schedule to align. - - Returns: - Schedule with reallocated instructions. - """ - pass - - @property - @abc.abstractmethod - def is_sequential(self) -> bool: - """Return ``True`` if this is sequential alignment context. - - This information is used to evaluate DAG equivalency of two :class:`.ScheduleBlock`s. - When the context has two pulses in different channels, - a sequential context subtype intends to return following scheduling outcome. - - .. code-block:: text - - ┌────────┐ - D0: ┤ pulse1 ├──────────── - └────────┘ ┌────────┐ - D1: ────────────┤ pulse2 ├ - └────────┘ - - On the other hand, parallel context with ``is_sequential=False`` returns - - .. code-block:: text - - ┌────────┐ - D0: ┤ pulse1 ├ - ├────────┤ - D1: ┤ pulse2 ├ - └────────┘ - - All subclasses must implement this method according to scheduling strategy. - """ - pass - - def __eq__(self, other: object) -> bool: - """Check equality of two transforms.""" - if type(self) is not type(other): - return False - if self._context_params != other._context_params: - return False - return True - - def __repr__(self): - return f"{self.__class__.__name__}({', '.join(self._context_params)})" - - -class AlignLeft(AlignmentKind): - """Align instructions in as-soon-as-possible manner. - - Instructions are placed at earliest available timeslots. - """ - - def __init__(self): - """Create new left-justified context.""" - super().__init__(context_params=()) - - @property - def is_sequential(self) -> bool: - return False - - def align(self, schedule: Schedule) -> Schedule: - """Reallocate instructions according to the policy. - - Only top-level sub-schedules are aligned. If sub-schedules are nested, - nested schedules are not recursively aligned. - - Args: - schedule: Schedule to align. - - Returns: - Schedule with reallocated instructions. - """ - aligned = Schedule.initialize_from(schedule) - for _, child in schedule.children: - self._push_left_append(aligned, child) - - return aligned - - @staticmethod - def _push_left_append(this: Schedule, other: ScheduleComponent) -> Schedule: - """Return ``this`` with ``other`` inserted at the maximum time over - all channels shared between ```this`` and ``other``. - - Args: - this: Input schedule to which ``other`` will be inserted. - other: Other schedule to insert. - - Returns: - Push left appended schedule. - """ - this_channels = set(this.channels) - other_channels = set(other.channels) - shared_channels = list(this_channels & other_channels) - ch_slacks = [ - this.stop_time - this.ch_stop_time(channel) + other.ch_start_time(channel) - for channel in shared_channels - ] - - if ch_slacks: - slack_chan = shared_channels[np.argmin(ch_slacks)] - shared_insert_time = this.ch_stop_time(slack_chan) - other.ch_start_time(slack_chan) - else: - shared_insert_time = 0 - - # Handle case where channels not common to both might actually start - # after ``this`` has finished. - 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) - - -class AlignRight(AlignmentKind): - """Align instructions in as-late-as-possible manner. - - Instructions are placed at latest available timeslots. - """ - - def __init__(self): - """Create new right-justified context.""" - super().__init__(context_params=()) - - @property - def is_sequential(self) -> bool: - return False - - def align(self, schedule: Schedule) -> Schedule: - """Reallocate instructions according to the policy. - - Only top-level sub-schedules are aligned. If sub-schedules are nested, - nested schedules are not recursively aligned. - - Args: - schedule: Schedule to align. - - Returns: - Schedule with reallocated instructions. - """ - aligned = Schedule.initialize_from(schedule) - for _, child in reversed(schedule.children): - aligned = self._push_right_prepend(aligned, child) - - return aligned - - @staticmethod - def _push_right_prepend(this: Schedule, other: ScheduleComponent) -> Schedule: - """Return ``this`` with ``other`` inserted at the latest possible time - such that ``other`` ends before it overlaps with any of ``this``. - - If required ``this`` is shifted to start late enough so that there is room - to insert ``other``. - - Args: - this: Input schedule to which ``other`` will be inserted. - other: Other schedule to insert. - - Returns: - Push right prepended schedule. - """ - this_channels = set(this.channels) - other_channels = set(other.channels) - shared_channels = list(this_channels & other_channels) - ch_slacks = [ - this.ch_start_time(channel) - other.ch_stop_time(channel) for channel in shared_channels - ] - - if ch_slacks: - insert_time = min(ch_slacks) + other.start_time - else: - insert_time = this.stop_time - other.stop_time + other.start_time - - if insert_time < 0: - this.shift(-insert_time, inplace=True) - this.insert(0, other, inplace=True) - else: - this.insert(insert_time, other, inplace=True) - - return this - - -class AlignSequential(AlignmentKind): - """Align instructions sequentially. - - Instructions played on different channels are also arranged in a sequence. - No buffer time is inserted in between instructions. - """ - - def __init__(self): - """Create new sequential context.""" - super().__init__(context_params=()) - - @property - def is_sequential(self) -> bool: - return True - - def align(self, schedule: Schedule) -> Schedule: - """Reallocate instructions according to the policy. - - Only top-level sub-schedules are aligned. If sub-schedules are nested, - nested schedules are not recursively aligned. - - Args: - schedule: Schedule to align. - - Returns: - Schedule with reallocated instructions. - """ - aligned = Schedule.initialize_from(schedule) - for _, child in schedule.children: - aligned.insert(aligned.duration, child, inplace=True) - - return aligned - - -class AlignEquispaced(AlignmentKind): - """Align instructions with equispaced interval within a specified duration. - - Instructions played on different channels are also arranged in a sequence. - This alignment is convenient to create dynamical decoupling sequences such as PDD. - """ - - def __init__(self, duration: int | ParameterExpression): - """Create new equispaced context. - - Args: - duration: Duration of this context. This should be larger than the schedule duration. - If the specified duration is shorter than the schedule duration, - no alignment is performed and the input schedule is just returned. - This duration can be parametrized. - """ - super().__init__(context_params=(duration,)) - - @property - def is_sequential(self) -> bool: - return True - - @property - def duration(self): - """Return context duration.""" - return self._context_params[0] - - def align(self, schedule: Schedule) -> Schedule: - """Reallocate instructions according to the policy. - - Only top-level sub-schedules are aligned. If sub-schedules are nested, - nested schedules are not recursively aligned. - - Args: - schedule: Schedule to align. - - Returns: - Schedule with reallocated instructions. - """ - instruction_duration_validation(self.duration) - - total_duration = sum(child.duration for _, child in schedule.children) - if self.duration < total_duration: - return schedule - - total_delay = self.duration - total_duration - - if len(schedule.children) > 1: - # Calculate the interval in between sub-schedules. - # If the duration cannot be divided by the number of sub-schedules, - # the modulo is appended and prepended to the input schedule. - interval, mod = np.divmod(total_delay, len(schedule.children) - 1) - else: - interval = 0 - mod = total_delay - - # Calculate pre schedule delay - delay, mod = np.divmod(mod, 2) - - aligned = Schedule.initialize_from(schedule) - # Insert sub-schedules with interval - _t0 = int(aligned.stop_time + delay + mod) - for _, child in schedule.children: - aligned.insert(_t0, child, inplace=True) - _t0 = int(aligned.stop_time + interval) - - return aligned - - -class AlignFunc(AlignmentKind): - """Allocate instructions at position specified by callback function. - - The position is specified for each instruction of index ``j`` as a - fractional coordinate in [0, 1] within the specified duration. - - Instructions played on different channels are also arranged in a sequence. - This alignment is convenient to create dynamical decoupling sequences such as UDD. - - For example, UDD sequence with 10 pulses can be specified with following function. - - .. plot:: - :include-source: - :nofigs: - - import numpy as np - - def udd10_pos(j): - return np.sin(np.pi*j/(2*10 + 2))**2 - - .. note:: - - This context cannot be QPY serialized because of the callable. If you use this context, - your program cannot be saved in QPY format. - - """ - - def __init__(self, duration: int | ParameterExpression, func: Callable): - """Create new equispaced context. - - Args: - duration: Duration of this context. This should be larger than the schedule duration. - If the specified duration is shorter than the schedule duration, - no alignment is performed and the input schedule is just returned. - This duration can be parametrized. - func: A function that takes an index of sub-schedule and returns the - fractional coordinate of of that sub-schedule. The returned value should be - defined within [0, 1]. The pulse index starts from 1. - """ - super().__init__(context_params=(duration, func)) - - @property - def is_sequential(self) -> bool: - return True - - @property - def duration(self): - """Return context duration.""" - return self._context_params[0] - - @property - def func(self): - """Return context alignment function.""" - return self._context_params[1] - - def align(self, schedule: Schedule) -> Schedule: - """Reallocate instructions according to the policy. - - Only top-level sub-schedules are aligned. If sub-schedules are nested, - nested schedules are not recursively aligned. - - Args: - schedule: Schedule to align. - - Returns: - Schedule with reallocated instructions. - """ - instruction_duration_validation(self.duration) - - if self.duration < schedule.duration: - return schedule - - aligned = Schedule.initialize_from(schedule) - for ind, (_, child) in enumerate(schedule.children): - _t_center = self.duration * self.func(ind + 1) - _t0 = int(_t_center - 0.5 * child.duration) - if _t0 < 0 or _t0 > self.duration: - raise PulseError(f"Invalid schedule position t={_t0} is specified at index={ind}") - aligned.insert(_t0, child, inplace=True) - - return aligned diff --git a/qiskit/pulse/transforms/base_transforms.py b/qiskit/pulse/transforms/base_transforms.py deleted file mode 100644 index 011c050f578a..000000000000 --- a/qiskit/pulse/transforms/base_transforms.py +++ /dev/null @@ -1,71 +0,0 @@ -# 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 deleted file mode 100644 index 6d4c3cb3af7b..000000000000 --- a/qiskit/pulse/transforms/canonicalization.py +++ /dev/null @@ -1,504 +0,0 @@ -# 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. -"""Basic rescheduling functions which take schedule or instructions and return new schedules.""" -from __future__ import annotations -import typing -import warnings -from collections import defaultdict -from collections.abc import Iterable -from typing import Type - -import numpy as np - -from qiskit.pulse import channels as chans, exceptions, instructions -from qiskit.pulse.channels import ClassicalIOChannel -from qiskit.pulse.exceptions import PulseError -from qiskit.pulse.exceptions import UnassignedDurationError -from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap -from qiskit.pulse.instructions import directives -from qiskit.pulse.schedule import Schedule, ScheduleBlock, ScheduleComponent - -if typing.TYPE_CHECKING: - from qiskit.pulse.library import Pulse # pylint: disable=cyclic-import - - -def block_to_schedule(block: ScheduleBlock) -> Schedule: - """Convert ``ScheduleBlock`` to ``Schedule``. - - Args: - block: A ``ScheduleBlock`` to convert. - - Returns: - Scheduled pulse program. - - 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( - "All instruction durations should be assigned before creating `Schedule`." - "Please check `.parameters` to find unassigned parameter objects." - ) - - schedule = Schedule.initialize_from(block) - - 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) - - # transform with defined policy - return block.alignment_context.align(schedule) - - -def compress_pulses(schedules: list[Schedule]) -> list[Schedule]: - """Optimization pass to replace identical pulses. - - Args: - schedules: Schedules to compress. - - Returns: - Compressed schedules. - """ - existing_pulses: list[Pulse] = [] - new_schedules = [] - - for schedule in schedules: - new_schedule = Schedule.initialize_from(schedule) - - for time, inst in schedule.instructions: - if isinstance(inst, instructions.Play): - if inst.pulse in existing_pulses: - idx = existing_pulses.index(inst.pulse) - identical_pulse = existing_pulses[idx] - new_schedule.insert( - time, - instructions.Play(identical_pulse, inst.channel, inst.name), - inplace=True, - ) - else: - existing_pulses.append(inst.pulse) - new_schedule.insert(time, inst, inplace=True) - else: - new_schedule.insert(time, inst, inplace=True) - - new_schedules.append(new_schedule) - - return new_schedules - - -def flatten(program: Schedule) -> Schedule: - """Flatten (inline) any called nodes into a Schedule tree with no nested children. - - Args: - program: Pulse program to remove nested structure. - - Returns: - Flatten pulse program. - - Raises: - PulseError: When invalid data format is given. - """ - if isinstance(program, Schedule): - flat_sched = Schedule.initialize_from(program) - for time, inst in program.instructions: - flat_sched.insert(time, inst, inplace=True) - return flat_sched - else: - raise PulseError(f"Invalid input program {program.__class__.__name__} is specified.") - - -def inline_subroutines(program: Schedule | ScheduleBlock) -> Schedule | ScheduleBlock: - """Recursively remove call instructions and inline the respective subroutine instructions. - - Assigned parameter values, which are stored in the parameter table, are also applied. - The subroutine is copied before the parameter assignment to avoid mutation problem. - - Args: - program: A program which may contain the subroutine, i.e. ``Call`` instruction. - - Returns: - A schedule without subroutine. - - Raises: - PulseError: When input program is not valid data format. - """ - if isinstance(program, Schedule): - return _inline_schedule(program) - elif isinstance(program, ScheduleBlock): - return _inline_block(program) - else: - raise PulseError(f"Invalid program {program.__class__.__name__} is specified.") - - -def _inline_schedule(schedule: Schedule) -> Schedule: - """A helper function to inline subroutine of schedule. - - .. note:: If subroutine is ``ScheduleBlock`` it is converted into Schedule to get ``t0``. - """ - ret_schedule = Schedule.initialize_from(schedule) - for t0, inst in schedule.children: - # note that schedule.instructions unintentionally flatten the nested schedule. - # this should be performed by another transformer node. - if isinstance(inst, Schedule): - # recursively inline the program - inline_schedule = _inline_schedule(inst) - ret_schedule.insert(t0, inline_schedule, inplace=True) - else: - ret_schedule.insert(t0, inst, inplace=True) - return ret_schedule - - -def _inline_block(block: ScheduleBlock) -> ScheduleBlock: - """A helper function to inline subroutine of schedule block. - - .. note:: If subroutine is ``Schedule`` the function raises an error. - """ - ret_block = ScheduleBlock.initialize_from(block) - for inst in block.blocks: - if isinstance(inst, ScheduleBlock): - # recursively inline the program - inline_block = _inline_block(inst) - ret_block.append(inline_block, inplace=True) - else: - ret_block.append(inst, inplace=True) - return ret_block - - -def remove_directives(schedule: Schedule) -> Schedule: - """Remove directives. - - Args: - schedule: A schedule to remove compiler directives. - - Returns: - A schedule without directives. - """ - return schedule.exclude(instruction_types=[directives.Directive]) - - -def remove_trivial_barriers(schedule: Schedule) -> Schedule: - """Remove trivial barriers with 0 or 1 channels. - - Args: - schedule: A schedule to remove trivial barriers. - - Returns: - schedule: A schedule without trivial barriers - """ - - def filter_func(inst): - return isinstance(inst[1], directives.RelativeBarrier) and len(inst[1].channels) < 2 - - return schedule.exclude(filter_func) - - -def align_measures( - schedules: Iterable[ScheduleComponent], - inst_map: InstructionScheduleMap | None = None, - cal_gate: str = "u3", - max_calibration_duration: int | None = None, - align_time: int | None = None, - align_all: bool | None = True, -) -> list[Schedule]: - """Return new schedules where measurements occur at the same physical time. - - This transformation will align the first :class:`.Acquire` on - every channel to occur at the same time. - - Minimum measurement wait time (to allow for calibration pulses) is enforced - and may be set with ``max_calibration_duration``. - - By default only instructions containing a :class:`.AcquireChannel` or :class:`.MeasureChannel` - will be shifted. If you wish to keep the relative timing of all instructions in the schedule set - ``align_all=True``. - - This method assumes that ``MeasureChannel(i)`` and ``AcquireChannel(i)`` - correspond to the same qubit and the acquire/play instructions - should be shifted together on these channels. - - .. plot:: - :include-source: - :nofigs: - :context: reset - - from qiskit import pulse - from qiskit.pulse import transforms - - 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 - - aligned_sched, aligned_sched_shifted = transforms.align_measures([sched, sched_shifted]) - - assert aligned_sched == aligned_sched_shifted - - If it is desired to only shift acquisition and measurement stimulus instructions - set the flag ``align_all=False``: - - .. plot:: - :include-source: - :nofigs: - :context: - - aligned_sched, aligned_sched_shifted = transforms.align_measures( - [sched, sched_shifted], - align_all=False, - ) - - assert aligned_sched != aligned_sched_shifted - - - Args: - schedules: Collection of schedules to be aligned together - inst_map: Mapping of circuit operations to pulse schedules - cal_gate: The name of the gate to inspect for the calibration time - max_calibration_duration: If provided, inst_map and cal_gate will be ignored - align_time: If provided, this will be used as final align time. - align_all: Shift all instructions in the schedule such that they maintain - their relative alignment with the shifted acquisition instruction. - If ``False`` only the acquisition and measurement pulse instructions - will be shifted. - Returns: - The input list of schedules transformed to have their measurements aligned. - - Raises: - PulseError: If the provided alignment time is negative. - """ - - def get_first_acquire_times(schedules): - """Return a list of first acquire times for each schedule.""" - acquire_times = [] - for schedule in schedules: - visited_channels = set() - qubit_first_acquire_times: dict[int, int] = defaultdict(lambda: None) - - for time, inst in schedule.instructions: - if isinstance(inst, instructions.Acquire) and inst.channel not in visited_channels: - visited_channels.add(inst.channel) - qubit_first_acquire_times[inst.channel.index] = time - - acquire_times.append(qubit_first_acquire_times) - return acquire_times - - def get_max_calibration_duration(inst_map, cal_gate): - """Return the time needed to allow for readout discrimination calibration pulses.""" - # TODO (qiskit-terra #5472): fix behavior of this. - max_calibration_duration = 0 - for qubits in inst_map.qubits_with_instruction(cal_gate): - cmd = inst_map.get(cal_gate, qubits, np.pi, 0, np.pi) - max_calibration_duration = max(cmd.duration, max_calibration_duration) - return max_calibration_duration - - if align_time is not None and align_time < 0: - raise exceptions.PulseError("Align time cannot be negative.") - - first_acquire_times = get_first_acquire_times(schedules) - # Extract the maximum acquire in every schedule across all acquires in the schedule. - # If there are no acquires in the schedule default to 0. - max_acquire_times = [max(0, *times.values()) for times in first_acquire_times] - if align_time is None: - if max_calibration_duration is None: - if inst_map: - max_calibration_duration = get_max_calibration_duration(inst_map, cal_gate) - else: - max_calibration_duration = 0 - align_time = max(max_calibration_duration, *max_acquire_times) - - # Shift acquires according to the new scheduled time - new_schedules = [] - for sched_idx, schedule in enumerate(schedules): - new_schedule = Schedule.initialize_from(schedule) - stop_time = schedule.stop_time - - if align_all: - if first_acquire_times[sched_idx]: - shift = align_time - max_acquire_times[sched_idx] - else: - shift = align_time - stop_time - else: - shift = 0 - - for time, inst in schedule.instructions: - measurement_channels = { - chan.index - for chan in inst.channels - if isinstance(chan, (chans.MeasureChannel, chans.AcquireChannel)) - } - if measurement_channels: - sched_first_acquire_times = first_acquire_times[sched_idx] - max_start_time = max( - sched_first_acquire_times[chan] - for chan in measurement_channels - if chan in sched_first_acquire_times - ) - shift = align_time - max_start_time - - if shift < 0: - warnings.warn( - "The provided alignment time is scheduling an acquire instruction " - "earlier than it was scheduled for in the original Schedule. " - "This may result in an instruction being scheduled before t=0 and " - "an error being raised." - ) - new_schedule.insert(time + shift, inst, inplace=True) - - new_schedules.append(new_schedule) - - return new_schedules - - -def add_implicit_acquires(schedule: ScheduleComponent, meas_map: list[list[int]]) -> Schedule: - """Return a new schedule with implicit acquires from the measurement mapping replaced by - explicit ones. - - .. warning:: Since new acquires are being added, Memory Slots will be set to match the - qubit index. This may overwrite your specification. - - Args: - schedule: Schedule to be aligned. - meas_map: List of lists of qubits that are measured together. - - Returns: - A ``Schedule`` with the additional acquisition instructions. - """ - new_schedule = Schedule.initialize_from(schedule) - acquire_map = {} - - for time, inst in schedule.instructions: - if isinstance(inst, instructions.Acquire): - if inst.mem_slot and inst.mem_slot.index != inst.channel.index: - warnings.warn( - "One of your acquires was mapped to a memory slot which didn't match" - " the qubit index. I'm relabeling them to match." - ) - - # Get the label of all qubits that are measured with the qubit(s) in this instruction - all_qubits = [] - for sublist in meas_map: - if inst.channel.index in sublist: - all_qubits.extend(sublist) - # Replace the old acquire instruction by a new one explicitly acquiring all qubits in - # the measurement group. - for i in all_qubits: - explicit_inst = instructions.Acquire( - inst.duration, - chans.AcquireChannel(i), - mem_slot=chans.MemorySlot(i), - kernel=inst.kernel, - discriminator=inst.discriminator, - ) - if time not in acquire_map: - new_schedule.insert(time, explicit_inst, inplace=True) - acquire_map = {time: {i}} - elif i not in acquire_map[time]: - new_schedule.insert(time, explicit_inst, inplace=True) - acquire_map[time].add(i) - else: - new_schedule.insert(time, inst, inplace=True) - - return new_schedule - - -def pad( - schedule: Schedule, - channels: Iterable[chans.Channel] | None = None, - until: int | None = None, - inplace: bool = False, - pad_with: Type[instructions.Instruction] | None = None, -) -> 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. - pad_with: Pulse ``Instruction`` subclass to be used for padding. - Default to :class:`~qiskit.pulse.instructions.Delay` instruction. - - Returns: - The padded schedule. - - Raises: - PulseError: When non pulse instruction is set to `pad_with`. - """ - until = until or schedule.duration - channels = channels or schedule.channels - - if pad_with: - if issubclass(pad_with, instructions.Instruction): - pad_cls = pad_with - else: - raise PulseError( - f"'{pad_with.__class__.__name__}' is not valid pulse instruction to pad with." - ) - else: - pad_cls = instructions.Delay - - for channel in channels: - if isinstance(channel, ClassicalIOChannel): - continue - - if channel not in schedule.channels: - schedule = schedule.insert(0, instructions.Delay(until, channel), inplace=inplace) - continue - - prev_time = 0 - timeslots = iter(schedule.timeslots[channel]) - to_pad = [] - while prev_time < until: - try: - t0, t1 = next(timeslots) - except StopIteration: - to_pad.append((prev_time, until - prev_time)) - break - if prev_time < t0: - to_pad.append((prev_time, min(t0, until) - prev_time)) - prev_time = t1 - for t0, duration in to_pad: - schedule = schedule.insert(t0, pad_cls(duration, channel), inplace=inplace) - - return schedule diff --git a/qiskit/pulse/transforms/dag.py b/qiskit/pulse/transforms/dag.py deleted file mode 100644 index 92e346b0f00b..000000000000 --- a/qiskit/pulse/transforms/dag.py +++ /dev/null @@ -1,128 +0,0 @@ -# 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 functions to convert ScheduleBlock to DAG representation.""" -from __future__ import annotations - -import typing - -import rustworkx as rx - - -from qiskit.pulse.channels import Channel -from qiskit.pulse.exceptions import UnassignedReferenceError - -if typing.TYPE_CHECKING: - from qiskit.pulse import ScheduleBlock # pylint: disable=cyclic-import - - -def block_to_dag(block: ScheduleBlock) -> rx.PyDAG: - """Convert schedule block instruction into DAG. - - ``ScheduleBlock`` can be represented as a DAG as needed. - For example, equality of two programs are efficiently checked on DAG representation. - - .. plot:: - :include-source: - :nofigs: - :context: reset - - from qiskit import pulse - - my_gaussian0 = pulse.Gaussian(100, 0.5, 20) - my_gaussian1 = pulse.Gaussian(100, 0.3, 10) - - with pulse.build() as sched1: - with pulse.align_left(): - pulse.play(my_gaussian0, pulse.DriveChannel(0)) - pulse.shift_phase(1.57, pulse.DriveChannel(2)) - pulse.play(my_gaussian1, pulse.DriveChannel(1)) - - with pulse.build() as sched2: - with pulse.align_left(): - pulse.shift_phase(1.57, pulse.DriveChannel(2)) - pulse.play(my_gaussian1, pulse.DriveChannel(1)) - pulse.play(my_gaussian0, pulse.DriveChannel(0)) - - Here the ``sched1 `` and ``sched2`` are different implementations of the same program, - but it is difficult to confirm on the list representation. - - Another example is instruction optimization. - - .. plot:: - :include-source: - :nofigs: - :context: - - from qiskit import pulse - - with pulse.build() as sched: - with pulse.align_left(): - pulse.shift_phase(1.57, pulse.DriveChannel(1)) - pulse.play(my_gaussian0, pulse.DriveChannel(0)) - pulse.shift_phase(-1.57, pulse.DriveChannel(1)) - - In above program two ``shift_phase`` instructions can be cancelled out because - they are consecutive on the same drive channel. - This can be easily found on the DAG representation. - - Args: - block ("ScheduleBlock"): A schedule block to be converted. - - Returns: - Instructions in DAG representation. - - Raises: - PulseError: When the context is invalid subclass. - """ - if block.alignment_context.is_sequential: - return _sequential_allocation(block) - return _parallel_allocation(block) - - -def _sequential_allocation(block) -> rx.PyDAG: - """A helper function to create a DAG of a sequential alignment context.""" - dag = rx.PyDAG() - - edges: list[tuple[int, int]] = [] - prev_id = None - for elm in block.blocks: - node_id = dag.add_node(elm) - if dag.num_nodes() > 1: - edges.append((prev_id, node_id)) - prev_id = node_id - dag.add_edges_from_no_data(edges) - return dag - - -def _parallel_allocation(block) -> rx.PyDAG: - """A helper function to create a DAG of a parallel alignment context.""" - dag = rx.PyDAG() - - slots: dict[Channel, int] = {} - edges: set[tuple[int, int]] = set() - prev_reference = None - for elm in block.blocks: - node_id = dag.add_node(elm) - try: - for chan in elm.channels: - prev_id = slots.pop(chan, prev_reference) - if prev_id is not None: - edges.add((prev_id, node_id)) - slots[chan] = node_id - except UnassignedReferenceError: - # Broadcasting channels because the reference's channels are unknown. - for chan, prev_id in slots.copy().items(): - edges.add((prev_id, node_id)) - slots[chan] = node_id - prev_reference = node_id - dag.add_edges_from_no_data(list(edges)) - return dag diff --git a/qiskit/pulse/utils.py b/qiskit/pulse/utils.py deleted file mode 100644 index 2f2093817c8a..000000000000 --- a/qiskit/pulse/utils.py +++ /dev/null @@ -1,149 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Module for common pulse programming utilities.""" -from typing import List, Dict, Union, Sequence -import warnings - -import numpy as np - -from qiskit.circuit import ParameterVector, Parameter -from qiskit.circuit.parameterexpression import ParameterExpression -from qiskit.pulse.exceptions import UnassignedDurationError, QiskitError, PulseError - - -def format_meas_map(meas_map: List[List[int]]) -> Dict[int, List[int]]: - """ - Return a mapping from qubit label to measurement group given the nested list meas_map returned - by a backend configuration. (Qubits can not always be measured independently.) Sorts the - measurement group for consistency. - - Args: - meas_map: Groups of qubits that get measured together, for example: [[0, 1], [2, 3, 4]] - Returns: - Measure map in map format - """ - qubit_mapping = {} - for sublist in meas_map: - sublist.sort() - for q in sublist: - qubit_mapping[q] = sublist - return qubit_mapping - - -def format_parameter_value( - operand: ParameterExpression, - decimal: int = 10, -) -> Union[ParameterExpression, complex]: - """Convert ParameterExpression into the most suitable data type. - - Args: - operand: Operand value in arbitrary data type including ParameterExpression. - decimal: Number of digit to round returned value. - - Returns: - Value cast to non-parameter data type, when possible. - """ - if isinstance(operand, ParameterExpression): - try: - operand = operand.numeric() - except TypeError: - # Unassigned expression - return operand - - # Return integer before calling the numpy round function. - # The input value is multiplied by 10**decimals, rounds to an integer - # and divided by 10**decimals. For a large enough integer, - # this operation may introduce a rounding error in the float operations - # and accidentally returns a float number. - if isinstance(operand, int): - return operand - - # Remove truncation error and convert the result into Python builtin type. - # Value could originally contain a rounding error, e.g. 1.00000000001 - # which may occur during the parameter expression evaluation. - evaluated = np.round(operand, decimals=decimal).item() - - if isinstance(evaluated, complex): - if np.isclose(evaluated.imag, 0.0): - evaluated = evaluated.real - else: - warnings.warn( - "Assignment of complex values to ParameterExpression in Qiskit Pulse objects is " - "now pending deprecation. This will align the Pulse module with other modules " - "where such assignment wasn't possible to begin with. The typical use case for complex " - "parameters in the module was the SymbolicPulse library. As of Qiskit-Terra " - "0.23.0 all library pulses were converted from complex amplitude representation" - " to real representation using two floats (amp,angle), as used in the " - "ScalableSymbolicPulse class. This eliminated the need for complex parameters. " - "Any use of complex parameters (and particularly custom-built pulses) should be " - "converted in a similar fashion to avoid the use of complex parameters.", - PendingDeprecationWarning, - ) - return evaluated - # Type cast integer-like float into Python builtin integer, after rounding. - if evaluated.is_integer(): - return int(evaluated) - return evaluated - - -def instruction_duration_validation(duration: int): - """Validate instruction duration. - - Args: - duration: Instruction duration value to validate. - - Raises: - UnassignedDurationError: When duration is unassigned. - QiskitError: When invalid duration is assigned. - """ - if isinstance(duration, ParameterExpression): - raise UnassignedDurationError( - f"Instruction duration {repr(duration)} is not assigned. " - "Please bind all durations to an integer value before playing in the Schedule, " - "or use ScheduleBlock to align instructions with unassigned duration." - ) - - if not isinstance(duration, (int, np.integer)) or duration < 0: - raise QiskitError( - f"Instruction duration must be a non-negative integer, got {duration} instead." - ) - - -def _validate_parameter_vector(parameter: ParameterVector, value): - """Validate parameter vector and its value.""" - if not isinstance(value, Sequence): - raise PulseError( - f"Parameter vector '{parameter.name}' has length {len(parameter)}," - f" but was assigned to {value}." - ) - if len(parameter) != len(value): - raise PulseError( - f"Parameter vector '{parameter.name}' has length {len(parameter)}," - f" but was assigned to {len(value)} values." - ) - - -def _validate_single_parameter(parameter: Parameter, value): - """Validate single parameter and its value.""" - if not isinstance(value, (int, float, complex, ParameterExpression)): - raise PulseError(f"Parameter '{parameter.name}' is not assignable to {value}.") - - -def _validate_parameter_value(parameter, value): - """Validate parameter and its value.""" - if isinstance(parameter, ParameterVector): - _validate_parameter_vector(parameter, value) - return True - else: - _validate_single_parameter(parameter, value) - return False diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 9bb18dc304e3..dd58f2b9df41 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -18,7 +18,7 @@ .. currentmodule:: qiskit.qpy QPY is a binary serialization format for :class:`~.QuantumCircuit` -objects that is designed to be cross-platform, Python version agnostic, +objects that is designed to be cross-platform, Python version agnostic, and backwards compatible moving forward. QPY should be used if you need a mechanism to save or copy between systems a :class:`~.QuantumCircuit` that preserves the full Qiskit object structure (except for custom attributes diff --git a/qiskit/result/result.py b/qiskit/result/result.py index 2d1677d9e2fc..dadd4e5fa5fc 100644 --- a/qiskit/result/result.py +++ b/qiskit/result/result.py @@ -16,7 +16,6 @@ import warnings from qiskit.circuit.quantumcircuit import QuantumCircuit -from qiskit.pulse.schedule import Schedule from qiskit.exceptions import QiskitError from qiskit.quantum_info.states import statevector from qiskit.result.models import ExperimentResult, MeasLevel @@ -345,8 +344,8 @@ def _get_experiment(self, key=None): ) key = 0 - # Key is a QuantumCircuit/Schedule or str: retrieve result by name. - if isinstance(key, (QuantumCircuit, Schedule)): + # Key is a QuantumCircuit or str: retrieve result by name. + if isinstance(key, QuantumCircuit): key = key.name # Key is an integer: return result by index. if isinstance(key, int): diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index f7a4867f4f78..66115500d686 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -87,20 +87,11 @@ ResetAfterMeasureSimplification OptimizeCliffords ElidePermutations - NormalizeRXAngle OptimizeAnnotated Split2QUnitaries RemoveIdentityEquivalent ContractIdleWiresInControlFlow -Calibration -============= - -.. autosummary:: - :toctree: ../stubs/ - -.. autofunction:: rzx_templates - Scheduling ============= @@ -230,7 +221,6 @@ from .optimization import ResetAfterMeasureSimplification from .optimization import OptimizeCliffords from .optimization import ElidePermutations -from .optimization import NormalizeRXAngle from .optimization import OptimizeAnnotated from .optimization import RemoveIdentityEquivalent from .optimization import Split2QUnitaries @@ -256,9 +246,6 @@ from .synthesis import SolovayKitaevSynthesis from .synthesis import AQCSynthesisPlugin -# calibration -from .calibration.rzx_templates import rzx_templates - # circuit scheduling from .scheduling import TimeUnitConversion from .scheduling import ALAPScheduleAnalysis diff --git a/qiskit/transpiler/passes/calibration/rzx_templates.py b/qiskit/transpiler/passes/calibration/rzx_templates.py deleted file mode 100644 index 10f4d19ebd9e..000000000000 --- a/qiskit/transpiler/passes/calibration/rzx_templates.py +++ /dev/null @@ -1,51 +0,0 @@ -# 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. - -""" -Convenience function to load RZXGate based templates. -""" - -from enum import Enum -from typing import List, Dict - -from qiskit.circuit.library.templates import rzx - - -def rzx_templates(template_list: List[str] = None) -> Dict: - """Convenience function to get the cost_dict and templates for template matching. - - Args: - template_list: List of instruction names. - - Returns: - Decomposition templates and cost values. - """ - - class RZXTemplateMap(Enum): - """Mapping of instruction name to decomposition template.""" - - ZZ1 = rzx.rzx_zz1() - ZZ2 = rzx.rzx_zz2() - ZZ3 = rzx.rzx_zz3() - YZ = rzx.rzx_yz() - XZ = rzx.rzx_xz() - CY = rzx.rzx_cy() - - if template_list is None: - template_list = ["zz1", "zz2", "zz3", "yz", "xz", "cy"] - - templates = [RZXTemplateMap[gate.upper()].value for gate in template_list] - cost_dict = {"rzx": 0, "cx": 6, "rz": 0, "sx": 1, "p": 0, "h": 1, "rx": 1, "ry": 1} - - rzx_dict = {"template_list": templates, "user_cost_dict": cost_dict} - - return rzx_dict diff --git a/qiskit/transpiler/passes/optimization/__init__.py b/qiskit/transpiler/passes/optimization/__init__.py index 6420eb7d972c..a7e2581a98e7 100644 --- a/qiskit/transpiler/passes/optimization/__init__.py +++ b/qiskit/transpiler/passes/optimization/__init__.py @@ -34,7 +34,6 @@ from .optimize_cliffords import OptimizeCliffords from .collect_cliffords import CollectCliffords from .elide_permutations import ElidePermutations -from .normalize_rx_angle import NormalizeRXAngle from .optimize_annotated import OptimizeAnnotated from .remove_identity_equiv import RemoveIdentityEquivalent from .split_2q_unitaries import Split2QUnitaries diff --git a/qiskit/transpiler/passes/optimization/normalize_rx_angle.py b/qiskit/transpiler/passes/optimization/normalize_rx_angle.py deleted file mode 100644 index b6f36e07de36..000000000000 --- a/qiskit/transpiler/passes/optimization/normalize_rx_angle.py +++ /dev/null @@ -1,149 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Performs three optimizations to reduce the number of pulse calibrations for -the single-pulse RX gates: -Wrap RX Gate rotation angles into [0, pi] by sandwiching them with RZ gates. -Convert RX(pi/2) to SX, and RX(pi) to X if the calibrations exist in the target. -Quantize the RX rotation angles by assigning the same value for the angles -that differ within a resolution provided by the user. -""" - -import numpy as np - -from qiskit.transpiler.basepasses import TransformationPass -from qiskit.dagcircuit import DAGCircuit -from qiskit.circuit.library.standard_gates import RXGate, RZGate, SXGate, XGate - - -class NormalizeRXAngle(TransformationPass): - """Normalize theta parameter of RXGate instruction. - - The parameter normalization is performed with following steps. - - 1) Wrap RX Gate theta into [0, pi]. When theta is negative value, the gate is - decomposed into the following sequence. - - .. code-block:: - - ┌───────┐┌─────────┐┌────────┐ - q: ┤ Rz(π) ├┤ Rx(|θ|) ├┤ Rz(-π) ├ - └───────┘└─────────┘└────────┘ - - 2) If the operation is supported by target, convert RX(pi/2) to SX, and RX(pi) to X. - - 3) Quantize theta value according to the user-specified resolution. - - This will help reduce the size of calibration data sent over the wire, - and allow us to exploit the more accurate, hardware-calibrated pulses. - Note that pulse calibration might be attached per each rotation angle. - """ - - def __init__(self, target=None, resolution_in_radian=0): - """NormalizeRXAngle initializer. - - Args: - target (Target): The :class:`~.Target` representing the target backend. - If the target contains SX and X calibrations, this pass will replace the - corresponding RX gates with SX and X gates. - resolution_in_radian (float): Resolution for RX rotation angle quantization. - If set to zero, this pass won't modify the rotation angles in the given DAG. - (=Provides arbitrary-angle RX) - """ - super().__init__() - self.target = target - self.resolution_in_radian = resolution_in_radian - self.already_generated = {} - - def quantize_angles(self, qubit, original_angle): - """Quantize the RX rotation angles by assigning the same value for the angles - that differ within a resolution provided by the user. - - Args: - qubit (qiskit.circuit.Qubit): This will be the dict key to access the list of - quantized rotation angles. - original_angle (float): Original rotation angle, before quantization. - - Returns: - float: Quantized angle. - """ - - if (angles := self.already_generated.get(qubit)) is None: - self.already_generated[qubit] = np.array([original_angle]) - return original_angle - similar_angles = angles[ - np.isclose(angles, original_angle, atol=self.resolution_in_radian / 2) - ] - if similar_angles.size == 0: - self.already_generated[qubit] = np.append(angles, original_angle) - return original_angle - return float(similar_angles[0]) - - def run(self, dag): - """Run the NormalizeRXAngle pass on ``dag``. - - Args: - dag (DAGCircuit): The DAG to be optimized. - - Returns: - DAGCircuit: A DAG with RX gate calibration. - """ - - # Iterate over all op_nodes and replace RX if eligible for modification. - for op_node in dag.op_nodes(): - if not isinstance(op_node.op, RXGate): - continue - - raw_theta = op_node.op.params[0] - wrapped_theta = np.arctan2(np.sin(raw_theta), np.cos(raw_theta)) # [-pi, pi] - - if self.resolution_in_radian: - wrapped_theta = self.quantize_angles(op_node.qargs[0], wrapped_theta) - - half_pi_rotation = np.isclose( - abs(wrapped_theta), np.pi / 2, atol=self.resolution_in_radian / 2 - ) - pi_rotation = np.isclose(abs(wrapped_theta), np.pi, atol=self.resolution_in_radian / 2) - - should_modify_node = ( - (wrapped_theta != raw_theta) - or (wrapped_theta < 0) - or half_pi_rotation - or pi_rotation - ) - - if should_modify_node: - mini_dag = DAGCircuit() - mini_dag.add_qubits(op_node.qargs) - - # new X-rotation gate with angle in [0, pi] - if half_pi_rotation: - physical_qubit_idx = dag.find_bit(op_node.qargs[0]).index - if self.target.instruction_supported("sx", (physical_qubit_idx,)): - mini_dag.apply_operation_back(SXGate(), qargs=op_node.qargs) - elif pi_rotation: - physical_qubit_idx = dag.find_bit(op_node.qargs[0]).index - if self.target.instruction_supported("x", (physical_qubit_idx,)): - mini_dag.apply_operation_back(XGate(), qargs=op_node.qargs) - else: - mini_dag.apply_operation_back( - RXGate(np.abs(wrapped_theta)), qargs=op_node.qargs - ) - - # sandwich with RZ if the intended rotation angle was negative - if wrapped_theta < 0: - mini_dag.apply_operation_front(RZGate(np.pi), qargs=op_node.qargs) - mini_dag.apply_operation_back(RZGate(-np.pi), qargs=op_node.qargs) - - dag.substitute_node_with_dag(node=op_node, input_dag=mini_dag, wires=op_node.qargs) - - return dag diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index b00e4c9e5112..5930dc17b986 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -63,7 +63,6 @@ class Optimize1qGatesDecomposition(TransformationPass): The decision to replace the original chain with a new re-synthesis depends on: - whether the original chain was out of basis: replace - whether the original chain was in basis but re-synthesis is lower error: replace - - whether the original chain contains a pulse gate: do not replace - whether the original chain amounts to identity: replace with null Error is computed as a multiplication of the errors of individual gates on that qubit. diff --git a/qiskit/transpiler/passes/scheduling/alignments/check_durations.py b/qiskit/transpiler/passes/scheduling/alignments/check_durations.py index 134f1c116a08..1f3e1c375299 100644 --- a/qiskit/transpiler/passes/scheduling/alignments/check_durations.py +++ b/qiskit/transpiler/passes/scheduling/alignments/check_durations.py @@ -22,7 +22,7 @@ class InstructionDurationCheck(AnalysisPass): This pass investigates the input quantum circuit and checks if the circuit requires rescheduling for execution. Note that this pass can be triggered without scheduling. - This pass only checks the duration of delay instructions and user defined pulse gates, + This pass only checks the duration of delay instructions, which report duration values without pre-scheduling. This pass assumes backend supported instructions, i.e. basis gates, have no violation diff --git a/qiskit/utils/deprecate_pulse.py b/qiskit/utils/deprecate_pulse.py deleted file mode 100644 index 376e3e06c8f1..000000000000 --- a/qiskit/utils/deprecate_pulse.py +++ /dev/null @@ -1,119 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2024 -# -# 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. - -""" -Deprecation functions for Qiskit Pulse. To be removed in Qiskit 2.0. -""" - -import warnings -import functools - -from qiskit.utils.deprecation import deprecate_func, deprecate_arg - - -def deprecate_pulse_func(func): - """Deprecation message for functions and classes""" - return deprecate_func( - since="1.3", - package_name="Qiskit", - removal_timeline="in Qiskit 2.0", - additional_msg="The entire Qiskit Pulse package is being deprecated " - "and will be moved to the Qiskit Dynamics repository: " - "https://github.com/qiskit-community/qiskit-dynamics", - )(func) - - -def deprecate_pulse_dependency(*args, moving_to_dynamics: bool = False, **kwargs): - # pylint: disable=missing-param-doc - """Deprecation message for functions and classes which use or depend on Pulse - - Args: - moving_to_dynamics: set to True if the dependency is moving to Qiskit Dynamics. This affects - the deprecation message being printed, namely saying explicitly whether the dependency will - be moved to Qiskit Dynamics or whether it will just be removed without an alternative. - """ - - def msg_handler(func): - fully_qual_name = format(f"{func.__module__}.{func.__qualname__}") - if ".__init__" in fully_qual_name: # Deprecating a class' vis it __init__ method - fully_qual_name = fully_qual_name[:-9] - elif "is_property" not in kwargs: # Deprecating either a function or a method - fully_qual_name += "()" - - message = ( - "The entire Qiskit Pulse package is being deprecated and will be moved to the Qiskit " - "Dynamics repository: https://github.com/qiskit-community/qiskit-dynamics." - + ( - format(f" Note that ``{fully_qual_name}`` will be moved as well.") - if moving_to_dynamics - else format( - f" Note that once removed, ``{fully_qual_name}`` will have no alternative in Qiskit." - ) - ) - ) - - decorator = deprecate_func( - since="1.3", - package_name="Qiskit", - removal_timeline="in Qiskit 2.0", - additional_msg=message, - **kwargs, - )(func) - - # Taken when `deprecate_pulse_dependency` is used with no arguments and with empty parentheses, - # in which case the decorated function is passed - - return decorator - - if args: - return msg_handler(args[0]) - return msg_handler - - -def deprecate_pulse_arg(arg_name: str, **kwargs): - """Deprecation message for arguments related to Pulse""" - return deprecate_arg( - name=arg_name, - since="1.3", - package_name="Qiskit", - removal_timeline="in Qiskit 2.0", - additional_msg="The entire Qiskit Pulse package is being deprecated " - "and this argument uses a dependency on the package.", - **kwargs, - ) - - -def ignore_pulse_deprecation_warnings(func): - """Ignore deprecation warnings emitted from the pulse package""" - - @functools.wraps(func) - def wrapper(*args, **kwargs): - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", category=DeprecationWarning, message="The (.*) ``qiskit.pulse" - ) - return func(*args, **kwargs) - - return wrapper - - -def decorate_test_methods(decorator): - """Put a given decorator on all the decorated class methods whose name starts with `test_`""" - - def cls_wrapper(cls): - for attr in dir(cls): - if attr.startswith("test_") and callable(object.__getattribute__(cls, attr)): - setattr(cls, attr, decorator(object.__getattribute__(cls, attr))) - - return cls - - return cls_wrapper diff --git a/qiskit/visualization/__init__.py b/qiskit/visualization/__init__.py index 6d1b4e86622d..654cf2b583a9 100644 --- a/qiskit/visualization/__init__.py +++ b/qiskit/visualization/__init__.py @@ -18,7 +18,7 @@ .. currentmodule:: qiskit.visualization The visualization module contain functions that visualizes measurement outcome counts, quantum -states, circuits, pulses, devices and more. +states, circuits, devices and more. To use visualization functions, you are required to install visualization optionals to your development environment: @@ -281,8 +281,6 @@ from .pass_manager_visualization import pass_manager_drawer from .pass_manager_visualization import staged_pass_manager_drawer -from .pulse_v2 import draw as pulse_drawer - from .timeline import draw as timeline_drawer from .exceptions import VisualizationError @@ -290,7 +288,3 @@ # These modules aren't part of the public interface, and were moved in Terra 0.22. They're # re-imported here to allow a backwards compatible path, and should be deprecated in Terra 0.23. from .circuit import text, matplotlib, latex - -# Prepare for migration of old versioned name to unversioned name. The `pulse_drawer_v2` name can -# be deprecated in Terra 0.24, as `pulse_drawer` became available by that name in Terra 0.23. -pulse_drawer_v2 = pulse_drawer diff --git a/qiskit/visualization/pulse_v2/__init__.py b/qiskit/visualization/pulse_v2/__init__.py deleted file mode 100644 index 169341f9b896..000000000000 --- a/qiskit/visualization/pulse_v2/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -""" -Pulse visualization module. -""" - -# interface -from qiskit.visualization.pulse_v2.interface import draw - -# stylesheets -from qiskit.visualization.pulse_v2.stylesheet import IQXStandard, IQXSimple, IQXDebugging diff --git a/qiskit/visualization/pulse_v2/core.py b/qiskit/visualization/pulse_v2/core.py deleted file mode 100644 index 20686f6fb4f6..000000000000 --- a/qiskit/visualization/pulse_v2/core.py +++ /dev/null @@ -1,901 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -""" -Core module of the pulse drawer. - -This module provides the `DrawerCanvas` which is a collection of `Chart` object. -The `Chart` object is a collection of drawings. A user can assign multiple channels -to a single chart instance. For example, we can define a chart for specific qubit -and assign all related channels to the chart. This chart-channel mapping is defined by -the function specified by ``layout.chart_channel_map`` of the stylesheet. - -Because this chart instance is decoupled from the coordinate system of the plotter, -we can arbitrarily place charts on the plotter canvas, i.e. if we want to create 3D plot, -each chart may be placed on the X-Z plane and charts are arranged along the Y-axis. -Thus this data model maximizes the flexibility to generate an output image. - -The chart instance is not just a container of drawings, as it also performs -data processing like binding abstract coordinates and truncating long pulses for an axis break. -Each chart object has `.parent` which points to the `DrawerCanvas` instance so that -each child chart can refer to the global figure settings such as time range and axis break. - - -Initialization -~~~~~~~~~~~~~~ -The `DataCanvas` and `Chart` are not exposed to users as they are implicitly -initialized in the interface function. It is noteworthy that the data canvas is agnostic -to plotters. This means once the canvas instance is initialized we can reuse this data -among multiple plotters. The canvas is initialized with a stylesheet and quantum backend -information :py:class:`~qiskit.visualization.pulse_v2.device_info.DrawerBackendInfo`. -Chart instances are automatically generated when pulse program is loaded. - - ```python - canvas = DrawerCanvas(stylesheet=stylesheet, device=device) - canvas.load_program(sched) - canvas.update() - ``` - -Once all properties are set, `.update` method is called to apply changes to drawings. -If the `DrawDataContainer` is initialized without backend information, the output shows -the time in units of the system cycle time `dt` and the frequencies are initialized to zero. - -Update -~~~~~~ -To update the image, a user can set new values to canvas and then call the `.update` method. - - ```python - canvas.set_time_range(2000, 3000, seconds=False) - canvas.update() - ``` - -All stored drawings are updated accordingly. The plotter API can access to -drawings with `.collections` property of chart instance. This returns -an iterator of drawing with the unique data key. -If a plotter provides object handler for plotted shapes, the plotter API can manage -the lookup table of the handler and the drawing by using this data key. -""" - -from __future__ import annotations - -from collections.abc import Iterator, Sequence -from copy import deepcopy -from enum import Enum -from functools import partial -from itertools import chain - -import numpy as np -from qiskit import pulse -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 - - -class DrawerCanvas: - """Collection of `Chart` and configuration data. - - Pulse channels are associated with some `Chart` instance and - drawing data object are stored in the `Chart` instance. - - Device, stylesheet, and some user generators are stored in the `DrawingCanvas` - and `Chart` instances are also attached to the `DrawerCanvas` as children. - Global configurations are accessed by those children to modify - the appearance of the `Chart` output. - """ - - def __init__(self, stylesheet: QiskitPulseStyle, device: device_info.DrawerBackendInfo): - """Create new data container with backend system information. - - Args: - stylesheet: Stylesheet to decide appearance of output image. - device: Backend information to run the program. - """ - # stylesheet - self.formatter = stylesheet.formatter - self.generator = stylesheet.generator - self.layout = stylesheet.layout - - # device info - self.device = device - - # chart - self.global_charts = Chart(parent=self, name="global") - self.charts: list[Chart] = [] - - # visible controls - self.disable_chans: set[pulse.channels.Channel] = set() - self.disable_types: set[str] = set() - - # data scaling - self.chan_scales: dict[ - pulse.channels.DriveChannel - | pulse.channels.MeasureChannel - | pulse.channels.ControlChannel - | pulse.channels.AcquireChannel, - float, - ] = {} - - # global time - self._time_range = (0, 0) - self._time_breaks: list[tuple[int, int]] = [] - - # title - self.fig_title = "" - - @property - def time_range(self) -> tuple[int, int]: - """Return current time range to draw. - - Calculate net duration and add side margin to edge location. - - Returns: - Time window considering side margin. - """ - t0, t1 = self._time_range - - total_time_elimination = 0 - for t0b, t1b in self.time_breaks: - if t1b > t0 and t0b < t1: - total_time_elimination += t1b - t0b - net_duration = t1 - t0 - total_time_elimination - - new_t0 = t0 - net_duration * self.formatter["margin.left_percent"] - new_t1 = t1 + net_duration * self.formatter["margin.right_percent"] - - return new_t0, new_t1 - - @time_range.setter - def time_range(self, new_range: tuple[int, int]): - """Update time range to draw.""" - self._time_range = new_range - - @property - def time_breaks(self) -> list[tuple[int, int]]: - """Return time breaks with time range. - - If an edge of time range is in the axis break period, - the axis break period is recalculated. - - Raises: - VisualizationError: When axis break is greater than time window. - - Returns: - List of axis break periods considering the time window edges. - """ - t0, t1 = self._time_range - - axis_breaks = [] - for t0b, t1b in self._time_breaks: - if t0b >= t1 or t1b <= t0: - # skip because break period is outside of time window - continue - - if t0b < t0 and t1b > t1: - raise VisualizationError( - "Axis break is greater than time window. Nothing will be drawn." - ) - if t0b < t0 < t1b: - if t1b - t0 > self.formatter["axis_break.length"]: - new_t0 = t0 + 0.5 * self.formatter["axis_break.max_length"] - axis_breaks.append((new_t0, t1b)) - continue - if t0b < t1 < t1b: - if t1 - t0b > self.formatter["axis_break.length"]: - new_t1 = t1 - 0.5 * self.formatter["axis_break.max_length"] - axis_breaks.append((t0b, new_t1)) - continue - axis_breaks.append((t0b, t1b)) - - return axis_breaks - - @time_breaks.setter - def time_breaks(self, new_breaks: list[tuple[int, int]]): - """Set new time breaks.""" - self._time_breaks = sorted(new_breaks, key=lambda x: x[0]) - - def load_program( - self, - program: pulse.Waveform | pulse.SymbolicPulse | pulse.Schedule | pulse.ScheduleBlock, - ): - """Load a program to draw. - - Args: - program: Pulse program or waveform to draw. - - Raises: - VisualizationError: When input program is invalid data format. - """ - if isinstance(program, (pulse.Schedule, pulse.ScheduleBlock)): - self._schedule_loader(program) - elif isinstance(program, (pulse.Waveform, pulse.SymbolicPulse)): - self._waveform_loader(program) - else: - raise VisualizationError(f"Data type {type(program)} is not supported.") - - # update time range - self.set_time_range(0, program.duration, seconds=False) - - # set title - self.fig_title = self.layout["figure_title"](program=program, device=self.device) - - def _waveform_loader( - self, - program: pulse.Waveform | pulse.SymbolicPulse, - ): - """Load Waveform instance. - - This function is sub-routine of py:method:`load_program`. - - Args: - program: `Waveform` to draw. - """ - chart = Chart(parent=self) - - # add waveform data - fake_inst = pulse.Play(program, types.WaveformChannel()) - inst_data = types.PulseInstruction( - t0=0, - dt=self.device.dt, - frame=types.PhaseFreqTuple(phase=0, freq=0), - inst=fake_inst, - is_opaque=program.is_parameterized(), - ) - for gen in self.generator["waveform"]: - obj_generator = partial(gen, formatter=self.formatter, device=self.device) - for data in obj_generator(inst_data): - chart.add_data(data) - - self.charts.append(chart) - - def _schedule_loader(self, program: pulse.Schedule | pulse.ScheduleBlock): - """Load Schedule instance. - - This function is sub-routine of py:method:`load_program`. - - Args: - program: `Schedule` to draw. - """ - program = target_qobj_transform(program, remove_directives=False) - - # initialize scale values - self.chan_scales = {} - for chan in program.channels: - if isinstance(chan, pulse.channels.DriveChannel): - self.chan_scales[chan] = self.formatter["channel_scaling.drive"] - elif isinstance(chan, pulse.channels.MeasureChannel): - self.chan_scales[chan] = self.formatter["channel_scaling.measure"] - elif isinstance(chan, pulse.channels.ControlChannel): - self.chan_scales[chan] = self.formatter["channel_scaling.control"] - elif isinstance(chan, pulse.channels.AcquireChannel): - self.chan_scales[chan] = self.formatter["channel_scaling.acquire"] - else: - self.chan_scales[chan] = 1.0 - - # create charts - mapper = self.layout["chart_channel_map"] - for name, chans in mapper( - channels=program.channels, formatter=self.formatter, device=self.device - ): - - chart = Chart(parent=self, name=name) - - # add standard pulse instructions - for chan in chans: - chart.load_program(program=program, chan=chan) - - # add barriers - barrier_sched = program.filter( - instruction_types=[pulse.instructions.RelativeBarrier], channels=chans - ) - for t0, _ in barrier_sched.instructions: - inst_data = types.BarrierInstruction(t0, self.device.dt, chans) - for gen in self.generator["barrier"]: - obj_generator = partial(gen, formatter=self.formatter, device=self.device) - for data in obj_generator(inst_data): - chart.add_data(data) - - # add chart axis - chart_axis = types.ChartAxis(name=chart.name, channels=chart.channels) - for gen in self.generator["chart"]: - obj_generator = partial(gen, formatter=self.formatter, device=self.device) - for data in obj_generator(chart_axis): - chart.add_data(data) - - self.charts.append(chart) - - # add snapshot data to global - snapshot_sched = program.filter(instruction_types=[pulse.instructions.Snapshot]) - for t0, inst in snapshot_sched.instructions: - inst_data = types.SnapshotInstruction(t0, self.device.dt, inst.label, inst.channels) - for gen in self.generator["snapshot"]: - obj_generator = partial(gen, formatter=self.formatter, device=self.device) - for data in obj_generator(inst_data): - self.global_charts.add_data(data) - - # calculate axis break - self.time_breaks = self._calculate_axis_break(program) - - def _calculate_axis_break(self, program: pulse.Schedule) -> list[tuple[int, int]]: - """A helper function to calculate axis break of long pulse sequence. - - Args: - program: A schedule to calculate axis break. - - Returns: - List of axis break periods. - """ - axis_breaks = [] - - edges = set() - for t0, t1 in chain.from_iterable(program.timeslots.values()): - if t1 - t0 > 0: - edges.add(t0) - edges.add(t1) - edges = sorted(edges) - - for t0, t1 in zip(edges[:-1], edges[1:]): - if t1 - t0 > self.formatter["axis_break.length"]: - t_l = t0 + 0.5 * self.formatter["axis_break.max_length"] - t_r = t1 - 0.5 * self.formatter["axis_break.max_length"] - axis_breaks.append((t_l, t_r)) - - return axis_breaks - - def set_time_range(self, t_start: float, t_end: float, seconds: bool = True): - """Set time range to draw. - - All child chart instances are updated when time range is updated. - - Args: - t_start: Left boundary of drawing in units of cycle time or real time. - t_end: Right boundary of drawing in units of cycle time or real time. - seconds: Set `True` if times are given in SI unit rather than dt. - - Raises: - VisualizationError: When times are given in float without specifying dt. - """ - # convert into nearest cycle time - if seconds: - if self.device.dt is not None: - t_start = int(np.round(t_start / self.device.dt)) - t_end = int(np.round(t_end / self.device.dt)) - else: - raise VisualizationError( - "Setting time range with SI units requires backend `dt` information." - ) - self.time_range = (t_start, t_end) - - def set_disable_channel(self, channel: pulse.channels.Channel, remove: bool = True): - """Interface method to control visibility of pulse channels. - - Specified object in the blocked list will not be shown. - - Args: - channel: A pulse channel object to disable. - remove: Set `True` to disable, set `False` to enable. - """ - if remove: - self.disable_chans.add(channel) - else: - self.disable_chans.discard(channel) - - def set_disable_type(self, data_type: types.DataTypes, remove: bool = True): - """Interface method to control visibility of data types. - - Specified object in the blocked list will not be shown. - - Args: - data_type: A drawing data type to disable. - remove: Set `True` to disable, set `False` to enable. - """ - if isinstance(data_type, Enum): - data_type_str = str(data_type.value) - else: - data_type_str = data_type - - if remove: - self.disable_types.add(data_type_str) - else: - self.disable_types.discard(data_type_str) - - def update(self): - """Update all associated charts and generate actual drawing data from template object. - - This method should be called before the canvas is passed to the plotter. - """ - for chart in self.charts: - chart.update() - - -class Chart: - """A collection of drawing to be shown on the same line. - - Multiple pulse channels can be assigned to a single `Chart`. - The parent `DrawerCanvas` should be specified to refer to the current user preference. - - The vertical value of each `Chart` should be in the range [-1, 1]. - This truncation should be performed in the plotter interface. - """ - - # unique index of chart - chart_index = 0 - - # list of waveform type names - waveform_types = [ - str(types.WaveformType.REAL.value), - str(types.WaveformType.IMAG.value), - str(types.WaveformType.OPAQUE.value), - ] - - def __init__(self, parent: DrawerCanvas, name: str | None = None): - """Create new chart. - - Args: - parent: `DrawerCanvas` that this `Chart` instance belongs to. - name: Name of this `Chart` instance. - """ - self.parent = parent - - # data stored in this channel - self._collections: dict[str, drawings.ElementaryData] = {} - self._output_dataset: dict[str, drawings.ElementaryData] = {} - - # channel metadata - self.index = self._cls_index() - self.name = name or "" - self._channels: set[pulse.channels.Channel] = set() - - # vertical axis information - self.vmax = 0 - self.vmin = 0 - self.scale = 1.0 - - self._increment_cls_index() - - def add_data(self, data: drawings.ElementaryData): - """Add drawing to collections. - - If the given object already exists in the collections, - this interface replaces the old object instead of adding new entry. - - Args: - data: New drawing to add. - """ - self._collections[data.data_key] = data - - def load_program(self, program: pulse.Schedule, chan: pulse.channels.Channel): - """Load pulse schedule. - - This method internally generates `ChannelEvents` to parse the program - for the specified pulse channel. This method is called once - - Args: - program: Pulse schedule to load. - chan: A pulse channels associated with this instance. - """ - chan_events = events.ChannelEvents.load_program(program, chan) - chan_events.set_config( - dt=self.parent.device.dt, - init_frequency=self.parent.device.get_channel_frequency(chan), - init_phase=0, - ) - - # create objects associated with waveform - for gen in self.parent.generator["waveform"]: - waveforms = chan_events.get_waveforms() - obj_generator = partial(gen, formatter=self.parent.formatter, device=self.parent.device) - drawing_items = [obj_generator(waveform) for waveform in waveforms] - for drawing_item in list(chain.from_iterable(drawing_items)): - self.add_data(drawing_item) - - # create objects associated with frame change - for gen in self.parent.generator["frame"]: - frames = chan_events.get_frame_changes() - obj_generator = partial(gen, formatter=self.parent.formatter, device=self.parent.device) - drawing_items = [obj_generator(frame) for frame in frames] - for drawing_item in list(chain.from_iterable(drawing_items)): - self.add_data(drawing_item) - - self._channels.add(chan) - - def update(self): - """Update vertical data range and scaling factor of this chart. - - Those parameters are updated based on current time range in the parent canvas. - """ - self._output_dataset.clear() - self.vmax = 0 - self.vmin = 0 - - # waveform - for key, data in self._collections.items(): - if data.data_type not in Chart.waveform_types: - continue - - # truncate, assume no abstract coordinate in waveform sample - trunc_x, trunc_y = self._truncate_data(data) - - # no available data points - if trunc_x.size == 0 or trunc_y.size == 0: - continue - - # update y range - scale = min(self.parent.chan_scales.get(chan, 1.0) for chan in data.channels) - self.vmax = max(scale * np.max(trunc_y), self.vmax) - self.vmin = min(scale * np.min(trunc_y), self.vmin) - - # generate new data - new_data = deepcopy(data) - new_data.xvals = trunc_x - new_data.yvals = trunc_y - - self._output_dataset[key] = new_data - - # calculate chart level scaling factor - if self.parent.formatter["control.auto_chart_scaling"]: - max_val = max( - abs(self.vmax), abs(self.vmin), self.parent.formatter["general.vertical_resolution"] - ) - self.scale = min(1.0 / max_val, self.parent.formatter["general.max_scale"]) - else: - self.scale = 1.0 - - # update vertical range with scaling and limitation - self.vmax = max( - self.scale * self.vmax, self.parent.formatter["channel_scaling.pos_spacing"] - ) - - self.vmin = min( - self.scale * self.vmin, self.parent.formatter["channel_scaling.neg_spacing"] - ) - - # other data - for key, data in self._collections.items(): - if data.data_type in Chart.waveform_types: - continue - - # truncate - trunc_x, trunc_y = self._truncate_data(data) - - # no available data points - if trunc_x.size == 0 or trunc_y.size == 0: - continue - - # generate new data - new_data = deepcopy(data) - new_data.xvals = trunc_x - new_data.yvals = trunc_y - - self._output_dataset[key] = new_data - - @property - def is_active(self) -> bool: - """Check if there is any active waveform data in this entry. - - Returns: - Return `True` if there is any visible waveform in this chart. - """ - for data in self._output_dataset.values(): - if data.data_type in Chart.waveform_types and self._check_visible(data): - return True - return False - - @property - def collections(self) -> Iterator[tuple[str, drawings.ElementaryData]]: - """Return currently active entries from drawing data collection. - - The object is returned with unique name as a key of an object handler. - When the horizontal coordinate contains `AbstractCoordinate`, - the value is substituted by current time range preference. - """ - for name, data in self._output_dataset.items(): - # prepare unique name - unique_id = f"chart{self.index:d}_{name}" - if self._check_visible(data): - yield unique_id, data - - @property - def channels(self) -> list[pulse.channels.Channel]: - """Return a list of channels associated with this chart. - - Returns: - List of channels associated with this chart. - """ - return list(self._channels) - - def _truncate_data(self, data: drawings.ElementaryData) -> tuple[np.ndarray, np.ndarray]: - """A helper function to truncate drawings according to time breaks. - - # TODO: move this function to common module to support axis break for timeline. - - Args: - data: Drawing object to truncate. - - Returns: - Set of truncated numpy arrays for x and y coordinate. - """ - xvals = self._bind_coordinate(data.xvals) - yvals = self._bind_coordinate(data.yvals) - - if isinstance(data, drawings.BoxData): - # truncate box data. these object don't require interpolation at axis break. - return self._truncate_boxes(xvals, yvals) - elif data.data_type in [types.LabelType.PULSE_NAME, types.LabelType.OPAQUE_BOXTEXT]: - # truncate pulse labels. these objects are not removed by truncation. - return self._truncate_pulse_labels(xvals, yvals) - else: - # other objects - return self._truncate_vectors(xvals, yvals) - - def _truncate_pulse_labels( - self, xvals: np.ndarray, yvals: np.ndarray - ) -> tuple[np.ndarray, np.ndarray]: - """A helper function to remove text according to time breaks. - - Args: - xvals: Time points. - yvals: Data points. - - Returns: - Set of truncated numpy arrays for x and y coordinate. - """ - xpos = xvals[0] - t0, t1 = self.parent.time_range - - if xpos < t0 or xpos > t1: - return np.array([]), np.array([]) - offset_accumulation = 0 - for tl, tr in self.parent.time_breaks: - if xpos < tl: - return np.array([xpos - offset_accumulation]), yvals - if tl < xpos < tr: - return np.array([tl - offset_accumulation]), yvals - else: - offset_accumulation += tr - tl - return np.array([xpos - offset_accumulation]), yvals - - def _truncate_boxes( - self, xvals: np.ndarray, yvals: np.ndarray - ) -> tuple[np.ndarray, np.ndarray]: - """A helper function to clip box object according to time breaks. - - Args: - xvals: Time points. - yvals: Data points. - - Returns: - Set of truncated numpy arrays for x and y coordinate. - """ - x0, x1 = xvals - t0, t1 = self.parent.time_range - - if x1 < t0 or x0 > t1: - # out of drawing range - return np.array([]), np.array([]) - - # clip outside - x0 = max(t0, x0) - x1 = min(t1, x1) - - offset_accumulate = 0 - for tl, tr in self.parent.time_breaks: - tl -= offset_accumulate - tr -= offset_accumulate - - # - # truncate, there are 5 patterns wrt the relative position of truncation and xvals - # - if x1 < tl: - break - - if tl < x0 and tr > x1: - # case 1: all data points are truncated - # : +-----+ : - # : |/////| : - # -----:---+-----+---:----- - # l 0 1 r - return np.array([]), np.array([]) - elif tl < x1 < tr: - # case 2: t < tl, right side is truncated - # +---:-----+ : - # | ://///| : - # -----+---:-----+---:----- - # 0 l 1 r - x1 = tl - elif tl < x0 < tr: - # case 3: tr > t, left side is truncated - # : +-----:---+ - # : |/////: | - # -----:---+-----:---+----- - # l 0 r 1 - x0 = tl - x1 = tl + t1 - tr - elif tl > x0 and tr < x1: - # case 4: tr > t > tl, middle part is truncated - # +---:-----:---+ - # | ://///: | - # -----+---:-----:---+----- - # 0 l r 1 - x1 -= tr - tl - elif tr < x0: - # case 5: tr > t > tl, nothing truncated but need time shift - # : : +---+ - # : : | | - # -----:---:-----+---+----- - # l r 0 1 - x0 -= tr - tl - x1 -= tr - tl - - offset_accumulate += tr - tl - - return np.asarray([x0, x1], dtype=float), yvals - - def _truncate_vectors( - self, xvals: np.ndarray, yvals: np.ndarray - ) -> tuple[np.ndarray, np.ndarray]: - """A helper function to remove sequential data points according to time breaks. - - Args: - xvals: Time points. - yvals: Data points. - - Returns: - Set of truncated numpy arrays for x and y coordinate. - """ - xvals = np.asarray(xvals, dtype=float) - yvals = np.asarray(yvals, dtype=float) - t0, t1 = self.parent.time_range - - if max(xvals) < t0 or min(xvals) > t1: - # out of drawing range - return np.array([]), np.array([]) - - if min(xvals) < t0: - # truncate x less than left limit - inds = xvals > t0 - yvals = np.append(np.interp(t0, xvals, yvals), yvals[inds]) - xvals = np.append(t0, xvals[inds]) - - if max(xvals) > t1: - # truncate x larger than right limit - inds = xvals < t1 - yvals = np.append(yvals[inds], np.interp(t1, xvals, yvals)) - xvals = np.append(xvals[inds], t1) - - # time breaks - trunc_xvals = [xvals] - trunc_yvals = [yvals] - offset_accumulate = 0 - for tl, tr in self.parent.time_breaks: - sub_xs = trunc_xvals.pop() - sub_ys = trunc_yvals.pop() - tl -= offset_accumulate - tr -= offset_accumulate - - # - # truncate, there are 5 patterns wrt the relative position of truncation and xvals - # - min_xs = min(sub_xs) - max_xs = max(sub_xs) - if max_xs < tl: - trunc_xvals.append(sub_xs) - trunc_yvals.append(sub_ys) - break - - if tl < min_xs and tr > max_xs: - # case 1: all data points are truncated - # : +-----+ : - # : |/////| : - # -----:---+-----+---:----- - # l min max r - return np.array([]), np.array([]) - elif tl < max_xs < tr: - # case 2: t < tl, right side is truncated - # +---:-----+ : - # | ://///| : - # -----+---:-----+---:----- - # min l max r - inds = sub_xs > tl - trunc_xvals.append(np.append(tl, sub_xs[inds]) - (tl - min_xs)) - trunc_yvals.append(np.append(np.interp(tl, sub_xs, sub_ys), sub_ys[inds])) - elif tl < min_xs < tr: - # case 3: tr > t, left side is truncated - # : +-----:---+ - # : |/////: | - # -----:---+-----:---+----- - # l min r max - inds = sub_xs < tr - trunc_xvals.append(np.append(sub_xs[inds], tr)) - trunc_yvals.append(np.append(sub_ys[inds], np.interp(tr, sub_xs, sub_ys))) - elif tl > min_xs and tr < max_xs: - # case 4: tr > t > tl, middle part is truncated - # +---:-----:---+ - # | ://///: | - # -----+---:-----:---+----- - # min l r max - inds0 = sub_xs < tl - trunc_xvals.append(np.append(sub_xs[inds0], tl)) - trunc_yvals.append(np.append(sub_ys[inds0], np.interp(tl, sub_xs, sub_ys))) - inds1 = sub_xs > tr - trunc_xvals.append(np.append(tr, sub_xs[inds1]) - (tr - tl)) - trunc_yvals.append(np.append(np.interp(tr, sub_xs, sub_ys), sub_ys[inds1])) - elif tr < min_xs: - # case 5: tr > t > tl, nothing truncated but need time shift - # : : +---+ - # : : | | - # -----:---:-----+---+----- - # l r 0 1 - trunc_xvals.append(sub_xs - (tr - tl)) - trunc_yvals.append(sub_ys) - else: - # no need to truncate - trunc_xvals.append(sub_xs) - trunc_yvals.append(sub_ys) - offset_accumulate += tr - tl - - new_x = np.concatenate(trunc_xvals) - new_y = np.concatenate(trunc_yvals) - - return np.asarray(new_x, dtype=float), np.asarray(new_y, dtype=float) - - def _bind_coordinate(self, vals: Sequence[types.Coordinate] | np.ndarray) -> np.ndarray: - """A helper function to bind actual coordinates to an `AbstractCoordinate`. - - Args: - vals: Sequence of coordinate objects associated with a drawing. - - Returns: - Numpy data array with substituted values. - """ - - def substitute(val: types.Coordinate): - if val == types.AbstractCoordinate.LEFT: - return self.parent.time_range[0] - if val == types.AbstractCoordinate.RIGHT: - return self.parent.time_range[1] - if val == types.AbstractCoordinate.TOP: - return self.vmax - if val == types.AbstractCoordinate.BOTTOM: - return self.vmin - raise VisualizationError(f"Coordinate {val} is not supported.") - - try: - return np.asarray(vals, dtype=float) - except (TypeError, ValueError): - return np.asarray(list(map(substitute, vals)), dtype=float) - - def _check_visible(self, data: drawings.ElementaryData) -> bool: - """A helper function to check if the data is visible. - - Args: - data: Drawing object to test. - - Returns: - Return `True` if the data is visible. - """ - is_active_type = data.data_type not in self.parent.disable_types - is_active_chan = any(chan not in self.parent.disable_chans for chan in data.channels) - if not (is_active_type and is_active_chan): - return False - - return True - - @classmethod - def _increment_cls_index(cls): - """Increment counter of the chart.""" - cls.chart_index += 1 - - @classmethod - def _cls_index(cls) -> int: - """Return counter index of the chart.""" - return cls.chart_index diff --git a/qiskit/visualization/pulse_v2/device_info.py b/qiskit/visualization/pulse_v2/device_info.py deleted file mode 100644 index 3a3f2688db59..000000000000 --- a/qiskit/visualization/pulse_v2/device_info.py +++ /dev/null @@ -1,137 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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 backend information formatted to generate drawing data. - -This instance will be provided to generator functions. The module provides an abstract -class :py:class:``DrawerBackendInfo`` with necessary methods to generate drawing objects. - -Because the data structure of backend class may depend on providers, this abstract class -has an abstract factory method `create_from_backend`. Each subclass should provide -the factory method which conforms to the associated provider. By default we provide -:py:class:``OpenPulseBackendInfo`` class that has the factory method taking backends -satisfying OpenPulse specification [1]. - -This class can be also initialized without the factory method by manually specifying -required information. This may be convenient for visualizing a pulse program for simulator -backend that only has a device Hamiltonian information. This requires two mapping objects -for channel/qubit and channel/frequency along with the system cycle time. - -If those information are not provided, this class will be initialized with a set of -empty data and the drawer illustrates a pulse program without any specific information. - -Reference: -- [1] Qiskit Backend Specifications for OpenQASM and OpenPulse Experiments, - https://arxiv.org/abs/1809.03452 -""" - -from abc import ABC, abstractmethod -from collections import defaultdict -from typing import Dict, List, Union, Optional - -from qiskit import pulse -from qiskit.providers.backend import Backend, BackendV2 - - -class DrawerBackendInfo(ABC): - """Backend information to be used for the drawing data generation.""" - - def __init__( - self, - name: Optional[str] = None, - dt: Optional[float] = None, - channel_frequency_map: Optional[Dict[pulse.channels.Channel, float]] = None, - qubit_channel_map: Optional[Dict[int, List[pulse.channels.Channel]]] = None, - ): - """Create new backend information. - - Args: - name: Name of the backend. - dt: System cycle time. - channel_frequency_map: Mapping of channel and associated frequency. - qubit_channel_map: Mapping of qubit and associated channels. - """ - self.backend_name = name or "no-backend" - self._dt = dt - self._chan_freq_map = channel_frequency_map or {} - self._qubit_channel_map = qubit_channel_map or {} - - @classmethod - @abstractmethod - def create_from_backend(cls, backend: Backend): - """Initialize a class with backend information provided by provider. - - Args: - backend: Backend object. - """ - raise NotImplementedError - - @property - def dt(self): - """Return cycle time.""" - return self._dt - - def get_qubit_index(self, chan: pulse.channels.Channel) -> Union[int, None]: - """Get associated qubit index of given channel object.""" - for qind, chans in self._qubit_channel_map.items(): - if chan in chans: - return qind - return chan.index - - def get_channel_frequency(self, chan: pulse.channels.Channel) -> Union[float, None]: - """Get frequency of given channel object.""" - return self._chan_freq_map.get(chan, None) - - -class OpenPulseBackendInfo(DrawerBackendInfo): - """Drawing information of backend that conforms to OpenPulse specification.""" - - @classmethod - def create_from_backend(cls, backend: Backend): - """Initialize a class with backend information provided by provider. - - Args: - backend: Backend object. - - Returns: - OpenPulseBackendInfo: New configured instance. - """ - chan_freqs = {} - qubit_channel_map = defaultdict(list) - - if isinstance(backend, BackendV2): - # Pure V2 model doesn't contain channel frequency information. - name = backend.name - dt = backend.dt - - # load qubit channel mapping - for qind in range(backend.num_qubits): - # channels are NotImplemented by default so we must catch arbitrary error. - try: - qubit_channel_map[qind].append(backend.drive_channel(qind)) - except Exception: # pylint: disable=broad-except - pass - try: - qubit_channel_map[qind].append(backend.measure_channel(qind)) - except Exception: # pylint: disable=broad-except - pass - for tind in range(backend.num_qubits): - try: - qubit_channel_map[qind].extend(backend.control_channel(qubits=(qind, tind))) - except Exception: # pylint: disable=broad-except - pass - else: - raise RuntimeError("Backend object not yet supported") - - return OpenPulseBackendInfo( - name=name, dt=dt, channel_frequency_map=chan_freqs, qubit_channel_map=qubit_channel_map - ) diff --git a/qiskit/visualization/pulse_v2/drawings.py b/qiskit/visualization/pulse_v2/drawings.py deleted file mode 100644 index e5a61512a6cd..000000000000 --- a/qiskit/visualization/pulse_v2/drawings.py +++ /dev/null @@ -1,253 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -""" -Drawing objects for pulse drawer. - -Drawing objects play two important roles: - - Allowing unittests of visualization module. Usually it is hard for image files to be tested. - - Removing program parser from each plotter interface. We can easily add new plotter. - -This module is based on the structure of matplotlib as it is the primary plotter -of the pulse drawer. However this interface is agnostic to the actual plotter. - -Design concept -~~~~~~~~~~~~~~ -When we think about dynamically updating drawings, it will be most efficient to -update only the changed properties of drawings rather than regenerating entirely from scratch. -Thus the core :py:class:`qiskit.visualization.pulse_v2.core.DrawerCanvas` generates -all possible drawings in the beginning and then the canvas instance manages -visibility of each drawing according to the end-user request. - -Data key -~~~~~~~~ -In the abstract class ``ElementaryData`` common attributes to represent a drawing are -specified. In addition, drawings have the `data_key` property that returns an -unique hash of the object for comparison. -This key is generated from a data type and the location of the drawing in the canvas. -See py:mod:`qiskit.visualization.pulse_v2.types` for detail on the data type. -If a data key cannot distinguish two independent objects, you need to add a new data type. -The data key may be used in the plotter interface to identify the object. - -Drawing objects -~~~~~~~~~~~~~~~ -To support not only `matplotlib` but also multiple plotters, those drawings should be -universal and designed without strong dependency on modules in `matplotlib`. -This means drawings that represent primitive geometries are preferred. -It should be noted that there will be no unittest for each plotter API, which takes -drawings and outputs image data, we should avoid adding a complicated geometry -that has a context of the pulse program. - -For example, a pulse envelope is complex valued number array and may be represented -by two lines with different colors associated with the real and the imaginary component. -We can use two line-type objects rather than defining a new drawing that takes -complex value. As many plotters don't support an API that visualizes complex-valued -data arrays, if we introduced such a drawing and wrote a custom wrapper function -on top of the existing API, it could be difficult to prevent bugs with the CI tools -due to lack of the effective unittest. -""" -from __future__ import annotations - -from abc import ABC -from enum import Enum -from typing import Any - -import numpy as np - -from qiskit.pulse.channels import Channel -from qiskit.visualization.pulse_v2 import types -from qiskit.visualization.exceptions import VisualizationError - - -class ElementaryData(ABC): - """Base class of the pulse visualization interface.""" - - __hash__ = None - - def __init__( - self, - data_type: str | Enum, - xvals: np.ndarray, - yvals: np.ndarray, - channels: Channel | list[Channel] | None = None, - meta: dict[str, Any] | None = None, - ignore_scaling: bool = False, - styles: dict[str, Any] | None = None, - ): - """Create new drawing. - - Args: - data_type: String representation of this drawing. - xvals: Series of horizontal coordinate that the object is drawn. - yvals: Series of vertical coordinate that the object is drawn. - channels: Pulse channel object bound to this drawing. - meta: Meta data dictionary of the object. - ignore_scaling: Set ``True`` to disable scaling. - styles: Style keyword args of the object. This conforms to `matplotlib`. - """ - if channels and isinstance(channels, Channel): - channels = [channels] - - if isinstance(data_type, Enum): - data_type = data_type.value - - self.data_type = str(data_type) - self.xvals = np.array(xvals, dtype=object) - self.yvals = np.array(yvals, dtype=object) - self.channels: list[Channel] = channels or [] - self.meta = meta or {} - self.ignore_scaling = ignore_scaling - self.styles = styles or {} - - @property - def data_key(self): - """Return unique hash of this object.""" - return str( - hash((self.__class__.__name__, self.data_type, tuple(self.xvals), tuple(self.yvals))) - ) - - def __repr__(self): - return f"{self.__class__.__name__}(type={self.data_type}, key={self.data_key})" - - def __eq__(self, other): - return isinstance(other, self.__class__) and self.data_key == other.data_key - - -class LineData(ElementaryData): - """Drawing object to represent object appears as a line. - - This is the counterpart of `matplotlib.pyplot.plot`. - """ - - def __init__( - self, - data_type: str | Enum, - xvals: np.ndarray | list[types.Coordinate], - yvals: np.ndarray | list[types.Coordinate], - fill: bool = False, - channels: Channel | list[Channel] | None = None, - meta: dict[str, Any] | None = None, - ignore_scaling: bool = False, - styles: dict[str, Any] | None = None, - ): - """Create new drawing. - - Args: - data_type: String representation of this drawing. - channels: Pulse channel object bound to this drawing. - xvals: Series of horizontal coordinate that the object is drawn. - yvals: Series of vertical coordinate that the object is drawn. - fill: Set ``True`` to fill the area under curve. - meta: Meta data dictionary of the object. - ignore_scaling: Set ``True`` to disable scaling. - styles: Style keyword args of the object. This conforms to `matplotlib`. - """ - self.fill = fill - - super().__init__( - data_type=data_type, - xvals=xvals, - yvals=yvals, - channels=channels, - meta=meta, - ignore_scaling=ignore_scaling, - styles=styles, - ) - - -class TextData(ElementaryData): - """Drawing object to represent object appears as a text. - - This is the counterpart of `matplotlib.pyplot.text`. - """ - - def __init__( - self, - data_type: str | Enum, - xvals: np.ndarray | list[types.Coordinate], - yvals: np.ndarray | list[types.Coordinate], - text: str, - latex: str | None = None, - channels: Channel | list[Channel] | None = None, - meta: dict[str, Any] | None = None, - ignore_scaling: bool = False, - styles: dict[str, Any] | None = None, - ): - """Create new drawing. - - Args: - data_type: String representation of this drawing. - channels: Pulse channel object bound to this drawing. - xvals: Series of horizontal coordinate that the object is drawn. - yvals: Series of vertical coordinate that the object is drawn. - text: String to show in the canvas. - latex: Latex representation of the text (if backend supports latex drawing). - meta: Meta data dictionary of the object. - ignore_scaling: Set ``True`` to disable scaling. - styles: Style keyword args of the object. This conforms to `matplotlib`. - """ - self.text = text - self.latex = latex or "" - - super().__init__( - data_type=data_type, - xvals=xvals, - yvals=yvals, - channels=channels, - meta=meta, - ignore_scaling=ignore_scaling, - styles=styles, - ) - - -class BoxData(ElementaryData): - """Drawing object that represents box shape. - - This is the counterpart of `matplotlib.patches.Rectangle`. - """ - - def __init__( - self, - data_type: str | Enum, - xvals: np.ndarray | list[types.Coordinate], - yvals: np.ndarray | list[types.Coordinate], - channels: Channel | list[Channel] | None = None, - meta: dict[str, Any] | None = None, - ignore_scaling: bool = False, - styles: dict[str, Any] | None = None, - ): - """Create new box. - - Args: - data_type: String representation of this drawing. - xvals: Left and right coordinate that the object is drawn. - yvals: Top and bottom coordinate that the object is drawn. - channels: Pulse channel object bound to this drawing. - meta: Meta data dictionary of the object. - ignore_scaling: Set ``True`` to disable scaling. - styles: Style keyword args of the object. This conforms to `matplotlib`. - - Raises: - VisualizationError: When number of data points are not equals to 2. - """ - if len(xvals) != 2 or len(yvals) != 2: - raise VisualizationError("Length of data points are not equals to 2.") - - super().__init__( - data_type=data_type, - xvals=xvals, - yvals=yvals, - channels=channels, - meta=meta, - ignore_scaling=ignore_scaling, - styles=styles, - ) diff --git a/qiskit/visualization/pulse_v2/events.py b/qiskit/visualization/pulse_v2/events.py deleted file mode 100644 index 8830d8e7a1db..000000000000 --- a/qiskit/visualization/pulse_v2/events.py +++ /dev/null @@ -1,260 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -r""" -Channel event manager for pulse schedules. - -This module provides a `ChannelEvents` class that manages a series of instructions for a -pulse channel. Channel-wise filtering of the pulse program makes -the arrangement of channels easier in the core drawer function. -The `ChannelEvents` class is expected to be called by other programs (not by end-users). - -The `ChannelEvents` class instance is created with the class method ``load_program``: - -.. plot:: - :include-source: - :nofigs: - - event = ChannelEvents.load_program(sched, DriveChannel(0)) - -The `ChannelEvents` is created for a specific pulse channel and loosely assorts pulse -instructions within the channel with different visualization purposes. - -Phase and frequency related instructions are loosely grouped as frame changes. -The instantaneous value of those operands are combined and provided as ``PhaseFreqTuple``. -Instructions that have finite duration are grouped as waveforms. - -The grouped instructions are returned as an iterator by the corresponding method call: - -.. plot:: - :include-source: - :nofigs: - - for t0, frame, instruction in event.get_waveforms(): - ... - - for t0, frame_change, instructions in event.get_frame_changes(): - ... - -The class method ``get_waveforms`` returns the iterator of waveform type instructions with -the ``PhaseFreqTuple`` (frame) at the time when instruction is issued. -This is because a pulse envelope of ``Waveform`` may be modulated with a -phase factor $exp(-i \omega t - \phi)$ with frequency $\omega$ and phase $\phi$ and -appear on the canvas. Thus, it is better to tell users in which phase and frequency -the pulse envelope is modulated from a viewpoint of program debugging. - -On the other hand, the class method ``get_frame_changes`` returns a ``PhaseFreqTuple`` that -represents a total amount of change at that time because it is convenient to know -the operand value itself when we debug a program. - -Because frame change type instructions are usually zero duration, multiple instructions -can be issued at the same time and those operand values should be appropriately -combined. In Qiskit Pulse we have set and shift type instructions for the frame control, -the set type instruction will be converted into the relevant shift amount for visualization. -Note that these instructions are not interchangeable and the order should be kept. -For example: - -.. plot:: - :include-source: - :nofigs: - - sched1 = Schedule() - sched1 = sched1.insert(0, ShiftPhase(-1.57, DriveChannel(0)) - sched1 = sched1.insert(0, SetPhase(3.14, DriveChannel(0)) - - sched2 = Schedule() - sched2 = sched2.insert(0, SetPhase(3.14, DriveChannel(0)) - sched2 = sched2.insert(0, ShiftPhase(-1.57, DriveChannel(0)) - -In this example, ``sched1`` and ``sched2`` will have different frames. -On the drawer canvas, the total frame change amount of +3.14 should be shown for ``sched1``, -while ``sched2`` is +1.57. Since the `SetPhase` and the `ShiftPhase` instruction behave -differently, we cannot simply sum up the operand values in visualization output. - -It should be also noted that zero duration instructions issued at the same time will be -overlapped on the canvas. Thus it is convenient to plot a total frame change amount rather -than plotting each operand value bound to the instruction. -""" -from __future__ import annotations -from collections import defaultdict -from collections.abc import Iterator - -from qiskit import pulse, circuit -from qiskit.visualization.pulse_v2.types import PhaseFreqTuple, PulseInstruction - - -class ChannelEvents: - """Channel event manager.""" - - _waveform_group = ( - pulse.instructions.Play, - pulse.instructions.Delay, - pulse.instructions.Acquire, - ) - _frame_group = ( - pulse.instructions.SetFrequency, - pulse.instructions.ShiftFrequency, - pulse.instructions.SetPhase, - pulse.instructions.ShiftPhase, - ) - - def __init__( - self, - waveforms: dict[int, pulse.Instruction], - frames: dict[int, list[pulse.Instruction]], - channel: pulse.channels.Channel, - ): - """Create new event manager. - - Args: - waveforms: List of waveforms shown in this channel. - frames: List of frame change type instructions shown in this channel. - channel: Channel object associated with this manager. - """ - self._waveforms = waveforms - self._frames = frames - self.channel = channel - - # initial frame - self._init_phase = 0.0 - self._init_frequency = 0.0 - - # time resolution - self._dt = 0.0 - - @classmethod - def load_program(cls, program: pulse.Schedule, channel: pulse.channels.Channel): - """Load a pulse program represented by ``Schedule``. - - Args: - program: Target ``Schedule`` to visualize. - channel: The channel managed by this instance. - - Returns: - ChannelEvents: The channel event manager for the specified channel. - """ - waveforms = {} - frames = defaultdict(list) - - # parse instructions - for t0, inst in program.filter(channels=[channel]).instructions: - if isinstance(inst, cls._waveform_group): - if inst.duration == 0: - # special case, duration of delay can be zero - continue - waveforms[t0] = inst - elif isinstance(inst, cls._frame_group): - frames[t0].append(inst) - - return ChannelEvents(waveforms, frames, channel) - - def set_config(self, dt: float, init_frequency: float, init_phase: float): - """Setup system status. - - Args: - dt: Time resolution in sec. - init_frequency: Modulation frequency in Hz. - init_phase: Initial phase in rad. - """ - self._dt = dt or 1.0 - self._init_frequency = init_frequency or 0.0 - self._init_phase = init_phase or 0.0 - - def get_waveforms(self) -> Iterator[PulseInstruction]: - """Return waveform type instructions with frame.""" - sorted_frame_changes = sorted(self._frames.items(), key=lambda x: x[0], reverse=True) - sorted_waveforms = sorted(self._waveforms.items(), key=lambda x: x[0]) - - # bind phase and frequency with instruction - phase = self._init_phase - frequency = self._init_frequency - for t0, inst in sorted_waveforms: - is_opaque = False - - while len(sorted_frame_changes) > 0 and sorted_frame_changes[-1][0] <= t0: - _, frame_changes = sorted_frame_changes.pop() - phase, frequency = ChannelEvents._calculate_current_frame( - frame_changes=frame_changes, phase=phase, frequency=frequency - ) - - # Convert parameter expression into float - if isinstance(phase, circuit.ParameterExpression): - phase = float(phase.bind({param: 0 for param in phase.parameters})) - if isinstance(frequency, circuit.ParameterExpression): - frequency = float(frequency.bind({param: 0 for param in frequency.parameters})) - - frame = PhaseFreqTuple(phase, frequency) - - # Check if pulse has unbound parameters - if isinstance(inst, pulse.Play): - is_opaque = inst.pulse.is_parameterized() - - yield PulseInstruction(t0, self._dt, frame, inst, is_opaque) - - def get_frame_changes(self) -> Iterator[PulseInstruction]: - """Return frame change type instructions with total frame change amount.""" - # TODO parse parametrized FCs correctly - - sorted_frame_changes = sorted(self._frames.items(), key=lambda x: x[0]) - - phase = self._init_phase - frequency = self._init_frequency - for t0, frame_changes in sorted_frame_changes: - is_opaque = False - - pre_phase = phase - pre_frequency = frequency - phase, frequency = ChannelEvents._calculate_current_frame( - frame_changes=frame_changes, phase=phase, frequency=frequency - ) - - # keep parameter expression to check either phase or frequency is parameterized - frame = PhaseFreqTuple(phase - pre_phase, frequency - pre_frequency) - - # remove parameter expressions to find if next frame is parameterized - if isinstance(phase, circuit.ParameterExpression): - phase = float(phase.bind({param: 0 for param in phase.parameters})) - is_opaque = True - if isinstance(frequency, circuit.ParameterExpression): - frequency = float(frequency.bind({param: 0 for param in frequency.parameters})) - is_opaque = True - - yield PulseInstruction(t0, self._dt, frame, frame_changes, is_opaque) - - @classmethod - def _calculate_current_frame( - cls, frame_changes: list[pulse.instructions.Instruction], phase: float, frequency: float - ) -> tuple[float, float]: - """Calculate the current frame from the previous frame. - - If parameter is unbound phase or frequency accumulation with this instruction is skipped. - - Args: - frame_changes: List of frame change instructions at a specific time. - phase: Phase of previous frame. - frequency: Frequency of previous frame. - - Returns: - Phase and frequency of new frame. - """ - - for frame_change in frame_changes: - if isinstance(frame_change, pulse.instructions.SetFrequency): - frequency = frame_change.frequency - elif isinstance(frame_change, pulse.instructions.ShiftFrequency): - frequency += frame_change.frequency - elif isinstance(frame_change, pulse.instructions.SetPhase): - phase = frame_change.phase - elif isinstance(frame_change, pulse.instructions.ShiftPhase): - phase += frame_change.phase - - return phase, frequency diff --git a/qiskit/visualization/pulse_v2/generators/__init__.py b/qiskit/visualization/pulse_v2/generators/__init__.py deleted file mode 100644 index fd671857eb90..000000000000 --- a/qiskit/visualization/pulse_v2/generators/__init__.py +++ /dev/null @@ -1,40 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -""" -Customizable object generators for pulse drawer. -""" - -from qiskit.visualization.pulse_v2.generators.barrier import gen_barrier - -from qiskit.visualization.pulse_v2.generators.chart import ( - gen_baseline, - gen_channel_freqs, - gen_chart_name, - gen_chart_scale, -) - -from qiskit.visualization.pulse_v2.generators.frame import ( - gen_formatted_frame_values, - gen_formatted_freq_mhz, - gen_formatted_phase, - gen_frame_symbol, - gen_raw_operand_values_compact, -) - -from qiskit.visualization.pulse_v2.generators.snapshot import gen_snapshot_name, gen_snapshot_symbol - -from qiskit.visualization.pulse_v2.generators.waveform import ( - gen_filled_waveform_stepwise, - gen_ibmq_latex_waveform_name, - gen_waveform_max_value, -) diff --git a/qiskit/visualization/pulse_v2/generators/barrier.py b/qiskit/visualization/pulse_v2/generators/barrier.py deleted file mode 100644 index 85f8271cf142..000000000000 --- a/qiskit/visualization/pulse_v2/generators/barrier.py +++ /dev/null @@ -1,76 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -# pylint: disable=unused-argument - -"""Barrier generators. - -A collection of functions that generate drawings from formatted input data. -See py:mod:`qiskit.visualization.pulse_v2.types` for more info on the required data. - -In this module the input data is `types.BarrierInstruction`. - -An end-user can write arbitrary functions that generate custom drawings. -Generators in this module are called with the `formatter` and `device` kwargs. -These data provides stylesheet configuration and backend system configuration. - -The format of generator is restricted to: - - ```python - - def my_object_generator(data: BarrierInstruction, - formatter: Dict[str, Any], - device: DrawerBackendInfo) -> List[ElementaryData]: - pass - ``` - -Arbitrary generator function satisfying the above format can be accepted. -Returned `ElementaryData` can be arbitrary subclasses that are implemented in -the plotter API. -""" -from typing import Dict, Any, List - -from qiskit.visualization.pulse_v2 import drawings, types, device_info - - -def gen_barrier( - data: types.BarrierInstruction, formatter: Dict[str, Any], device: device_info.DrawerBackendInfo -) -> List[drawings.LineData]: - """Generate the barrier from provided relative barrier instruction. - - Stylesheets: - - The `barrier` style is applied. - - Args: - data: Barrier instruction data to draw. - formatter: Dictionary of stylesheet settings. - device: Backend configuration. - Returns: - List of `LineData` drawings. - """ - style = { - "alpha": formatter["alpha.barrier"], - "zorder": formatter["layer.barrier"], - "linewidth": formatter["line_width.barrier"], - "linestyle": formatter["line_style.barrier"], - "color": formatter["color.barrier"], - } - - line = drawings.LineData( - data_type=types.LineType.BARRIER, - channels=data.channels, - xvals=[data.t0, data.t0], - yvals=[types.AbstractCoordinate.BOTTOM, types.AbstractCoordinate.TOP], - styles=style, - ) - - return [line] diff --git a/qiskit/visualization/pulse_v2/generators/chart.py b/qiskit/visualization/pulse_v2/generators/chart.py deleted file mode 100644 index 26a92fe3695e..000000000000 --- a/qiskit/visualization/pulse_v2/generators/chart.py +++ /dev/null @@ -1,208 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -# pylint: disable=unused-argument - -"""Chart axis generators. - -A collection of functions that generate drawings from formatted input data. -See py:mod:`qiskit.visualization.pulse_v2.types` for more info on the required data. - -In this module the input data is `types.ChartAxis`. - -An end-user can write arbitrary functions that generate custom drawings. -Generators in this module are called with the `formatter` and `device` kwargs. -These data provides stylesheet configuration and backend system configuration. - -The format of generator is restricted to: - - ```python - - def my_object_generator(data: ChartAxis, - formatter: Dict[str, Any], - device: DrawerBackendInfo) -> List[ElementaryData]: - pass - ``` - -Arbitrary generator function satisfying the above format can be accepted. -Returned `ElementaryData` can be arbitrary subclasses that are implemented in -the plotter API. -""" -from typing import Dict, Any, List - -from qiskit.visualization.pulse_v2 import drawings, types, device_info - - -def gen_baseline( - data: types.ChartAxis, formatter: Dict[str, Any], device: device_info.DrawerBackendInfo -) -> List[drawings.LineData]: - """Generate the baseline associated with the chart. - - Stylesheets: - - The `baseline` style is applied. - - Args: - data: Chart axis data to draw. - formatter: Dictionary of stylesheet settings. - device: Backend configuration. - - Returns: - List of `LineData` drawings. - """ - style = { - "alpha": formatter["alpha.baseline"], - "zorder": formatter["layer.baseline"], - "linewidth": formatter["line_width.baseline"], - "linestyle": formatter["line_style.baseline"], - "color": formatter["color.baseline"], - } - - baseline = drawings.LineData( - data_type=types.LineType.BASELINE, - channels=data.channels, - xvals=[types.AbstractCoordinate.LEFT, types.AbstractCoordinate.RIGHT], - yvals=[0, 0], - ignore_scaling=True, - styles=style, - ) - - return [baseline] - - -def gen_chart_name( - data: types.ChartAxis, formatter: Dict[str, Any], device: device_info.DrawerBackendInfo -) -> List[drawings.TextData]: - """Generate the name of chart. - - Stylesheets: - - The `axis_label` style is applied. - - Args: - data: Chart axis data to draw. - formatter: Dictionary of stylesheet settings. - device: Backend configuration. - - Returns: - List of `TextData` drawings. - """ - style = { - "zorder": formatter["layer.axis_label"], - "color": formatter["color.axis_label"], - "size": formatter["text_size.axis_label"], - "va": "center", - "ha": "right", - } - - text = drawings.TextData( - data_type=types.LabelType.CH_NAME, - channels=data.channels, - xvals=[types.AbstractCoordinate.LEFT], - yvals=[0], - text=data.name, - ignore_scaling=True, - styles=style, - ) - - return [text] - - -def gen_chart_scale( - data: types.ChartAxis, formatter: Dict[str, Any], device: device_info.DrawerBackendInfo -) -> List[drawings.TextData]: - """Generate the current scaling value of the chart. - - Stylesheets: - - The `axis_label` style is applied. - - The `annotate` style is partially applied for the font size. - - Args: - data: Chart axis data to draw. - formatter: Dictionary of stylesheet settings. - device: Backend configuration. - - Returns: - List of `TextData` drawings. - """ - style = { - "zorder": formatter["layer.axis_label"], - "color": formatter["color.axis_label"], - "size": formatter["text_size.annotate"], - "va": "center", - "ha": "right", - } - - scale_val = f"x{types.DynamicString.SCALE}" - - text = drawings.TextData( - data_type=types.LabelType.CH_INFO, - channels=data.channels, - xvals=[types.AbstractCoordinate.LEFT], - yvals=[-formatter["label_offset.chart_info"]], - text=scale_val, - ignore_scaling=True, - styles=style, - ) - - return [text] - - -def gen_channel_freqs( - data: types.ChartAxis, formatter: Dict[str, Any], device: device_info.DrawerBackendInfo -) -> List[drawings.TextData]: - """Generate the frequency values of associated channels. - - Stylesheets: - - The `axis_label` style is applied. - - The `annotate` style is partially applied for the font size. - - Args: - data: Chart axis data to draw. - formatter: Dictionary of stylesheet settings. - device: Backend configuration. - - Returns: - List of `TextData` drawings. - """ - style = { - "zorder": formatter["layer.axis_label"], - "color": formatter["color.axis_label"], - "size": formatter["text_size.annotate"], - "va": "center", - "ha": "right", - } - - if len(data.channels) > 1: - sources = [] - for chan in data.channels: - freq = device.get_channel_frequency(chan) - if not freq: - continue - sources.append(f"{chan.name.upper()}: {freq / 1e9:.2f} GHz") - freq_text = ", ".join(sources) - else: - freq = device.get_channel_frequency(data.channels[0]) - if freq: - freq_text = f"{freq / 1e9:.2f} GHz" - else: - freq_text = "" - - text = drawings.TextData( - data_type=types.LabelType.CH_INFO, - channels=data.channels, - xvals=[types.AbstractCoordinate.LEFT], - yvals=[-formatter["label_offset.chart_info"]], - text=freq_text or "no freq.", - ignore_scaling=True, - styles=style, - ) - - return [text] diff --git a/qiskit/visualization/pulse_v2/generators/frame.py b/qiskit/visualization/pulse_v2/generators/frame.py deleted file mode 100644 index 8b71b8596bb4..000000000000 --- a/qiskit/visualization/pulse_v2/generators/frame.py +++ /dev/null @@ -1,436 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -# pylint: disable=unused-argument - -"""Frame change generators. - -A collection of functions that generate drawings from formatted input data. -See py:mod:`qiskit.visualization.pulse_v2.types` for more info on the required data. - -In this module the input data is `types.PulseInstruction`. - -An end-user can write arbitrary functions that generate custom drawings. -Generators in this module are called with the `formatter` and `device` kwargs. -These data provides stylesheet configuration and backend system configuration. - -The format of generator is restricted to: - - ```python - - def my_object_generator(data: PulseInstruction, - formatter: Dict[str, Any], - device: DrawerBackendInfo) -> List[ElementaryData]: - pass - ``` - -Arbitrary generator function satisfying the above format can be accepted. -Returned `ElementaryData` can be arbitrary subclasses that are implemented in -the plotter API. -""" -from fractions import Fraction -from typing import Dict, Any, List, Tuple - -import numpy as np -from qiskit.pulse import instructions -from qiskit.visualization.exceptions import VisualizationError -from qiskit.visualization.pulse_v2 import drawings, types, device_info - - -def gen_formatted_phase( - data: types.PulseInstruction, formatter: Dict[str, Any], device: device_info.DrawerBackendInfo -) -> List[drawings.TextData]: - """Generate the formatted virtual Z rotation label from provided frame instruction. - - Rotation angle is expressed in units of pi. - If the denominator of fraction is larger than 10, the angle is expressed in units of radian. - - For example: - - A value -3.14 is converted into `VZ(\\pi)` - - A value 1.57 is converted into `VZ(-\\frac{\\pi}{2})` - - A value 0.123 is converted into `VZ(-0.123 rad.)` - - Stylesheets: - - The `frame_change` style is applied. - - The `annotate` style is applied for font size. - - Notes: - The phase operand of `PhaseShift` instruction has opposite sign to the Z gate definition. - Thus the sign of rotation angle is inverted. - - Args: - data: Frame change instruction data to draw. - formatter: Dictionary of stylesheet settings. - device: Backend configuration. - - Returns: - List of `TextData` drawings. - """ - _max_denom = 10 - - style = { - "zorder": formatter["layer.frame_change"], - "color": formatter["color.frame_change"], - "size": formatter["text_size.annotate"], - "va": "center", - "ha": "center", - } - - plain_phase, latex_phase = _phase_to_text( - formatter=formatter, phase=data.frame.phase, max_denom=_max_denom, flip=True - ) - - text = drawings.TextData( - data_type=types.LabelType.FRAME, - channels=data.inst[0].channel, - xvals=[data.t0], - yvals=[formatter["label_offset.frame_change"]], - text=f"VZ({plain_phase})", - latex=rf"{{\rm VZ}}({latex_phase})", - ignore_scaling=True, - styles=style, - ) - - return [text] - - -def gen_formatted_freq_mhz( - data: types.PulseInstruction, formatter: Dict[str, Any], device: device_info.DrawerBackendInfo -) -> List[drawings.TextData]: - """Generate the formatted frequency change label from provided frame instruction. - - Frequency change is expressed in units of MHz. - - For example: - - A value 1,234,567 is converted into `\\Delta f = 1.23 MHz` - - Stylesheets: - - The `frame_change` style is applied. - - The `annotate` style is applied for font size. - - Args: - data: Frame change instruction data to draw. - formatter: Dictionary of stylesheet settings. - device: Backend configuration. - - Returns: - List of `TextData` drawings. - """ - _unit = "MHz" - - style = { - "zorder": formatter["layer.frame_change"], - "color": formatter["color.frame_change"], - "size": formatter["text_size.annotate"], - "va": "center", - "ha": "center", - } - - plain_freq, latex_freq = _freq_to_text(formatter=formatter, freq=data.frame.freq, unit=_unit) - - text = drawings.TextData( - data_type=types.LabelType.FRAME, - channels=data.inst[0].channel, - xvals=[data.t0], - yvals=[formatter["label_offset.frame_change"]], - text=f"\u0394f = {plain_freq}", - latex=rf"\Delta f = {latex_freq}", - ignore_scaling=True, - styles=style, - ) - - return [text] - - -def gen_formatted_frame_values( - data: types.PulseInstruction, formatter: Dict[str, Any], device: device_info.DrawerBackendInfo -) -> List[drawings.TextData]: - """Generate the formatted virtual Z rotation label and the frequency change label - from provided frame instruction. - - Phase value is placed on top of the symbol, and frequency value is placed below the symbol. - See :py:func:`gen_formatted_phase` and :py:func:`gen_formatted_freq_mhz` for details. - - Stylesheets: - - The `frame_change` style is applied. - - The `annotate` style is applied for font size. - - Args: - data: Frame change instruction data to draw. - formatter: Dictionary of stylesheet settings. - device: Backend configuration. - - Returns: - List of `TextData` drawings. - """ - texts = [] - - _max_denom = 10 - _unit = "MHz" - - style = { - "zorder": formatter["layer.frame_change"], - "color": formatter["color.frame_change"], - "size": formatter["text_size.annotate"], - "ha": "center", - } - - # phase value - if data.frame.phase != 0: - plain_phase, latex_phase = _phase_to_text( - formatter=formatter, phase=data.frame.phase, max_denom=_max_denom, flip=True - ) - phase_style = {"va": "center"} - phase_style.update(style) - - phase = drawings.TextData( - data_type=types.LabelType.FRAME, - channels=data.inst[0].channel, - xvals=[data.t0], - yvals=[formatter["label_offset.frame_change"]], - text=f"VZ({plain_phase})", - latex=rf"{{\rm VZ}}({latex_phase})", - ignore_scaling=True, - styles=phase_style, - ) - texts.append(phase) - - # frequency value - if data.frame.freq != 0: - plain_freq, latex_freq = _freq_to_text( - formatter=formatter, freq=data.frame.freq, unit=_unit - ) - freq_style = {"va": "center"} - freq_style.update(style) - - freq = drawings.TextData( - data_type=types.LabelType.FRAME, - channels=data.inst[0].channel, - xvals=[data.t0], - yvals=[2 * formatter["label_offset.frame_change"]], - text=f"\u0394f = {plain_freq}", - latex=rf"\Delta f = {latex_freq}", - ignore_scaling=True, - styles=freq_style, - ) - texts.append(freq) - - return texts - - -def gen_raw_operand_values_compact( - data: types.PulseInstruction, formatter: Dict[str, Any], device: device_info.DrawerBackendInfo -) -> List[drawings.TextData]: - """Generate the formatted virtual Z rotation label and the frequency change label - from provided frame instruction. - - Raw operand values are shown in compact form. Frequency change is expressed - in scientific notation. Values are shown in two lines. - - For example: - - A phase change 1.57 and frequency change 1,234,567 are written by `1.57\\n1.2e+06` - - Stylesheets: - - The `frame_change` style is applied. - - The `annotate` style is applied for font size. - - Args: - data: Frame change instruction data to draw. - formatter: Dictionary of stylesheet settings. - device: Backend configuration. - - Returns: - List of `TextData` drawings. - """ - - style = { - "zorder": formatter["layer.frame_change"], - "color": formatter["color.frame_change"], - "size": formatter["text_size.annotate"], - "va": "center", - "ha": "center", - } - - if data.frame.freq == 0: - freq_sci_notation = "0.0" - else: - abs_freq = np.abs(data.frame.freq) - base = data.frame.freq / (10 ** int(np.floor(np.log10(abs_freq)))) - exponent = int(np.floor(np.log10(abs_freq))) - freq_sci_notation = f"{base:.1f}e{exponent:d}" - frame_info = f"{data.frame.phase:.2f}\n{freq_sci_notation}" - - text = drawings.TextData( - data_type=types.LabelType.FRAME, - channels=data.inst[0].channel, - xvals=[data.t0], - yvals=[1.2 * formatter["label_offset.frame_change"]], - text=frame_info, - ignore_scaling=True, - styles=style, - ) - - return [text] - - -def gen_frame_symbol( - data: types.PulseInstruction, formatter: Dict[str, Any], device: device_info.DrawerBackendInfo -) -> List[drawings.TextData]: - """Generate a frame change symbol with instruction meta data from provided frame instruction. - - Stylesheets: - - The `frame_change` style is applied. - - The symbol type in unicode is specified in `formatter.unicode_symbol.frame_change`. - - The symbol type in latex is specified in `formatter.latex_symbol.frame_change`. - - Args: - data: Frame change instruction data to draw. - formatter: Dictionary of stylesheet settings. - device: Backend configuration. - - Returns: - List of `TextData` drawings. - """ - if data.frame.phase == 0 and data.frame.freq == 0: - return [] - - style = { - "zorder": formatter["layer.frame_change"], - "color": formatter["color.frame_change"], - "size": formatter["text_size.frame_change"], - "va": "center", - "ha": "center", - } - - program = [] - for inst in data.inst: - if isinstance(inst, (instructions.SetFrequency, instructions.ShiftFrequency)): - try: - program.append(f"{inst.__class__.__name__}({inst.frequency:.2e} Hz)") - except TypeError: - # parameter expression - program.append(f"{inst.__class__.__name__}({inst.frequency})") - elif isinstance(inst, (instructions.SetPhase, instructions.ShiftPhase)): - try: - program.append(f"{inst.__class__.__name__}({inst.phase:.2f} rad.)") - except TypeError: - # parameter expression - program.append(f"{inst.__class__.__name__}({inst.phase})") - - meta = { - "total phase change": data.frame.phase, - "total frequency change": data.frame.freq, - "program": ", ".join(program), - "t0 (cycle time)": data.t0, - "t0 (sec)": data.t0 * data.dt if data.dt else "N/A", - } - - text = drawings.TextData( - data_type=types.SymbolType.FRAME, - channels=data.inst[0].channel, - xvals=[data.t0], - yvals=[0], - text=formatter["unicode_symbol.frame_change"], - latex=formatter["latex_symbol.frame_change"], - ignore_scaling=True, - meta=meta, - styles=style, - ) - - return [text] - - -def _phase_to_text( - formatter: Dict[str, Any], phase: float, max_denom: int = 10, flip: bool = True -) -> Tuple[str, str]: - """A helper function to convert a float value to text with pi. - - Args: - formatter: Dictionary of stylesheet settings. - phase: A phase value in units of rad. - max_denom: Maximum denominator. Return raw value if exceed. - flip: Set `True` to flip the sign. - - Returns: - Standard text and latex text of phase value. - """ - try: - phase = float(phase) - except TypeError: - # unbound parameter - return ( - formatter["unicode_symbol.phase_parameter"], - formatter["latex_symbol.phase_parameter"], - ) - - frac = Fraction(np.abs(phase) / np.pi) - - if phase == 0: - return "0", r"0" - - num = frac.numerator - denom = frac.denominator - if denom > max_denom: - # denominator is too large - latex = rf"{np.abs(phase):.2f}" - plain = f"{np.abs(phase):.2f}" - else: - if num == 1: - if denom == 1: - latex = r"\pi" - plain = "pi" - else: - latex = rf"\pi/{denom:d}" - plain = f"pi/{denom:d}" - else: - latex = rf"{num:d}/{denom:d} \pi" - plain = f"{num:d}/{denom:d} pi" - - if flip: - sign = "-" if phase > 0 else "" - else: - sign = "-" if phase < 0 else "" - - return sign + plain, sign + latex - - -def _freq_to_text(formatter: Dict[str, Any], freq: float, unit: str = "MHz") -> Tuple[str, str]: - """A helper function to convert a freq value to text with supplementary unit. - - Args: - formatter: Dictionary of stylesheet settings. - freq: A frequency value in units of Hz. - unit: Supplementary unit. THz, GHz, MHz, kHz, Hz are supported. - - Returns: - Standard text and latex text of phase value. - - Raises: - VisualizationError: When unsupported unit is specified. - """ - try: - freq = float(freq) - except TypeError: - # unbound parameter - return formatter["unicode_symbol.freq_parameter"], formatter["latex_symbol.freq_parameter"] - - unit_table = {"THz": 1e12, "GHz": 1e9, "MHz": 1e6, "kHz": 1e3, "Hz": 1} - - try: - value = freq / unit_table[unit] - except KeyError as ex: - raise VisualizationError(f"Unit {unit} is not supported.") from ex - - latex = rf"{value:.2f}~{{\rm {unit}}}" - plain = f"{value:.2f} {unit}" - - return plain, latex diff --git a/qiskit/visualization/pulse_v2/generators/snapshot.py b/qiskit/visualization/pulse_v2/generators/snapshot.py deleted file mode 100644 index d86c89997a02..000000000000 --- a/qiskit/visualization/pulse_v2/generators/snapshot.py +++ /dev/null @@ -1,133 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -# pylint: disable=unused-argument - -"""Snapshot generators. - -A collection of functions that generate drawings from formatted input data. -See py:mod:`qiskit.visualization.pulse_v2.types` for more info on the required data. - -In this module the input data is `types.SnapshotInstruction`. - -An end-user can write arbitrary functions that generate custom drawings. -Generators in this module are called with the `formatter` and `device` kwargs. -These data provides stylesheet configuration and backend system configuration. - -The format of generator is restricted to: - - ```python - - def my_object_generator(data: SnapshotInstruction, - formatter: Dict[str, Any], - device: DrawerBackendInfo) -> List[ElementaryData]: - pass - ``` - -Arbitrary generator function satisfying the above format can be accepted. -Returned `ElementaryData` can be arbitrary subclasses that are implemented in -the plotter API. -""" -from typing import Dict, Any, List - -from qiskit.visualization.pulse_v2 import drawings, types, device_info - - -def gen_snapshot_name( - data: types.SnapshotInstruction, - formatter: Dict[str, Any], - device: device_info.DrawerBackendInfo, -) -> List[drawings.TextData]: - """Generate the name of snapshot. - - Stylesheets: - - The `snapshot` style is applied for snapshot symbol. - - The `annotate` style is applied for label font size. - - Args: - data: Snapshot instruction data to draw. - formatter: Dictionary of stylesheet settings. - device: Backend configuration. - - Returns: - List of `TextData` drawings. - """ - style = { - "zorder": formatter["layer.snapshot"], - "color": formatter["color.snapshot"], - "size": formatter["text_size.annotate"], - "va": "center", - "ha": "center", - } - - text = drawings.TextData( - data_type=types.LabelType.SNAPSHOT, - channels=data.inst.channel, - xvals=[data.t0], - yvals=[formatter["label_offset.snapshot"]], - text=data.inst.name, - ignore_scaling=True, - styles=style, - ) - - return [text] - - -def gen_snapshot_symbol( - data: types.SnapshotInstruction, - formatter: Dict[str, Any], - device: device_info.DrawerBackendInfo, -) -> List[drawings.TextData]: - """Generate a snapshot symbol with instruction meta data from provided snapshot instruction. - - Stylesheets: - - The `snapshot` style is applied for snapshot symbol. - - The symbol type in unicode is specified in `formatter.unicode_symbol.snapshot`. - - The symbol type in latex is specified in `formatter.latex_symbol.snapshot`. - - Args: - data: Snapshot instruction data to draw. - formatter: Dictionary of stylesheet settings. - device: Backend configuration. - - Returns: - List of `TextData` drawings. - """ - style = { - "zorder": formatter["layer.snapshot"], - "color": formatter["color.snapshot"], - "size": formatter["text_size.snapshot"], - "va": "bottom", - "ha": "center", - } - - meta = { - "snapshot type": data.inst.type, - "t0 (cycle time)": data.t0, - "t0 (sec)": data.t0 * data.dt if data.dt else "N/A", - "name": data.inst.name, - "label": data.inst.label, - } - - text = drawings.TextData( - data_type=types.SymbolType.SNAPSHOT, - channels=data.inst.channel, - xvals=[data.t0], - yvals=[0], - text=formatter["unicode_symbol.snapshot"], - latex=formatter["latex_symbol.snapshot"], - ignore_scaling=True, - meta=meta, - styles=style, - ) - - return [text] diff --git a/qiskit/visualization/pulse_v2/generators/waveform.py b/qiskit/visualization/pulse_v2/generators/waveform.py deleted file mode 100644 index e770f271c454..000000000000 --- a/qiskit/visualization/pulse_v2/generators/waveform.py +++ /dev/null @@ -1,645 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -# pylint: disable=unused-argument - -"""Waveform generators. - -A collection of functions that generate drawings from formatted input data. -See py:mod:`qiskit.visualization.pulse_v2.types` for more info on the required data. - -In this module the input data is `types.PulseInstruction`. - -An end-user can write arbitrary functions that generate custom drawings. -Generators in this module are called with the `formatter` and `device` kwargs. -These data provides stylesheet configuration and backend system configuration. - -The format of generator is restricted to: - - ```python - - def my_object_generator(data: PulseInstruction, - formatter: Dict[str, Any], - device: DrawerBackendInfo) -> List[ElementaryData]: - pass - ``` - -Arbitrary generator function satisfying the above format can be accepted. -Returned `ElementaryData` can be arbitrary subclasses that are implemented in -the plotter API. -""" - -from __future__ import annotations -import re -from fractions import Fraction -from typing import Any - -import numpy as np - -from qiskit import pulse, circuit -from qiskit.pulse import instructions, library -from qiskit.visualization.exceptions import VisualizationError -from qiskit.visualization.pulse_v2 import drawings, types, device_info - - -def gen_filled_waveform_stepwise( - data: types.PulseInstruction, formatter: dict[str, Any], device: device_info.DrawerBackendInfo -) -> list[drawings.LineData | drawings.BoxData | drawings.TextData]: - """Generate filled area objects of the real and the imaginary part of waveform envelope. - - The curve of envelope is not interpolated nor smoothed and presented - as stepwise function at each data point. - - Stylesheets: - - The `fill_waveform` style is applied. - - Args: - data: Waveform instruction data to draw. - formatter: Dictionary of stylesheet settings. - device: Backend configuration. - - Returns: - List of `LineData`, `BoxData`, or `TextData` drawings. - - Raises: - VisualizationError: When the instruction parser returns invalid data format. - """ - # generate waveform data - waveform_data = _parse_waveform(data) - channel = data.inst.channel - - # update metadata - meta = waveform_data.meta - qind = device.get_qubit_index(channel) - meta.update({"qubit": qind if qind is not None else "N/A"}) - - if isinstance(waveform_data, types.ParsedInstruction): - # Draw waveform with fixed shape - - xdata = waveform_data.xvals - ydata = waveform_data.yvals - - # phase modulation - if formatter["control.apply_phase_modulation"]: - ydata = np.asarray(ydata, dtype=complex) * np.exp(1j * data.frame.phase) - else: - ydata = np.asarray(ydata, dtype=complex) - - return _draw_shaped_waveform( - xdata=xdata, ydata=ydata, meta=meta, channel=channel, formatter=formatter - ) - - elif isinstance(waveform_data, types.OpaqueShape): - # Draw parametric pulse with unbound parameters - - # parameter name - unbound_params = [] - for pname, pval in data.inst.pulse.parameters.items(): - if isinstance(pval, circuit.ParameterExpression): - unbound_params.append(pname) - - pulse_data = data.inst.pulse - if isinstance(pulse_data, library.SymbolicPulse): - pulse_shape = pulse_data.pulse_type - else: - pulse_shape = "Waveform" - - return _draw_opaque_waveform( - init_time=data.t0, - duration=waveform_data.duration, - pulse_shape=pulse_shape, - pnames=unbound_params, - meta=meta, - channel=channel, - formatter=formatter, - ) - - else: - raise VisualizationError("Invalid data format is provided.") - - -def gen_ibmq_latex_waveform_name( - data: types.PulseInstruction, formatter: dict[str, Any], device: device_info.DrawerBackendInfo -) -> list[drawings.TextData]: - r"""Generate the formatted instruction name associated with the waveform. - - Channel name and ID string are removed and the rotation angle is expressed in units of pi. - The controlled rotation angle associated with the CR pulse name is divided by 2. - - Note that in many scientific articles the controlled rotation angle implies - the actual rotation angle, but in IQX backend the rotation angle represents - the difference between rotation angles with different control qubit states. - - For example: - - 'X90p_d0_abcdefg' is converted into 'X(\frac{\pi}{2})' - - 'CR90p_u0_abcdefg` is converted into 'CR(\frac{\pi}{4})' - - Stylesheets: - - The `annotate` style is applied. - - Notes: - This generator can convert pulse names used in the IQX backends. - If pulses are provided by the third party providers or the user defined, - the generator output may be the as-is pulse name. - - Args: - data: Waveform instruction data to draw. - formatter: Dictionary of stylesheet settings. - device: Backend configuration. - - Returns: - List of `TextData` drawings. - """ - if data.is_opaque: - return [] - - style = { - "zorder": formatter["layer.annotate"], - "color": formatter["color.annotate"], - "size": formatter["text_size.annotate"], - "va": "center", - "ha": "center", - } - - if isinstance(data.inst, pulse.instructions.Acquire): - systematic_name = "Acquire" - latex_name = None - elif isinstance(data.inst, instructions.Delay): - systematic_name = data.inst.name or "Delay" - latex_name = None - else: - pulse_data = data.inst.pulse - if pulse_data.name: - systematic_name = pulse_data.name - else: - if isinstance(pulse_data, library.SymbolicPulse): - systematic_name = pulse_data.pulse_type - else: - systematic_name = "Waveform" - - template = r"(?P[A-Z]+)(?P[0-9]+)?(?P[pm])_(?P[dum])[0-9]+" - match_result = re.match(template, systematic_name) - if match_result is not None: - match_dict = match_result.groupdict() - sign = "" if match_dict["sign"] == "p" else "-" - if match_dict["op"] == "CR": - # cross resonance - if match_dict["ch"] == "u": - op_name = r"{\rm CR}" - else: - op_name = r"\overline{\rm CR}" - # IQX name def is not standard. Echo CR is annotated with pi/4 rather than pi/2 - angle_val = match_dict["angle"] - frac = Fraction(int(int(angle_val) / 2), 180) - if frac.numerator == 1: - angle = rf"\pi/{frac.denominator:d}" - else: - angle = rf"{frac.numerator:d}/{frac.denominator:d} \pi" - else: - # single qubit pulse - # pylint: disable-next=consider-using-f-string - op_name = r"{{\rm {}}}".format(match_dict["op"]) - angle_val = match_dict["angle"] - if angle_val is None: - angle = r"\pi" - else: - frac = Fraction(int(angle_val), 180) - if frac.numerator == 1: - angle = rf"\pi/{frac.denominator:d}" - else: - angle = rf"{frac.numerator:d}/{frac.denominator:d} \pi" - latex_name = rf"{op_name}({sign}{angle})" - else: - latex_name = None - - text = drawings.TextData( - data_type=types.LabelType.PULSE_NAME, - channels=data.inst.channel, - xvals=[data.t0 + 0.5 * data.inst.duration], - yvals=[-formatter["label_offset.pulse_name"]], - text=systematic_name, - latex=latex_name, - ignore_scaling=True, - styles=style, - ) - - return [text] - - -def gen_waveform_max_value( - data: types.PulseInstruction, formatter: dict[str, Any], device: device_info.DrawerBackendInfo -) -> list[drawings.TextData]: - """Generate the annotation for the maximum waveform height for - the real and the imaginary part of the waveform envelope. - - Maximum values smaller than the vertical resolution limit is ignored. - - Stylesheets: - - The `annotate` style is applied. - - Args: - data: Waveform instruction data to draw. - formatter: Dictionary of stylesheet settings. - device: Backend configuration. - - Returns: - List of `TextData` drawings. - """ - if data.is_opaque: - return [] - - style = { - "zorder": formatter["layer.annotate"], - "color": formatter["color.annotate"], - "size": formatter["text_size.annotate"], - "ha": "center", - } - - # only pulses. - if isinstance(data.inst, instructions.Play): - # pulse - operand = data.inst.pulse - if isinstance(operand, pulse.SymbolicPulse): - pulse_data = operand.get_waveform() - else: - pulse_data = operand - xdata = np.arange(pulse_data.duration) + data.t0 - ydata = pulse_data.samples - else: - return [] - - # phase modulation - if formatter["control.apply_phase_modulation"]: - ydata = np.asarray(ydata, dtype=complex) * np.exp(1j * data.frame.phase) - else: - ydata = np.asarray(ydata, dtype=complex) - - texts = [] - - # max of real part - re_maxind = np.argmax(np.abs(ydata.real)) - if np.abs(ydata.real[re_maxind]) > 0.01: - # generator shows only 2 digits after the decimal point. - if ydata.real[re_maxind] > 0: - max_val = f"{ydata.real[re_maxind]:.2f}\n\u25BE" - re_style = {"va": "bottom"} - else: - max_val = f"\u25B4\n{ydata.real[re_maxind]:.2f}" - re_style = {"va": "top"} - re_style.update(style) - re_text = drawings.TextData( - data_type=types.LabelType.PULSE_INFO, - channels=data.inst.channel, - xvals=[xdata[re_maxind]], - yvals=[ydata.real[re_maxind]], - text=max_val, - styles=re_style, - ) - texts.append(re_text) - - # max of imag part - im_maxind = np.argmax(np.abs(ydata.imag)) - if np.abs(ydata.imag[im_maxind]) > 0.01: - # generator shows only 2 digits after the decimal point. - if ydata.imag[im_maxind] > 0: - max_val = f"{ydata.imag[im_maxind]:.2f}\n\u25BE" - im_style = {"va": "bottom"} - else: - max_val = f"\u25B4\n{ydata.imag[im_maxind]:.2f}" - im_style = {"va": "top"} - im_style.update(style) - im_text = drawings.TextData( - data_type=types.LabelType.PULSE_INFO, - channels=data.inst.channel, - xvals=[xdata[im_maxind]], - yvals=[ydata.imag[im_maxind]], - text=max_val, - styles=im_style, - ) - texts.append(im_text) - - return texts - - -def _draw_shaped_waveform( - xdata: np.ndarray, - ydata: np.ndarray, - meta: dict[str, Any], - channel: pulse.channels.PulseChannel, - formatter: dict[str, Any], -) -> list[drawings.LineData | drawings.BoxData | drawings.TextData]: - """A private function that generates drawings of stepwise pulse lines. - - Args: - xdata: Array of horizontal coordinate of waveform envelope. - ydata: Array of vertical coordinate of waveform envelope. - meta: Metadata dictionary of the waveform. - channel: Channel associated with the waveform to draw. - formatter: Dictionary of stylesheet settings. - - Returns: - List of drawings. - - Raises: - VisualizationError: When the waveform color for channel is not defined. - """ - fill_objs: list[drawings.LineData | drawings.BoxData | drawings.TextData] = [] - - resolution = formatter["general.vertical_resolution"] - - # stepwise interpolation - xdata: np.ndarray = np.concatenate((xdata, [xdata[-1] + 1])) - ydata = np.repeat(ydata, 2) - re_y = np.real(ydata) - im_y = np.imag(ydata) - time: np.ndarray = np.concatenate(([xdata[0]], np.repeat(xdata[1:-1], 2), [xdata[-1]])) - - # setup style options - style = { - "alpha": formatter["alpha.fill_waveform"], - "zorder": formatter["layer.fill_waveform"], - "linewidth": formatter["line_width.fill_waveform"], - "linestyle": formatter["line_style.fill_waveform"], - } - - try: - color_real, color_imag = formatter["color.waveforms"][channel.prefix.upper()] - except KeyError as ex: - raise VisualizationError( - f"Waveform color for channel type {channel.prefix} is not defined" - ) from ex - - # create real part - if np.any(re_y): - # data compression - re_valid_inds = _find_consecutive_index(re_y, resolution) - # stylesheet - re_style = {"color": color_real} - re_style.update(style) - # metadata - re_meta = {"data": "real"} - re_meta.update(meta) - # active xy data - re_xvals = time[re_valid_inds] - re_yvals = re_y[re_valid_inds] - - # object - real = drawings.LineData( - data_type=types.WaveformType.REAL, - channels=channel, - xvals=re_xvals, - yvals=re_yvals, - fill=formatter["control.fill_waveform"], - meta=re_meta, - styles=re_style, - ) - fill_objs.append(real) - - # create imaginary part - if np.any(im_y): - # data compression - im_valid_inds = _find_consecutive_index(im_y, resolution) - # stylesheet - im_style = {"color": color_imag} - im_style.update(style) - # metadata - im_meta = {"data": "imag"} - im_meta.update(meta) - # active xy data - im_xvals = time[im_valid_inds] - im_yvals = im_y[im_valid_inds] - - # object - imag = drawings.LineData( - data_type=types.WaveformType.IMAG, - channels=channel, - xvals=im_xvals, - yvals=im_yvals, - fill=formatter["control.fill_waveform"], - meta=im_meta, - styles=im_style, - ) - fill_objs.append(imag) - - return fill_objs - - -def _draw_opaque_waveform( - init_time: int, - duration: int, - pulse_shape: str, - pnames: list[str], - meta: dict[str, Any], - channel: pulse.channels.PulseChannel, - formatter: dict[str, Any], -) -> list[drawings.LineData | drawings.BoxData | drawings.TextData]: - """A private function that generates drawings of stepwise pulse lines. - - Args: - init_time: Time when the opaque waveform starts. - duration: Duration of opaque waveform. This can be None or ParameterExpression. - pulse_shape: String that represents pulse shape. - pnames: List of parameter names. - meta: Metadata dictionary of the waveform. - channel: Channel associated with the waveform to draw. - formatter: Dictionary of stylesheet settings. - - Returns: - List of drawings. - """ - fill_objs: list[drawings.LineData | drawings.BoxData | drawings.TextData] = [] - - fc, ec = formatter["color.opaque_shape"] - # setup style options - box_style = { - "zorder": formatter["layer.fill_waveform"], - "alpha": formatter["alpha.opaque_shape"], - "linewidth": formatter["line_width.opaque_shape"], - "linestyle": formatter["line_style.opaque_shape"], - "facecolor": fc, - "edgecolor": ec, - } - - if duration is None or isinstance(duration, circuit.ParameterExpression): - duration = formatter["box_width.opaque_shape"] - - box_obj = drawings.BoxData( - data_type=types.WaveformType.OPAQUE, - channels=channel, - xvals=[init_time, init_time + duration], - yvals=[ - -0.5 * formatter["box_height.opaque_shape"], - 0.5 * formatter["box_height.opaque_shape"], - ], - meta=meta, - ignore_scaling=True, - styles=box_style, - ) - fill_objs.append(box_obj) - - # parameter name - func_repr = f"{pulse_shape}({', '.join(pnames)})" - - text_style = { - "zorder": formatter["layer.annotate"], - "color": formatter["color.annotate"], - "size": formatter["text_size.annotate"], - "va": "bottom", - "ha": "center", - } - - text_obj = drawings.TextData( - data_type=types.LabelType.OPAQUE_BOXTEXT, - channels=channel, - xvals=[init_time + 0.5 * duration], - yvals=[0.5 * formatter["box_height.opaque_shape"]], - text=func_repr, - ignore_scaling=True, - styles=text_style, - ) - - fill_objs.append(text_obj) - - return fill_objs - - -def _find_consecutive_index(data_array: np.ndarray, resolution: float) -> np.ndarray: - """A helper function to return non-consecutive index from the given list. - - This drastically reduces memory footprint to represent a drawing, - especially for samples of very long flat-topped Gaussian pulses. - Tiny value fluctuation smaller than `resolution` threshold is removed. - - Args: - data_array: The array of numbers. - resolution: Minimum resolution of sample values. - - Returns: - The compressed data array. - """ - try: - vector = np.asarray(data_array, dtype=float) - diff = np.diff(vector) - diff[np.where(np.abs(diff) < resolution)] = 0 - # keep left and right edges - consecutive_l = np.insert(diff.astype(bool), 0, True) - consecutive_r = np.append(diff.astype(bool), True) - return consecutive_l | consecutive_r - - except ValueError: - return np.ones_like(data_array).astype(bool) - - -def _parse_waveform( - data: types.PulseInstruction, -) -> types.ParsedInstruction | types.OpaqueShape: - """A helper function that generates an array for the waveform with - instruction metadata. - - Args: - data: Instruction data set - - Raises: - VisualizationError: When invalid instruction type is loaded. - - Returns: - A data source to generate a drawing. - """ - inst = data.inst - - meta: dict[str, Any] = {} - if isinstance(inst, instructions.Play): - # pulse - operand = inst.pulse - if isinstance(operand, pulse.SymbolicPulse): - # parametric pulse - params = operand.parameters - duration = params.pop("duration", None) - if isinstance(duration, circuit.Parameter): - duration = None - - if isinstance(operand, library.SymbolicPulse): - pulse_shape = operand.pulse_type - else: - pulse_shape = "Waveform" - meta["waveform shape"] = pulse_shape - - meta.update( - { - key: val.name if isinstance(val, circuit.Parameter) else val - for key, val in params.items() - } - ) - if data.is_opaque: - # parametric pulse with unbound parameter - if duration: - meta.update( - { - "duration (cycle time)": inst.duration, - "duration (sec)": inst.duration * data.dt if data.dt else "N/A", - } - ) - else: - meta.update({"duration (cycle time)": "N/A", "duration (sec)": "N/A"}) - - meta.update( - { - "t0 (cycle time)": data.t0, - "t0 (sec)": data.t0 * data.dt if data.dt else "N/A", - "phase": data.frame.phase, - "frequency": data.frame.freq, - "name": inst.name, - } - ) - - return types.OpaqueShape(duration=duration, meta=meta) - else: - # fixed shape parametric pulse - pulse_data = operand.get_waveform() - else: - # waveform - pulse_data = operand - xdata = np.arange(pulse_data.duration) + data.t0 - ydata = pulse_data.samples - elif isinstance(inst, instructions.Delay): - # delay - xdata = np.arange(inst.duration) + data.t0 - ydata = np.zeros(inst.duration) - elif isinstance(inst, instructions.Acquire): - # acquire - xdata = np.arange(inst.duration) + data.t0 - ydata = np.ones(inst.duration) - acq_data = { - "memory slot": inst.mem_slot.name, - "register slot": inst.reg_slot.name if inst.reg_slot else "N/A", - "discriminator": inst.discriminator.name if inst.discriminator else "N/A", - "kernel": inst.kernel.name if inst.kernel else "N/A", - } - meta.update(acq_data) - else: - raise VisualizationError( - f"Unsupported instruction {inst.__class__.__name__} by " "filled envelope." - ) - - meta.update( - { - "duration (cycle time)": inst.duration, - "duration (sec)": inst.duration * data.dt if data.dt else "N/A", - "t0 (cycle time)": data.t0, - "t0 (sec)": data.t0 * data.dt if data.dt else "N/A", - "phase": data.frame.phase, - "frequency": data.frame.freq, - "name": inst.name, - } - ) - - return types.ParsedInstruction(xvals=xdata, yvals=ydata, meta=meta) diff --git a/qiskit/visualization/pulse_v2/interface.py b/qiskit/visualization/pulse_v2/interface.py deleted file mode 100644 index b879c73224dd..000000000000 --- a/qiskit/visualization/pulse_v2/interface.py +++ /dev/null @@ -1,463 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Qiskit pulse drawer. - -This module provides a common user interface for the pulse drawer. -The `draw` function takes a pulse program to visualize with a stylesheet and -backend information along with several control arguments. -The drawer canvas object is internally initialized from the input data and -the configured canvas is passed to the one of plotter APIs to generate visualization data. -""" - -from typing import Union, Optional, Dict, Any, Tuple, List - -from qiskit.providers import Backend -from qiskit.pulse import Waveform, SymbolicPulse, Schedule, ScheduleBlock -from qiskit.pulse.channels import Channel -from qiskit.visualization.exceptions import VisualizationError -from qiskit.visualization.pulse_v2 import core, device_info, stylesheet, types -from qiskit.exceptions import MissingOptionalLibraryError -from qiskit.utils import deprecate_arg -from qiskit.utils.deprecate_pulse import deprecate_pulse_dependency - - -@deprecate_pulse_dependency(moving_to_dynamics=True) -@deprecate_arg("show_barrier", new_alias="plot_barrier", since="1.1.0", pending=True) -def draw( - program: Union[Waveform, SymbolicPulse, Schedule, ScheduleBlock], - style: Optional[Dict[str, Any]] = None, - backend: Optional[Backend] = None, - time_range: Optional[Tuple[int, int]] = None, - time_unit: str = types.TimeUnits.CYCLES.value, - disable_channels: Optional[List[Channel]] = None, - show_snapshot: bool = True, - show_framechange: bool = True, - show_waveform_info: bool = True, - plot_barrier: bool = True, - plotter: str = types.Plotter.Mpl2D.value, - axis: Optional[Any] = None, - show_barrier: bool = True, -): - """Generate visualization data for pulse programs. - - Args: - program: Program to visualize. This program can be arbitrary Qiskit Pulse program, - such as :py:class:`~qiskit.pulse.Waveform`, :py:class:`~qiskit.pulse.SymbolicPulse`, - :py:class:`~qiskit.pulse.Schedule` and :py:class:`~qiskit.pulse.ScheduleBlock`. - style: Stylesheet options. This can be dictionary or preset stylesheet classes. See - :py:class:`~qiskit.visualization.pulse_v2.stylesheets.IQXStandard`, - :py:class:`~qiskit.visualization.pulse_v2.stylesheets.IQXSimple`, and - :py:class:`~qiskit.visualization.pulse_v2.stylesheets.IQXDebugging` for details of - preset stylesheets. See also the stylesheet section for details of configuration keys. - backend: Backend object to play the input pulse program. If provided, the plotter - may use to make the visualization hardware aware. - time_range: Set horizontal axis limit. Tuple ``(tmin, tmax)``. - time_unit: The unit of specified time range either ``dt`` or ``ns``. - The unit of ``ns`` is available only when ``backend`` object is provided. - disable_channels: A control property to show specific pulse channel. - Pulse channel instances provided as a list is not shown in the output image. - show_snapshot: Show snapshot instructions. - show_framechange: Show frame change instructions. The frame change represents - instructions that modulate phase or frequency of pulse channels. - show_waveform_info: Show waveform annotations, i.e. name, of waveforms. - Set ``True`` to show additional information about waveforms. - plot_barrier: Show barrier lines. - plotter: Name of plotter API to generate an output image. - One of following APIs should be specified:: - - mpl2d: Matplotlib API for 2D image generation. - Matplotlib API to generate 2D image. Charts are placed along y axis with - vertical offset. This API takes matplotlib.axes.Axes as `axis` input. - - `axis` and `style` kwargs may depend on the plotter. - axis: Arbitrary object passed to the plotter. If this object is provided, - the plotters use a given ``axis`` instead of internally initializing - a figure object. This object format depends on the plotter. - See plotter argument for details. - show_barrier: DEPRECATED. Show barrier lines. - - Returns: - Visualization output data. - The returned data type depends on the `plotter`. - If matplotlib family is specified, this will be a `matplotlib.pyplot.Figure` data. - The returned data is generated by the :meth:`get_image` method of the specified plotter API. - - .. _style-dict-doc: - - **Style Dict Details** - - The stylesheet kwarg contains numerous options that define the style of the - output pulse visualization. - The stylesheet options can be classified into `formatter`, `generator` and `layout`. - Those options available in the stylesheet are defined below: - - Args: - formatter.general.fig_width: Width of output image (default `13`). - formatter.general.fig_chart_height: Height of output image per chart. - The height of each chart is multiplied with this factor and the - sum of all chart heights becomes the height of output image (default `1.5`). - formatter.general.vertical_resolution: Vertical resolution of the pulse envelope. - The change of data points below this limit is ignored (default `1e-6`). - formatter.general.max_scale: Maximum scaling factor of each chart. This factor is - considered when chart auto-scaling is enabled (default `100`). - formatter.color.waveforms: A dictionary of the waveform colors to use for - each element type in the output visualization. The default values are:: - - { - 'W': `['#648fff', '#002999']`, - 'D': `['#648fff', '#002999']`, - 'U': `['#ffb000', '#994A00']`, - 'M': `['#dc267f', '#760019']`, - 'A': `['#dc267f', '#760019']` - } - - formatter.color.baseline: Color code of lines of zero line of each chart - (default `'#000000'`). - formatter.color.barrier: Color code of lines of barrier (default `'#222222'`). - formatter.color.background: Color code of the face color of canvas - (default `'#f2f3f4'`). - formatter.color.fig_title: Color code of the figure title text - (default `'#000000'`). - formatter.color.annotate: Color code of annotation texts in the canvas - (default `'#222222'`). - formatter.color.frame_change: Color code of the symbol for frame changes - (default `'#000000'`). - formatter.color.snapshot: Color code of the symbol for snapshot - (default `'#000000'`) - formatter.color.opaque_shape: Color code of the face and edge of opaque shape box - (default `['#fffacd', '#000000']`) - formatter.color.axis_label: Color code of axis labels (default `'#000000'`). - formatter.alpha.fill_waveform: Transparency of waveforms. A value in the range from - `0` to `1`. The value `0` gives completely transparent waveforms (default `0.3`). - formatter.alpha.baseline: Transparency of base lines. A value in the range from - `0` to `1`. The value `0` gives completely transparent base lines (default `1.0`). - formatter.alpha.barrier: Transparency of barrier lines. A value in the range from - `0` to `1`. The value `0` gives completely transparent barrier lines (default `0.7`). - formatter.alpha.opaque_shape: Transparency of opaque shape box. A value in the range from - `0` to `1`. The value `0` gives completely transparent barrier lines (default `0.7`). - formatter.layer.fill_waveform: Layer index of waveforms. Larger number comes - in the front of the output image (default `2`). - formatter.layer.baseline: Layer index of baselines. Larger number comes - in the front of the output image (default `1`). - formatter.layer.barrier: Layer index of barrier lines. Larger number comes - in the front of the output image (default `1`). - formatter.layer.annotate: Layer index of annotations. Larger number comes - in the front of the output image (default `5`). - formatter.layer.axis_label: Layer index of axis labels. Larger number comes - in the front of the output image (default `5`). - formatter.layer.frame_change: Layer index of frame change symbols. Larger number comes - in the front of the output image (default `4`). - formatter.layer.snapshot: Layer index of snapshot symbols. Larger number comes - in the front of the output image (default `3`). - formatter.layer.fig_title: Layer index of the figure title. Larger number comes - in the front of the output image (default `6`). - formatter.margin.top: Margin from the top boundary of the figure canvas to - the surface of the first chart (default `0.5`). - formatter.margin.bottom: Margin from the bottom boundary of the figure canvas to - the surface of the last chart (default `0.5`). - formatter.margin.left_percent: Margin from the left boundary of the figure canvas to - the zero point of the horizontal axis. The value is in units of percentage of - the whole program duration. If the duration is 100 and the value of 0.5 is set, - this keeps left margin of 5 (default `0.05`). - formatter.margin.right_percent: Margin from the right boundary of the figure canvas to - the left limit of the horizontal axis. The value is in units of percentage of - the whole program duration. If the duration is 100 and the value of 0.5 is set, - this keeps right margin of 5 (default `0.05`). - formatter.margin.between_channel: Vertical margin between charts (default `0.2`). - formatter.label_offset.pulse_name: Offset of pulse name annotations from the - chart baseline (default `0.3`). - formatter.label_offset.chart_info: Offset of chart info annotations from the - chart baseline (default `0.3`). - formatter.label_offset.frame_change: Offset of frame change annotations from the - chart baseline (default `0.3`). - formatter.label_offset.snapshot: Offset of snapshot annotations from the - chart baseline (default `0.3`). - formatter.text_size.axis_label: Text size of axis labels (default `15`). - formatter.text_size.annotate: Text size of annotations (default `12`). - formatter.text_size.frame_change: Text size of frame change symbols (default `20`). - formatter.text_size.snapshot: Text size of snapshot symbols (default `20`). - formatter.text_size.fig_title: Text size of the figure title (default `15`). - formatter.text_size.axis_break_symbol: Text size of axis break symbols (default `15`). - formatter.line_width.fill_waveform: Line width of the fringe of filled waveforms - (default `0`). - formatter.line_width.axis_break: Line width of axis breaks. - The axis break line paints over other drawings with the background - face color (default `6`). - formatter.line_width.baseline: Line width of base lines (default `1`) - formatter.line_width.barrier: Line width of barrier lines (default `1`). - formatter.line_width.opaque_shape: Line width of opaque shape box (default `1`). - formatter.line_style.fill_waveform: Line style of the fringe of filled waveforms. This - conforms to the line style spec of matplotlib (default `'-'`). - formatter.line_style.baseline: Line style of base lines. This - conforms to the line style spec of matplotlib (default `'-'`). - formatter.line_style.barrier: Line style of barrier lines. This - conforms to the line style spec of matplotlib (default `':'`). - formatter.line_style.opaque_shape: Line style of opaque shape box. This - conforms to the line style spec of matplotlib (default `'--'`). - formatter.channel_scaling.drive: Default scaling value of drive channel - waveforms (default `1.0`). - formatter.channel_scaling.control: Default scaling value of control channel - waveforms (default `1.0`). - formatter.channel_scaling.measure: Default scaling value of measure channel - waveforms (default `1.0`). - formatter.channel_scaling.acquire: Default scaling value of acquire channel - waveforms (default `1.0`). - formatter.channel_scaling.pos_spacing: Minimum height of chart above the baseline. - Chart top is determined based on the maximum height of waveforms associated - with the chart. If the maximum height is below this value, this value is set - as the chart top (default 0.1). - formatter.channel_scaling.neg_spacing: Minimum height of chart below the baseline. - Chart bottom is determined based on the minimum height of waveforms associated - with the chart. If the minimum height is above this value, this value is set - as the chart bottom (default -0.1). - formatter.box_width.opaque_shape: Default box length of the waveform representation - when the instruction is parameterized and duration is not bound or not defined. - Value is units in dt (default: 150). - formatter.box_height.opaque_shape: Default box height of the waveform representation - when the instruction is parameterized (default: 0.4). - formatter.axis_break.length: Waveform or idle time duration that axis break is - applied. Intervals longer than this value are truncated. - The value is in units of data points (default `3000`). - formatter.axis_break.max_length: Length of new waveform or idle time duration - after axis break is applied. Longer intervals are truncated to this length - (default `1000`). - formatter.control.fill_waveform: Set `True` to fill waveforms with face color - (default `True`). When you disable this option, you should set finite line width - to `formatter.line_width.fill_waveform`, otherwise nothing will appear in the graph. - formatter.control.apply_phase_modulation: Set `True` to apply phase modulation - to the waveforms (default `True`). - formatter.control.show_snapshot_channel: Set `True` to show snapshot instructions - (default `True`). - formatter.control.show_acquire_channel: Set `True` to show acquire channels - (default `True`). - formatter.control.show_empty_channel: Set `True` to show charts without any waveforms - (default `True`). - formatter.control.auto_chart_scaling: Set `True` to apply auto-scaling to charts - (default `True`). - formatter.control.axis_break: Set `True` to apply axis break for long intervals - (default `True`). - formatter.unicode_symbol.frame_change: Text that represents the symbol of - frame change. This text is used when the plotter doesn't support latex - (default u'\u21BA'). - formatter.unicode_symbol.snapshot: Text that represents the symbol of - snapshot. This text is used when the plotter doesn't support latex - (default u'\u21AF'). - formatter.unicode_symbol.phase_parameter: Text that represents the symbol of - parameterized phase value. This text is used when the plotter doesn't support latex - (default u'\u03b8'). - formatter.unicode_symbol.freq_parameter: Text that represents the symbol of - parameterized frequency value. This text is used when the plotter doesn't support latex - (default 'f'). - formatter.latex_symbol.frame_change: Latex text that represents the symbol of - frame change (default r'\\circlearrowleft'). - formatter.latex_symbol.snapshot: Latex text that represents the symbol of - snapshot (default ''). - formatter.latex_symbol.phase_parameter: Latex text that represents the symbol of - parameterized phase value (default r'\theta'). - formatter.latex_symbol.freq_parameter: Latex text that represents the symbol of - parameterized frequency value (default 'f'). - generator.waveform: List of callback functions that generates drawing - for waveforms. Arbitrary callback functions satisfying the generator format - can be set here. There are some default generators in the pulse drawer. - See :py:mod:`~qiskit.visualization.pulse_v2.generators.waveform` for more details. - No default generator is set. - generator.frame: List of callback functions that generates drawing - for frame changes. Arbitrary callback functions satisfying the generator format - can be set here. There are some default generators in the pulse drawer. - See :py:mod:`~qiskit.visualization.pulse_v2.generators.frame` for more details. - No default generator is set. - generator.chart: List of callback functions that generates drawing - for charts. Arbitrary callback functions satisfying the generator format - can be set here. There are some default generators in the pulse drawer. - See :py:mod:`~qiskit.visualization.pulse_v2.generators.chart` for more details. - No default generator is set. - generator.snapshot: List of callback functions that generates drawing - for snapshots. Arbitrary callback functions satisfying the generator format - can be set here. There are some default generators in the pulse drawer. - See :py:mod:`~qiskit.visualization.pulse_v2.generators.snapshot` for more details. - No default generator is set. - generator.barrier: List of callback functions that generates drawing - for barriers. Arbitrary callback functions satisfying the generator format - can be set here. There are some default generators in the pulse drawer. - See :py:mod:`~qiskit.visualization.pulse_v2.generators.barrier` for more details. - No default generator is set. - layout.chart_channel_map: Callback function that determines the relationship - between pulse channels and charts. - See :py:mod:`~qiskit.visualization.pulse_v2.layout` for more details. - No default layout is set. - layout.time_axis_map: Callback function that determines the layout of - horizontal axis labels. - See :py:mod:`~qiskit.visualization.pulse_v2.layout` for more details. - No default layout is set. - layout.figure_title: Callback function that generates a string for - the figure title. - See :py:mod:`~qiskit.visualization.pulse_v2.layout` for more details. - No default layout is set. - - Examples: - To visualize a pulse program, you can call this function with set of - control arguments. Most of the appearance of the output image can be controlled by the - stylesheet. - - Drawing with the default stylesheet. - - .. plot:: - :alt: Output from the previous code. - :include-source: - - from qiskit import QuantumCircuit, transpile, schedule - from qiskit.visualization.pulse_v2 import draw - from qiskit.providers.fake_provider import GenericBackendV2 - - qc = QuantumCircuit(2) - qc.h(0) - qc.cx(0, 1) - qc.measure_all() - qc = transpile(qc, GenericBackendV2(5), layout_method='trivial') - sched = schedule(qc, GenericBackendV2(5)) - - draw(sched, backend=GenericBackendV2(5)) - - Drawing with the stylesheet suited for publication. - - .. plot:: - :alt: Output from the previous code. - :include-source: - - from qiskit import QuantumCircuit, transpile, schedule - from qiskit.visualization.pulse_v2 import draw, IQXSimple - from qiskit.providers.fake_provider import GenericBackendV2 - - qc = QuantumCircuit(2) - qc.h(0) - qc.cx(0, 1) - qc.measure_all() - qc = transpile(qc, GenericBackendV2(5), layout_method='trivial') - sched = schedule(qc, GenericBackendV2(5)) - - draw(sched, style=IQXSimple(), backend=GenericBackendV2(5)) - - Drawing with the stylesheet suited for program debugging. - - .. plot:: - :alt: Output from the previous code. - :include-source: - - from qiskit import QuantumCircuit, transpile, schedule - from qiskit.visualization.pulse_v2 import draw, IQXDebugging - from qiskit.providers.fake_provider import GenericBackendV2 - - qc = QuantumCircuit(2) - qc.h(0) - qc.cx(0, 1) - qc.measure_all() - qc = transpile(qc, GenericBackendV2(5), layout_method='trivial') - sched = schedule(qc, GenericBackendV2(5)) - - draw(sched, style=IQXDebugging(), backend=GenericBackendV2(5)) - - You can partially customize a preset stylesheet when initializing it. - - .. plot:: - :include-source: - :nofigs: - - my_style = { - 'formatter.channel_scaling.drive': 5, - 'formatter.channel_scaling.control': 1, - 'formatter.channel_scaling.measure': 5 - } - style = IQXStandard(**my_style) - # draw - draw(sched, style=style, backend=GenericBackendV2(5)) - - In the same way as above, you can create custom generator or layout functions - and update the existing stylesheet with custom functions. - This feature enables you to customize most of the appearance of the output image - without modifying the codebase. - - Raises: - MissingOptionalLibraryError: When required visualization package is not installed. - VisualizationError: When invalid plotter API or invalid time range is specified. - """ - del show_barrier - temp_style = stylesheet.QiskitPulseStyle() - temp_style.update(style or stylesheet.IQXStandard()) - - if backend: - device = device_info.OpenPulseBackendInfo.create_from_backend(backend) - else: - device = device_info.OpenPulseBackendInfo() - - # create empty canvas and load program - canvas = core.DrawerCanvas(stylesheet=temp_style, device=device) - canvas.load_program(program=program) - - # - # update configuration - # - - # time range - if time_range: - if time_unit == types.TimeUnits.CYCLES.value: - canvas.set_time_range(*time_range, seconds=False) - elif time_unit == types.TimeUnits.NS.value: - canvas.set_time_range(*time_range, seconds=True) - else: - raise VisualizationError(f"Invalid time unit {time_unit} is specified.") - - # channels not shown - if disable_channels: - for chan in disable_channels: - canvas.set_disable_channel(chan, remove=True) - - # show snapshots - if not show_snapshot: - canvas.set_disable_type(types.SymbolType.SNAPSHOT, remove=True) - canvas.set_disable_type(types.LabelType.SNAPSHOT, remove=True) - - # show frame changes - if not show_framechange: - canvas.set_disable_type(types.SymbolType.FRAME, remove=True) - canvas.set_disable_type(types.LabelType.FRAME, remove=True) - - # show waveform info - if not show_waveform_info: - canvas.set_disable_type(types.LabelType.PULSE_INFO, remove=True) - canvas.set_disable_type(types.LabelType.PULSE_NAME, remove=True) - - # show barrier - if not plot_barrier: - canvas.set_disable_type(types.LineType.BARRIER, remove=True) - - canvas.update() - - # - # Call plotter API and generate image - # - - if plotter == types.Plotter.Mpl2D.value: - try: - from qiskit.visualization.pulse_v2.plotters import Mpl2DPlotter - except ImportError as ex: - raise MissingOptionalLibraryError( - libname="Matplotlib", - name="plot_histogram", - pip_install="pip install matplotlib", - ) from ex - plotter_api = Mpl2DPlotter(canvas=canvas, axis=axis) - plotter_api.draw() - else: - raise VisualizationError(f"Plotter API {plotter} is not supported.") - - return plotter_api.get_image() diff --git a/qiskit/visualization/pulse_v2/layouts.py b/qiskit/visualization/pulse_v2/layouts.py deleted file mode 100644 index 13b42e394e94..000000000000 --- a/qiskit/visualization/pulse_v2/layouts.py +++ /dev/null @@ -1,387 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -# pylint: disable=unused-argument - -""" -A collection of functions that decide the layout of an output image. -See :py:mod:`~qiskit.visualization.pulse_v2.types` for more info on the required data. - -There are 3 types of layout functions in this module. - -1. layout.chart_channel_map - -An end-user can write arbitrary functions that output the custom channel ordering -associated with group name. Layout function in this module are called with the -`formatter` and `device` kwargs. These data provides stylesheet configuration -and backend system configuration. - -The layout function is restricted to: - - - ```python - def my_channel_layout(channels: List[pulse.channels.Channel], - formatter: Dict[str, Any], - device: DrawerBackendInfo - ) -> Iterator[Tuple[str, List[pulse.channels.Channel]]]: - ordered_channels = [] - # arrange order of channels - - for key, channels in my_ordering_dict.items(): - yield key, channels - ``` - -2. layout.time_axis_map - -An end-user can write arbitrary functions that output the `HorizontalAxis` data set that -will be later consumed by the plotter API to update the horizontal axis appearance. -Layout function in this module are called with the `time_window`, `axis_breaks`, and `dt` kwargs. -These data provides horizontal axis limit, axis break position, and time resolution, respectively. - -See py:mod:`qiskit.visualization.pulse_v2.types` for more info on the required -data. - - ```python - def my_horizontal_axis(time_window: Tuple[int, int], - axis_breaks: List[Tuple[int, int]], - dt: Optional[float] = None) -> HorizontalAxis: - # write horizontal axis configuration - - return horizontal_axis - ``` - -3. layout.figure_title - -An end-user can write arbitrary functions that output the string data that -will be later consumed by the plotter API to output the figure title. -Layout functions in this module are called with the `program` and `device` kwargs. -This data provides input program and backend system configurations. - - ```python - def my_figure_title(program: Union[pulse.Waveform, pulse.Schedule], - device: DrawerBackendInfo) -> str: - - return 'title' - ``` - -An arbitrary layout function satisfying the above format can be accepted. -""" - -from collections import defaultdict -from typing import List, Dict, Any, Tuple, Iterator, Optional, Union - -import numpy as np -from qiskit import pulse -from qiskit.visualization.pulse_v2 import types -from qiskit.visualization.pulse_v2.device_info import DrawerBackendInfo - - -def channel_type_grouped_sort( - channels: List[pulse.channels.Channel], formatter: Dict[str, Any], device: DrawerBackendInfo -) -> Iterator[Tuple[str, List[pulse.channels.Channel]]]: - """Layout function for the channel assignment to the chart instance. - - Assign single channel per chart. Channels are grouped by type and - sorted by index in ascending order. - - Stylesheet key: - `chart_channel_map` - - For example: - [D0, D2, C0, C2, M0, M2, A0, A2] -> [D0, D2, C0, C2, M0, M2, A0, A2] - - Args: - channels: Channels to show. - formatter: Dictionary of stylesheet settings. - device: Backend configuration. - - Yields: - Tuple of chart name and associated channels. - """ - chan_type_dict = defaultdict(list) - - for chan in channels: - chan_type_dict[type(chan)].append(chan) - - ordered_channels = [] - - # drive channels - d_chans = chan_type_dict.get(pulse.DriveChannel, []) - ordered_channels.extend(sorted(d_chans, key=lambda x: x.index)) - - # control channels - c_chans = chan_type_dict.get(pulse.ControlChannel, []) - ordered_channels.extend(sorted(c_chans, key=lambda x: x.index)) - - # measure channels - m_chans = chan_type_dict.get(pulse.MeasureChannel, []) - ordered_channels.extend(sorted(m_chans, key=lambda x: x.index)) - - # acquire channels - if formatter["control.show_acquire_channel"]: - a_chans = chan_type_dict.get(pulse.AcquireChannel, []) - ordered_channels.extend(sorted(a_chans, key=lambda x: x.index)) - - for chan in ordered_channels: - yield chan.name.upper(), [chan] - - -def channel_index_grouped_sort( - channels: List[pulse.channels.Channel], formatter: Dict[str, Any], device: DrawerBackendInfo -) -> Iterator[Tuple[str, List[pulse.channels.Channel]]]: - """Layout function for the channel assignment to the chart instance. - - Assign single channel per chart. Channels are grouped by the same index and - sorted by type. - - Stylesheet key: - `chart_channel_map` - - For example: - [D0, D2, C0, C2, M0, M2, A0, A2] -> [D0, D2, C0, C2, M0, M2, A0, A2] - - Args: - channels: Channels to show. - formatter: Dictionary of stylesheet settings. - device: Backend configuration. - - Yields: - Tuple of chart name and associated channels. - """ - chan_type_dict = defaultdict(list) - inds = set() - - for chan in channels: - chan_type_dict[type(chan)].append(chan) - inds.add(chan.index) - - d_chans = chan_type_dict.get(pulse.DriveChannel, []) - d_chans = sorted(d_chans, key=lambda x: x.index, reverse=True) - - u_chans = chan_type_dict.get(pulse.ControlChannel, []) - u_chans = sorted(u_chans, key=lambda x: x.index, reverse=True) - - m_chans = chan_type_dict.get(pulse.MeasureChannel, []) - m_chans = sorted(m_chans, key=lambda x: x.index, reverse=True) - - a_chans = chan_type_dict.get(pulse.AcquireChannel, []) - a_chans = sorted(a_chans, key=lambda x: x.index, reverse=True) - - ordered_channels = [] - - for ind in sorted(inds): - # drive channel - if len(d_chans) > 0 and d_chans[-1].index == ind: - ordered_channels.append(d_chans.pop()) - # control channel - if len(u_chans) > 0 and u_chans[-1].index == ind: - ordered_channels.append(u_chans.pop()) - # measure channel - if len(m_chans) > 0 and m_chans[-1].index == ind: - ordered_channels.append(m_chans.pop()) - # acquire channel - if formatter["control.show_acquire_channel"]: - if len(a_chans) > 0 and a_chans[-1].index == ind: - ordered_channels.append(a_chans.pop()) - - for chan in ordered_channels: - yield chan.name.upper(), [chan] - - -def channel_index_grouped_sort_u( - channels: List[pulse.channels.Channel], formatter: Dict[str, Any], device: DrawerBackendInfo -) -> Iterator[Tuple[str, List[pulse.channels.Channel]]]: - """Layout function for the channel assignment to the chart instance. - - Assign single channel per chart. Channels are grouped by the same index and - sorted by type except for control channels. Control channels are added to the - end of other channels. - - Stylesheet key: - `chart_channel_map` - - For example: - [D0, D2, C0, C2, M0, M2, A0, A2] -> [D0, D2, C0, C2, M0, M2, A0, A2] - - Args: - channels: Channels to show. - formatter: Dictionary of stylesheet settings. - device: Backend configuration. - - Yields: - Tuple of chart name and associated channels. - """ - chan_type_dict = defaultdict(list) - inds = set() - - for chan in channels: - chan_type_dict[type(chan)].append(chan) - inds.add(chan.index) - - d_chans = chan_type_dict.get(pulse.DriveChannel, []) - d_chans = sorted(d_chans, key=lambda x: x.index, reverse=True) - - m_chans = chan_type_dict.get(pulse.MeasureChannel, []) - m_chans = sorted(m_chans, key=lambda x: x.index, reverse=True) - - a_chans = chan_type_dict.get(pulse.AcquireChannel, []) - a_chans = sorted(a_chans, key=lambda x: x.index, reverse=True) - - u_chans = chan_type_dict.get(pulse.ControlChannel, []) - u_chans = sorted(u_chans, key=lambda x: x.index) - - ordered_channels = [] - - for ind in sorted(inds): - # drive channel - if len(d_chans) > 0 and d_chans[-1].index == ind: - ordered_channels.append(d_chans.pop()) - # measure channel - if len(m_chans) > 0 and m_chans[-1].index == ind: - ordered_channels.append(m_chans.pop()) - # acquire channel - if formatter["control.show_acquire_channel"]: - if len(a_chans) > 0 and a_chans[-1].index == ind: - ordered_channels.append(a_chans.pop()) - - # control channels - ordered_channels.extend(u_chans) - - for chan in ordered_channels: - yield chan.name.upper(), [chan] - - -def qubit_index_sort( - channels: List[pulse.channels.Channel], formatter: Dict[str, Any], device: DrawerBackendInfo -) -> Iterator[Tuple[str, List[pulse.channels.Channel]]]: - """Layout function for the channel assignment to the chart instance. - - Assign multiple channels per chart. Channels associated with the same qubit - are grouped in the same chart and sorted by qubit index in ascending order. - - Acquire channels are not shown. - - Stylesheet key: - `chart_channel_map` - - For example: - [D0, D2, C0, C2, M0, M2, A0, A2] -> [Q0, Q1, Q2] - - Args: - channels: Channels to show. - formatter: Dictionary of stylesheet settings. - device: Backend configuration. - - Yields: - Tuple of chart name and associated channels. - """ - _removed = ( - pulse.channels.AcquireChannel, - pulse.channels.MemorySlot, - pulse.channels.RegisterSlot, - ) - - qubit_channel_map = defaultdict(list) - - for chan in channels: - if isinstance(chan, _removed): - continue - qubit_channel_map[device.get_qubit_index(chan)].append(chan) - - sorted_map = sorted(qubit_channel_map.items(), key=lambda x: x[0]) - - for qind, chans in sorted_map: - yield f"Q{qind:d}", chans - - -def time_map_in_ns( - time_window: Tuple[int, int], axis_breaks: List[Tuple[int, int]], dt: Optional[float] = None -) -> types.HorizontalAxis: - """Layout function for the horizontal axis formatting. - - Calculate axis break and map true time to axis labels. Generate equispaced - 6 horizontal axis ticks. Convert into seconds if ``dt`` is provided. - - Args: - time_window: Left and right edge of this graph. - axis_breaks: List of axis break period. - dt: Time resolution of system. - - Returns: - Axis formatter object. - """ - # shift time axis - t0, t1 = time_window - t0_shift = t0 - t1_shift = t1 - - axis_break_pos = [] - offset_accumulation = 0 - for t0b, t1b in axis_breaks: - if t1b < t0 or t0b > t1: - continue - if t0 > t1b: - t0_shift -= t1b - t0b - if t1 > t1b: - t1_shift -= t1b - t0b - axis_break_pos.append(t0b - offset_accumulation) - offset_accumulation += t1b - t0b - - # axis label - axis_loc = np.linspace(max(t0_shift, 0), t1_shift, 6) - axis_label = axis_loc.copy() - - for t0b, t1b in axis_breaks: - offset = t1b - t0b - axis_label = np.where(axis_label > t0b, axis_label + offset, axis_label) - - # consider time resolution - if dt: - label = "Time (ns)" - axis_label *= dt * 1e9 - else: - label = "System cycle time (dt)" - - formatted_label = [f"{val:.0f}" for val in axis_label] - - return types.HorizontalAxis( - window=(t0_shift, t1_shift), - axis_map=dict(zip(axis_loc, formatted_label)), - axis_break_pos=axis_break_pos, - label=label, - ) - - -def detail_title(program: Union[pulse.Waveform, pulse.Schedule], device: DrawerBackendInfo) -> str: - """Layout function for generating figure title. - - This layout writes program name, program duration, and backend name in the title. - """ - title_str = [] - - # add program name - title_str.append(f"Name: {program.name}") - - # add program duration - dt = device.dt * 1e9 if device.dt else 1.0 - title_str.append(f"Duration: {program.duration * dt:.1f} {'ns' if device.dt else 'dt'}") - - # add device name - if device.backend_name != "no-backend": - title_str.append(f"Backend: {device.backend_name}") - - return ", ".join(title_str) - - -def empty_title(program: Union[pulse.Waveform, pulse.Schedule], device: DrawerBackendInfo) -> str: - """Layout function for generating an empty figure title.""" - return "" diff --git a/qiskit/visualization/pulse_v2/plotters/__init__.py b/qiskit/visualization/pulse_v2/plotters/__init__.py deleted file mode 100644 index 90c67065efa3..000000000000 --- a/qiskit/visualization/pulse_v2/plotters/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -""" -Plotter API for pulse drawer. -""" - -from qiskit.visualization.pulse_v2.plotters.matplotlib import Mpl2DPlotter diff --git a/qiskit/visualization/pulse_v2/plotters/base_plotter.py b/qiskit/visualization/pulse_v2/plotters/base_plotter.py deleted file mode 100644 index ee6f79235145..000000000000 --- a/qiskit/visualization/pulse_v2/plotters/base_plotter.py +++ /dev/null @@ -1,53 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Base plotter API.""" - -from abc import ABC, abstractmethod -from typing import Any - -from qiskit.visualization.pulse_v2 import core - - -class BasePlotter(ABC): - """Base class of Qiskit plotter.""" - - def __init__(self, canvas: core.DrawerCanvas): - """Create new plotter. - - Args: - canvas: Configured drawer canvas object. - """ - self.canvas = canvas - - @abstractmethod - def initialize_canvas(self): - """Format appearance of the canvas.""" - raise NotImplementedError - - @abstractmethod - def draw(self): - """Output drawing objects stored in canvas object.""" - raise NotImplementedError - - @abstractmethod - def get_image(self, interactive: bool = False) -> Any: - """Get image data to return. - - Args: - interactive: When set `True` show the circuit in a new window. - This depends on the matplotlib backend being used supporting this. - - Returns: - Image data. This depends on the plotter API. - """ - raise NotImplementedError diff --git a/qiskit/visualization/pulse_v2/plotters/matplotlib.py b/qiskit/visualization/pulse_v2/plotters/matplotlib.py deleted file mode 100644 index 1788a1254894..000000000000 --- a/qiskit/visualization/pulse_v2/plotters/matplotlib.py +++ /dev/null @@ -1,201 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Matplotlib plotter API.""" - -from typing import Optional - -import matplotlib -import matplotlib.pyplot as plt -import numpy as np -from matplotlib.patches import Rectangle - -from qiskit.visualization.exceptions import VisualizationError -from qiskit.visualization.pulse_v2 import core, drawings, types -from qiskit.visualization.pulse_v2.plotters.base_plotter import BasePlotter -from qiskit.visualization.utils import matplotlib_close_if_inline - - -class Mpl2DPlotter(BasePlotter): - """Matplotlib API for pulse drawer. - - This plotter places canvas charts along y axis of 2D canvas with vertical offset. - Each chart is map to X-Y axis of the canvas. - """ - - def __init__(self, canvas: core.DrawerCanvas, axis: Optional[plt.Axes] = None): - """Create new plotter. - - Args: - canvas: Configured drawer canvas object. Canvas object should be updated - with `.update` method before set to the plotter API. - axis: Matplotlib axis object. When `axis` is provided, the plotter updates - given axis instead of creating and returning new matplotlib figure. - """ - super().__init__(canvas=canvas) - - # calculate height of all charts - canvas_height = 0 - for chart in self.canvas.charts: - if not chart.is_active and not self.canvas.formatter["control.show_empty_channel"]: - continue - canvas_height += chart.vmax - chart.vmin - # set min canvas_height size - canvas_height = max(canvas_height, 0.1) - - if axis is None: - fig_h = canvas_height * self.canvas.formatter["general.fig_chart_height"] - fig_w = self.canvas.formatter["general.fig_width"] - - self.figure = plt.figure(figsize=(fig_w, fig_h)) - self.ax = self.figure.add_subplot(1, 1, 1) - else: - self.figure = axis.figure - self.ax = axis - - self.initialize_canvas() - - def initialize_canvas(self): - """Format appearance of matplotlib canvas.""" - self.ax.set_facecolor(self.canvas.formatter["color.background"]) - - # axis labels - self.ax.set_yticklabels([]) - self.ax.yaxis.set_tick_params(left=False) - - def draw(self): - """Output drawings stored in canvas object.""" - # axis configuration - axis_config = self.canvas.layout["time_axis_map"]( - time_window=self.canvas.time_range, - axis_breaks=self.canvas.time_breaks, - dt=self.canvas.device.dt, - ) - - current_y = 0 - margin_y = self.canvas.formatter["margin.between_channel"] - for chart in self.canvas.charts: - if not chart.is_active and not self.canvas.formatter["control.show_empty_channel"]: - continue - current_y -= chart.vmax - for _, data in chart.collections: - # calculate scaling factor - if not data.ignore_scaling: - # product of channel-wise scaling and chart level scaling - scale = max(self.canvas.chan_scales.get(chan, 1.0) for chan in data.channels) - scale *= chart.scale - else: - scale = 1.0 - - x = data.xvals - y = scale * data.yvals + current_y - - if isinstance(data, drawings.LineData): - # line object - if data.fill: - self.ax.fill_between(x, y1=y, y2=current_y * np.ones_like(y), **data.styles) - else: - self.ax.plot(x, y, **data.styles) - elif isinstance(data, drawings.TextData): - # text object - text = rf"${data.latex}$" if data.latex else data.text - # replace dynamic text - text = text.replace(types.DynamicString.SCALE, f"{chart.scale:.1f}") - self.ax.text(x=x[0], y=y[0], s=text, **data.styles) - elif isinstance(data, drawings.BoxData): - xy = x[0], y[0] - box = Rectangle( - xy, width=x[1] - x[0], height=y[1] - y[0], fill=True, **data.styles - ) - self.ax.add_patch(box) - else: - raise VisualizationError( - f"Data {data} is not supported " f"by {self.__class__.__name__}" - ) - # axis break - for pos in axis_config.axis_break_pos: - self.ax.text( - x=pos, - y=current_y, - s="//", - ha="center", - va="center", - zorder=self.canvas.formatter["layer.axis_label"], - fontsize=self.canvas.formatter["text_size.axis_break_symbol"], - rotation=180, - ) - - # shift chart position - current_y += chart.vmin - margin_y - - # remove the last margin - current_y += margin_y - - y_max = self.canvas.formatter["margin.top"] - y_min = current_y - self.canvas.formatter["margin.bottom"] - - # plot axis break line - for pos in axis_config.axis_break_pos: - self.ax.plot( - [pos, pos], - [y_min, y_max], - zorder=self.canvas.formatter["layer.fill_waveform"] + 1, - linewidth=self.canvas.formatter["line_width.axis_break"], - color=self.canvas.formatter["color.background"], - ) - - # label - self.ax.set_xticks(list(axis_config.axis_map.keys())) - self.ax.set_xticklabels( - list(axis_config.axis_map.values()), - fontsize=self.canvas.formatter["text_size.axis_label"], - ) - self.ax.set_xlabel( - axis_config.label, fontsize=self.canvas.formatter["text_size.axis_label"] - ) - - # boundary - if axis_config.window == (0, 0): - self.ax.set_xlim(0, 1) - else: - self.ax.set_xlim(*axis_config.window) - self.ax.set_ylim(y_min, y_max) - - # title - if self.canvas.fig_title: - self.ax.text( - x=axis_config.window[0], - y=y_max, - s=self.canvas.fig_title, - ha="left", - va="bottom", - zorder=self.canvas.formatter["layer.fig_title"], - color=self.canvas.formatter["color.fig_title"], - size=self.canvas.formatter["text_size.fig_title"], - ) - - def get_image(self, interactive: bool = False) -> matplotlib.pyplot.Figure: - """Get image data to return. - - Args: - interactive: When set `True` show the circuit in a new window. - This depends on the matplotlib backend being used supporting this. - - Returns: - Matplotlib figure data. - """ - matplotlib_close_if_inline(self.figure) - - if self.figure and interactive: - self.figure.show() - - return self.figure diff --git a/qiskit/visualization/pulse_v2/stylesheet.py b/qiskit/visualization/pulse_v2/stylesheet.py deleted file mode 100644 index 1a37756f4baa..000000000000 --- a/qiskit/visualization/pulse_v2/stylesheet.py +++ /dev/null @@ -1,312 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -r""" -Stylesheet for pulse drawer. - -The stylesheet `QiskitPulseStyle` is initialized with the hard-corded default values in -`default_style`. This instance is generated when the pulse drawer module is loaded so that -every lower modules can access to the information. - -The `QiskitPulseStyle` is a wrapper class of python dictionary with the structured keys -such as `formatter.color.fill_waveform_d` to represent a color code of the drive channel. -This key representation and initialization framework are the imitative of -`rcParams` of `matplotlib`. However, the `QiskitPulseStyle` is not compatible with the `rcParams` -because the pulse stylesheet is heavily specialized to the context of the pulse program. - -The settings of stylesheet are broadly separated into `formatter`, `generator` and `layout`. -The formatter is a nested dictionary of drawing parameters to control the appearance of -each visualization element. This takes similar data structure to the `rcParams` of `matplotlib`. -The generator is a list of callback functions that generates drawing objects from -given program and device data. The layout is a callback function that determines -the appearance of the output image. -""" - -from typing import Dict, Any, Mapping -from qiskit.visualization.pulse_v2 import generators, layouts - - -class QiskitPulseStyle(dict): - """Stylesheet for pulse drawer.""" - - def __init__(self): - super().__init__() - # to inform which stylesheet is applied. some plotter may not support specific style. - self.stylesheet = None - self.update(default_style()) - - def update(self, __m: Mapping[str, Any], **kwargs) -> None: - super().update(__m, **kwargs) - for key, value in __m.items(): - self.__setitem__(key, value) - self.stylesheet = __m.__class__.__name__ - - @property - def formatter(self): - """Return formatter field of style dictionary.""" - sub_dict = {} - for key, value in self.items(): - sub_keys = key.split(".") - if sub_keys[0] == "formatter": - sub_dict[".".join(sub_keys[1:])] = value - return sub_dict - - @property - def generator(self): - """Return generator field of style dictionary.""" - sub_dict = {} - for key, value in self.items(): - sub_keys = key.split(".") - if sub_keys[0] == "generator": - sub_dict[".".join(sub_keys[1:])] = value - return sub_dict - - @property - def layout(self): - """Return layout field of style dictionary.""" - sub_dict = {} - for key, value in self.items(): - sub_keys = key.split(".") - if sub_keys[0] == "layout": - sub_dict[".".join(sub_keys[1:])] = value - return sub_dict - - -class IQXStandard(dict): - """Standard pulse stylesheet. - - - Generate stepwise waveform envelope with latex pulse names. - - Apply phase modulation to waveforms. - - Plot frame change symbol with formatted operand values. - - Show chart name with scaling factor. - - Show snapshot and barrier. - - Do not show acquire channels. - - Channels are sorted by index and control channels are added to the end. - """ - - def __init__(self, **kwargs): - super().__init__() - style = { - "formatter.control.apply_phase_modulation": True, - "formatter.control.show_snapshot_channel": True, - "formatter.control.show_acquire_channel": False, - "formatter.control.show_empty_channel": False, - "formatter.control.auto_chart_scaling": True, - "formatter.control.axis_break": True, - "generator.waveform": [ - generators.gen_filled_waveform_stepwise, - generators.gen_ibmq_latex_waveform_name, - ], - "generator.frame": [generators.gen_frame_symbol, generators.gen_formatted_frame_values], - "generator.chart": [ - generators.gen_chart_name, - generators.gen_baseline, - generators.gen_channel_freqs, - ], - "generator.snapshot": [generators.gen_snapshot_symbol], - "generator.barrier": [generators.gen_barrier], - "layout.chart_channel_map": layouts.channel_index_grouped_sort_u, - "layout.time_axis_map": layouts.time_map_in_ns, - "layout.figure_title": layouts.detail_title, - } - style.update(**kwargs) - self.update(style) - - def __repr__(self): - return "Standard Pulse style sheet." - - -class IQXSimple(dict): - """Simple pulse stylesheet without channel notation. - - - Generate stepwise waveform envelope with latex pulse names. - - Apply phase modulation to waveforms. - - Do not show frame changes. - - Show chart name. - - Do not show snapshot and barrier. - - Do not show acquire channels. - - Channels are sorted by qubit index. - """ - - def __init__(self, **kwargs): - super().__init__() - style = { - "formatter.general.fig_chart_height": 5, - "formatter.control.apply_phase_modulation": True, - "formatter.control.show_snapshot_channel": True, - "formatter.control.show_acquire_channel": False, - "formatter.control.show_empty_channel": False, - "formatter.control.auto_chart_scaling": False, - "formatter.control.axis_break": True, - "generator.waveform": [ - generators.gen_filled_waveform_stepwise, - generators.gen_ibmq_latex_waveform_name, - ], - "generator.frame": [], - "generator.chart": [generators.gen_chart_name, generators.gen_baseline], - "generator.snapshot": [], - "generator.barrier": [], - "layout.chart_channel_map": layouts.qubit_index_sort, - "layout.time_axis_map": layouts.time_map_in_ns, - "layout.figure_title": layouts.empty_title, - } - style.update(**kwargs) - self.update(style) - - def __repr__(self): - return "Simple pulse style sheet for publication." - - -class IQXDebugging(dict): - """Pulse stylesheet for pulse programmers. Show details of instructions. - - # TODO: add more generators - - - Generate stepwise waveform envelope with latex pulse names. - - Generate annotation for waveform height. - - Apply phase modulation to waveforms. - - Plot frame change symbol with raw operand values. - - Show chart name and channel frequency. - - Show snapshot and barrier. - - Show acquire channels. - - Channels are sorted by index and control channels are added to the end. - """ - - def __init__(self, **kwargs): - super().__init__() - style = { - "formatter.control.apply_phase_modulation": True, - "formatter.control.show_snapshot_channel": True, - "formatter.control.show_acquire_channel": True, - "formatter.control.show_empty_channel": False, - "formatter.control.auto_chart_scaling": True, - "formatter.control.axis_break": True, - "generator.waveform": [ - generators.gen_filled_waveform_stepwise, - generators.gen_ibmq_latex_waveform_name, - generators.gen_waveform_max_value, - ], - "generator.frame": [ - generators.gen_frame_symbol, - generators.gen_raw_operand_values_compact, - ], - "generator.chart": [ - generators.gen_chart_name, - generators.gen_baseline, - generators.gen_channel_freqs, - ], - "generator.snapshot": [generators.gen_snapshot_symbol, generators.gen_snapshot_name], - "generator.barrier": [generators.gen_barrier], - "layout.chart_channel_map": layouts.channel_index_grouped_sort_u, - "layout.time_axis_map": layouts.time_map_in_ns, - "layout.figure_title": layouts.detail_title, - } - style.update(**kwargs) - self.update(style) - - def __repr__(self): - return "Pulse style sheet for pulse programmers." - - -def default_style() -> Dict[str, Any]: - """Define default values of the pulse stylesheet.""" - return { - "formatter.general.fig_width": 13, - "formatter.general.fig_chart_height": 1.5, - "formatter.general.vertical_resolution": 1e-6, - "formatter.general.max_scale": 100, - "formatter.color.waveforms": { - "W": ["#648fff", "#002999"], - "D": ["#648fff", "#002999"], - "U": ["#ffb000", "#994A00"], - "M": ["#dc267f", "#760019"], - "A": ["#dc267f", "#760019"], - }, - "formatter.color.baseline": "#000000", - "formatter.color.barrier": "#222222", - "formatter.color.background": "#f2f3f4", - "formatter.color.fig_title": "#000000", - "formatter.color.annotate": "#222222", - "formatter.color.frame_change": "#000000", - "formatter.color.snapshot": "#000000", - "formatter.color.axis_label": "#000000", - "formatter.color.opaque_shape": ["#f2f3f4", "#000000"], - "formatter.alpha.fill_waveform": 0.3, - "formatter.alpha.baseline": 1.0, - "formatter.alpha.barrier": 0.7, - "formatter.alpha.opaque_shape": 0.7, - "formatter.layer.fill_waveform": 2, - "formatter.layer.baseline": 1, - "formatter.layer.barrier": 1, - "formatter.layer.annotate": 5, - "formatter.layer.axis_label": 5, - "formatter.layer.frame_change": 4, - "formatter.layer.snapshot": 3, - "formatter.layer.fig_title": 6, - "formatter.margin.top": 0.5, - "formatter.margin.bottom": 0.5, - "formatter.margin.left_percent": 0.05, - "formatter.margin.right_percent": 0.05, - "formatter.margin.between_channel": 0.5, - "formatter.label_offset.pulse_name": 0.3, - "formatter.label_offset.chart_info": 0.3, - "formatter.label_offset.frame_change": 0.3, - "formatter.label_offset.snapshot": 0.3, - "formatter.text_size.axis_label": 15, - "formatter.text_size.annotate": 12, - "formatter.text_size.frame_change": 20, - "formatter.text_size.snapshot": 20, - "formatter.text_size.fig_title": 15, - "formatter.text_size.axis_break_symbol": 15, - "formatter.line_width.fill_waveform": 0, - "formatter.line_width.axis_break": 6, - "formatter.line_width.baseline": 1, - "formatter.line_width.barrier": 1, - "formatter.line_width.opaque_shape": 1, - "formatter.line_style.fill_waveform": "-", - "formatter.line_style.baseline": "-", - "formatter.line_style.barrier": ":", - "formatter.line_style.opaque_shape": "--", - "formatter.channel_scaling.drive": 1.0, - "formatter.channel_scaling.control": 1.0, - "formatter.channel_scaling.measure": 1.0, - "formatter.channel_scaling.acquire": 1.0, - "formatter.channel_scaling.pos_spacing": 0.1, - "formatter.channel_scaling.neg_spacing": -0.1, - "formatter.box_width.opaque_shape": 150, - "formatter.box_height.opaque_shape": 0.5, - "formatter.axis_break.length": 3000, - "formatter.axis_break.max_length": 1000, - "formatter.control.fill_waveform": True, - "formatter.control.apply_phase_modulation": True, - "formatter.control.show_snapshot_channel": True, - "formatter.control.show_acquire_channel": True, - "formatter.control.show_empty_channel": True, - "formatter.control.auto_chart_scaling": True, - "formatter.control.axis_break": True, - "formatter.unicode_symbol.frame_change": "\u21BA", - "formatter.unicode_symbol.snapshot": "\u21AF", - "formatter.unicode_symbol.phase_parameter": "\u03b8", - "formatter.unicode_symbol.freq_parameter": "f", - "formatter.latex_symbol.frame_change": r"\circlearrowleft", - "formatter.latex_symbol.snapshot": "", - "formatter.latex_symbol.phase_parameter": r"\theta", - "formatter.latex_symbol.freq_parameter": "f", - "generator.waveform": [], - "generator.frame": [], - "generator.chart": [], - "generator.snapshot": [], - "generator.barrier": [], - "layout.chart_channel_map": None, - "layout.time_axis_map": None, - "layout.figure_title": None, - } diff --git a/qiskit/visualization/pulse_v2/types.py b/qiskit/visualization/pulse_v2/types.py deleted file mode 100644 index 44cf52615870..000000000000 --- a/qiskit/visualization/pulse_v2/types.py +++ /dev/null @@ -1,242 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -# pylint: disable=invalid-name - -""" -Special data types. -""" -from __future__ import annotations - -from enum import Enum -from typing import NamedTuple, Union, Optional, NewType, Any, List - -import numpy as np -from qiskit import pulse - - -class PhaseFreqTuple(NamedTuple): - phase: float - freq: float - - -PhaseFreqTuple.__doc__ = "Data to represent a set of frequency and phase values." -PhaseFreqTuple.phase.__doc__ = "Phase value in rad." -PhaseFreqTuple.freq.__doc__ = "Frequency value in Hz." - - -PulseInstruction = NamedTuple( - "InstructionTuple", - [ - ("t0", int), - ("dt", Union[float, None]), - ("frame", PhaseFreqTuple), - ("inst", Union[pulse.Instruction, List[pulse.Instruction]]), - ("is_opaque", bool), - ], -) -PulseInstruction.__doc__ = "Data to represent pulse instruction for visualization." -PulseInstruction.t0.__doc__ = "A time when the instruction is issued." -PulseInstruction.dt.__doc__ = "System cycle time." -PulseInstruction.frame.__doc__ = "A reference frame to run instruction." -PulseInstruction.inst.__doc__ = "Pulse instruction." -PulseInstruction.is_opaque.__doc__ = "If there is any unbound parameters." - - -BarrierInstruction = NamedTuple( - "Barrier", [("t0", int), ("dt", Optional[float]), ("channels", List[pulse.channels.Channel])] -) -BarrierInstruction.__doc__ = "Data to represent special pulse instruction of barrier." -BarrierInstruction.t0.__doc__ = "A time when the instruction is issued." -BarrierInstruction.dt.__doc__ = "System cycle time." -BarrierInstruction.channels.__doc__ = "A list of channel associated with this barrier." - - -SnapshotInstruction = NamedTuple( - "Snapshots", [("t0", int), ("dt", Optional[float]), ("inst", pulse.instructions.Snapshot)] -) -SnapshotInstruction.__doc__ = "Data to represent special pulse instruction of snapshot." -SnapshotInstruction.t0.__doc__ = "A time when the instruction is issued." -SnapshotInstruction.dt.__doc__ = "System cycle time." -SnapshotInstruction.inst.__doc__ = "Snapshot instruction." - - -class ChartAxis(NamedTuple): - name: str - channels: list[pulse.channels.Channel] - - -ChartAxis.__doc__ = "Data to represent an axis information of chart." -ChartAxis.name.__doc__ = "Name of chart." -ChartAxis.channels.__doc__ = "Channels associated with chart." - - -class ParsedInstruction(NamedTuple): - xvals: np.ndarray - yvals: np.ndarray - meta: dict[str, Any] - - -ParsedInstruction.__doc__ = "Data to represent a parsed pulse instruction for object generation." -ParsedInstruction.xvals.__doc__ = "Numpy array of x axis data." -ParsedInstruction.yvals.__doc__ = "Numpy array of y axis data." -ParsedInstruction.meta.__doc__ = "Dictionary containing instruction details." - - -class OpaqueShape(NamedTuple): - duration: np.ndarray - meta: dict[str, Any] - - -OpaqueShape.__doc__ = "Data to represent a pulse instruction with parameterized shape." -OpaqueShape.duration.__doc__ = "Duration of instruction." -OpaqueShape.meta.__doc__ = "Dictionary containing instruction details." - - -class HorizontalAxis(NamedTuple): - window: tuple[int, int] - axis_map: dict[float, float | str] - axis_break_pos: list[int] - label: str - - -HorizontalAxis.__doc__ = "Data to represent configuration of horizontal axis." -HorizontalAxis.window.__doc__ = "Left and right edge of graph." -HorizontalAxis.axis_map.__doc__ = "Mapping of apparent coordinate system and actual location." -HorizontalAxis.axis_break_pos.__doc__ = "Locations of axis break." -HorizontalAxis.label.__doc__ = "Label of horizontal axis." - - -class WaveformType(str, Enum): - """ - Waveform data type. - - REAL: Assigned to objects that represent real part of waveform. - IMAG: Assigned to objects that represent imaginary part of waveform. - OPAQUE: Assigned to objects that represent waveform with unbound parameters. - """ - - REAL = "Waveform.Real" - IMAG = "Waveform.Imag" - OPAQUE = "Waveform.Opaque" - - -class LabelType(str, Enum): - """ - Label data type. - - PULSE_NAME: Assigned to objects that represent name of waveform. - PULSE_INFO: Assigned to objects that represent extra info about waveform. - OPAQUE_BOXTEXT: Assigned to objects that represent box text of opaque shapes. - CH_NAME: Assigned to objects that represent name of channel. - CH_SCALE: Assigned to objects that represent scaling factor of channel. - FRAME: Assigned to objects that represent value of frame. - SNAPSHOT: Assigned to objects that represent label of snapshot. - """ - - PULSE_NAME = "Label.Pulse.Name" - PULSE_INFO = "Label.Pulse.Info" - OPAQUE_BOXTEXT = "Label.Opaque.Boxtext" - CH_NAME = "Label.Channel.Name" - CH_INFO = "Label.Channel.Info" - FRAME = "Label.Frame.Value" - SNAPSHOT = "Label.Snapshot" - - -class SymbolType(str, Enum): - """ - Symbol data type. - - FRAME: Assigned to objects that represent symbol of frame. - SNAPSHOT: Assigned to objects that represent symbol of snapshot. - """ - - FRAME = "Symbol.Frame" - SNAPSHOT = "Symbol.Snapshot" - - -class LineType(str, Enum): - """ - Line data type. - - BASELINE: Assigned to objects that represent zero line of channel. - BARRIER: Assigned to objects that represent barrier line. - """ - - BASELINE = "Line.Baseline" - BARRIER = "Line.Barrier" - - -class AbstractCoordinate(str, Enum): - """Abstract coordinate that the exact value depends on the user preference. - - RIGHT: The horizontal coordinate at t0 shifted by the left margin. - LEFT: The horizontal coordinate at tf shifted by the right margin. - TOP: The vertical coordinate at the top of chart. - BOTTOM: The vertical coordinate at the bottom of chart. - """ - - RIGHT = "RIGHT" - LEFT = "LEFT" - TOP = "TOP" - BOTTOM = "BOTTOM" - - -class DynamicString(str, Enum): - """The string which is dynamically updated at the time of drawing. - - SCALE: A temporal value of chart scaling factor. - """ - - SCALE = "@scale" - - -class WaveformChannel(pulse.channels.PulseChannel): - """Dummy channel that doesn't belong to specific pulse channel.""" - - prefix = "w" - - def __init__(self): - """Create new waveform channel.""" - super().__init__(0) - - -class Plotter(str, Enum): - """Name of pulse plotter APIs. - - Mpl2D: Matplotlib plotter interface. Show charts in 2D canvas. - """ - - Mpl2D = "mpl2d" - - -class TimeUnits(str, Enum): - """Representation of time units. - - SYSTEM_CYCLE_TIME: System time dt. - NANO_SEC: Nano seconds. - """ - - CYCLES = "dt" - NS = "ns" - - -# convenient type to represent union of drawing data -# TODO: https://github.com/Qiskit/qiskit-terra/issues/9591 -# NewType means that a value of type Original cannot be used in places -# where a value of type Derived is expected -# (see https://docs.python.org/3/library/typing.html#newtype) -# This breaks a lot of type checking. -DataTypes = NewType("DataType", Union[WaveformType, LabelType, LineType, SymbolType]) - -# convenient type to represent union of values to represent a coordinate -Coordinate = NewType("Coordinate", Union[float, AbstractCoordinate]) diff --git a/qiskit_bot.yaml b/qiskit_bot.yaml index 73422179c8cb..5b217101c169 100644 --- a/qiskit_bot.yaml +++ b/qiskit_bot.yaml @@ -5,14 +5,8 @@ notifications: ".*": - "`@Qiskit/terra-core`" - "visualization/pulse_v2": - - "`@nkanazawa1989`" "visualization/timeline": - "`@nkanazawa1989`" - "pulse": - - "`@nkanazawa1989`" - "scheduler": - - "`@nkanazawa1989`" "qpy": - "`@mtreinish`" - "`@nkanazawa1989`" @@ -25,7 +19,7 @@ notifications: - "`@t-imamichi`" - "`@ajavadia`" - "`@levbishop`" - "(?!.*pulse.*)\\bvisualization\\b": + "visualization": - "@enavarro51" "^docs/": - "@Eric-Arellano" diff --git a/releasenotes/notes/remove-fake-v1-backends-b66bc47886702904.yaml b/releasenotes/notes/remove-fake-v1-backends-b66bc47886702904.yaml index 3e76174b1fe3..b85d6784c6b6 100644 --- a/releasenotes/notes/remove-fake-v1-backends-b66bc47886702904.yaml +++ b/releasenotes/notes/remove-fake-v1-backends-b66bc47886702904.yaml @@ -11,17 +11,16 @@ upgrade_providers: * ``FakeBackend`` * ``FakePulseBackend`` * ``FakeQasmBackend`` - - * Fake backends for special testing purposes: + * Fake backends for special testing purposes: * ``Fake1Q`` * ``FakeOpenPulse2Q`` * ``FakeOpenPulse3Q`` * Legacy fake backends: - + * ``Fake5QV1`` * ``Fake20QV1`` * ``Fake7QPulseV1`` * ``Fake27QPulseV1`` - * ``Fake127QPulseV1`` \ No newline at end of file + * ``Fake127QPulseV1`` diff --git a/releasenotes/notes/remove-pulse-calibrations-4486dc101b76ec51.yaml b/releasenotes/notes/remove-pulse-calibrations-4486dc101b76ec51.yaml index 314e2e8893c6..59fdfb9ad9a9 100644 --- a/releasenotes/notes/remove-pulse-calibrations-4486dc101b76ec51.yaml +++ b/releasenotes/notes/remove-pulse-calibrations-4486dc101b76ec51.yaml @@ -10,14 +10,16 @@ upgrade_transpiler: As part of Pulse removal in Qiskit 2.0, all pulse and calibration related functionality in the transpiler has been removed. This includes the following: - Passes that have been removed: - - * ``qiskit.transpiler.passes.PulseGates`` - * ``qiskit.transpiler.passes.ValidatePulseGates`` - * ``qiskit.transpiler.passes.RXCalibrationBuilder`` - * ``qiskit.transpiler.passes.RZXCalibrationBuilder`` - * ``qiskit.transpiler.passes.RZXCalibrationBuilderNoEcho`` - * ``qiskit.transpiler.passes.EchoRZXWeylDecomposition`` + The following passes and function have been removed: + + * ``qiskit.transpiler.passes.PulseGates`` pass + * ``qiskit.transpiler.passes.ValidatePulseGates`` pass + * ``qiskit.transpiler.passes.RXCalibrationBuilder`` pass + * ``qiskit.transpiler.passes.RZXCalibrationBuilder`` pass + * ``qiskit.transpiler.passes.RZXCalibrationBuilderNoEcho`` pass + * ``qiskit.transpiler.passes.EchoRZXWeylDecomposition`` pass + * ``qiskit.transpiler.passes.NoramlizeRXAngle`` pass + * ``qiskit.transpiler.passes.rzx_templates()`` function The ``inst_map`` argument has been removed from the following elements: diff --git a/releasenotes/notes/remove-pulse-eb43f66499092489.yaml b/releasenotes/notes/remove-pulse-eb43f66499092489.yaml new file mode 100644 index 000000000000..24b365d8a6a7 --- /dev/null +++ b/releasenotes/notes/remove-pulse-eb43f66499092489.yaml @@ -0,0 +1,28 @@ +--- +upgrade: + - | + Qiskit Pulse has been completely removed in this release, following its deprecation in Qiskit 1.3. + This include all pulse module files, pulse visualization functionality, support for ``ScheduleBlock`` + and pulse-gate serialization/deserialization capability in QPY, calibrations management in + :class:`.QuantumCircuit`, :class:`.Target` and :class:`.DAGCircuit` and pulse-based fake backends. + For more details about the removed components related to pulse, see the corresponding sections below. + + Note that Pulse migration to Qiskit Dynamics, as was the initial plan following the deprecation of Pulse, + has been put on hold due to Qiskit Dynamics development priorities. Users wanting to use Qiskit Pulse + as a frontend to supporting backends or in other uses-cases can still use it via Qiskit versions prior + to 2.0, which include Pulse functionality. + +upgrade_providers: + - | + As part of Pulse removal in Qiskit 2.0, the following methods have been removed: + + * ``qiskit.providers.BackendV2.instruction_schedule_map`` + * ``qiskit.providers.BackendV2.drive_channel`` + * ``qiskit.providers.BackendV2.measure_channel`` + * ``qiskit.providers.BackendV2.acquire_channel`` + * ``qiskit.providers.BackendV2.control_channel`` +upgrade_visualization: + - | + As part of the Pulse removal in Qiskit 2.0, support for pulse drawing via + ``qiskit.visualization.pulse_drawer`` has been removed. + diff --git a/test/python/primitives/test_backend_sampler_v2.py b/test/python/primitives/test_backend_sampler_v2.py index cd13975f8d9c..f0c85517d44a 100644 --- a/test/python/primitives/test_backend_sampler_v2.py +++ b/test/python/primitives/test_backend_sampler_v2.py @@ -708,7 +708,7 @@ def test_circuit_with_multiple_cregs(self, backend): self.assertTrue(hasattr(data, creg.name)) self._assert_allclose(getattr(data, creg.name), np.array(target[creg.name])) - @unittest.skipUnless(optionals.HAS_AER, "Aer is required to simuate control flow") + @unittest.skipUnless(optionals.HAS_AER, "Aer is required to simulate control flow") def test_circuit_with_aliased_cregs(self): """Test for circuit with aliased classical registers.""" backend = GenericBackendV2( diff --git a/test/python/pulse/__init__.py b/test/python/pulse/__init__.py deleted file mode 100644 index d3a35c2bafeb..000000000000 --- a/test/python/pulse/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# 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. - -"""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 deleted file mode 100644 index 4a6a879e00cc..000000000000 --- a/test/python/pulse/test_block.py +++ /dev/null @@ -1,930 +0,0 @@ -# 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. - -# pylint: disable=invalid-name - -"""Test cases for the pulse schedule block.""" -import re -from typing import List, Any -from qiskit import pulse, circuit -from qiskit.pulse import transforms -from qiskit.pulse.exceptions import PulseError -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class BaseTestBlock(QiskitTestCase): - """ScheduleBlock tests.""" - - @ignore_pulse_deprecation_warnings - def setUp(self): - super().setUp() - - self.test_waveform0 = pulse.Constant(100, 0.1) - self.test_waveform1 = pulse.Constant(200, 0.1) - - self.d0 = pulse.DriveChannel(0) - self.d1 = pulse.DriveChannel(1) - - self.left_context = transforms.AlignLeft() - self.right_context = transforms.AlignRight() - self.sequential_context = transforms.AlignSequential() - self.equispaced_context = transforms.AlignEquispaced(duration=1000) - - def _align_func(j): - return {1: 0.1, 2: 0.25, 3: 0.7, 4: 0.85}.get(j) - - self.func_context = transforms.AlignFunc(duration=1000, func=_align_func) - - def assertScheduleEqual(self, target, reference): - """Check if two block are equal schedule representation.""" - self.assertEqual(transforms.target_qobj_transform(target), reference) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestTransformation(BaseTestBlock): - """Test conversion of ScheduleBlock to Schedule.""" - - def test_left_alignment(self): - """Test left alignment context.""" - block = pulse.ScheduleBlock(alignment_context=self.left_context) - block = block.append(pulse.Play(self.test_waveform0, self.d0)) - block = block.append(pulse.Play(self.test_waveform1, self.d1)) - - ref_sched = pulse.Schedule() - ref_sched = ref_sched.insert(0, pulse.Play(self.test_waveform0, self.d0)) - ref_sched = ref_sched.insert(0, pulse.Play(self.test_waveform1, self.d1)) - - self.assertScheduleEqual(block, ref_sched) - - def test_right_alignment(self): - """Test right alignment context.""" - block = pulse.ScheduleBlock(alignment_context=self.right_context) - block = block.append(pulse.Play(self.test_waveform0, self.d0)) - block = block.append(pulse.Play(self.test_waveform1, self.d1)) - - ref_sched = pulse.Schedule() - ref_sched = ref_sched.insert(100, pulse.Play(self.test_waveform0, self.d0)) - ref_sched = ref_sched.insert(0, pulse.Play(self.test_waveform1, self.d1)) - - self.assertScheduleEqual(block, ref_sched) - - def test_sequential_alignment(self): - """Test sequential alignment context.""" - block = pulse.ScheduleBlock(alignment_context=self.sequential_context) - block = block.append(pulse.Play(self.test_waveform0, self.d0)) - block = block.append(pulse.Play(self.test_waveform1, self.d1)) - - ref_sched = pulse.Schedule() - ref_sched = ref_sched.insert(0, pulse.Play(self.test_waveform0, self.d0)) - ref_sched = ref_sched.insert(100, pulse.Play(self.test_waveform1, self.d1)) - - self.assertScheduleEqual(block, ref_sched) - - def test_equispace_alignment(self): - """Test equispace alignment context.""" - block = pulse.ScheduleBlock(alignment_context=self.equispaced_context) - for _ in range(4): - block = block.append(pulse.Play(self.test_waveform0, self.d0)) - - ref_sched = pulse.Schedule() - ref_sched = ref_sched.insert(0, pulse.Play(self.test_waveform0, self.d0)) - ref_sched = ref_sched.insert(300, pulse.Play(self.test_waveform0, self.d0)) - ref_sched = ref_sched.insert(600, pulse.Play(self.test_waveform0, self.d0)) - ref_sched = ref_sched.insert(900, pulse.Play(self.test_waveform0, self.d0)) - - self.assertScheduleEqual(block, ref_sched) - - def test_func_alignment(self): - """Test func alignment context.""" - block = pulse.ScheduleBlock(alignment_context=self.func_context) - for _ in range(4): - block = block.append(pulse.Play(self.test_waveform0, self.d0)) - - ref_sched = pulse.Schedule() - ref_sched = ref_sched.insert(50, pulse.Play(self.test_waveform0, self.d0)) - ref_sched = ref_sched.insert(200, pulse.Play(self.test_waveform0, self.d0)) - ref_sched = ref_sched.insert(650, pulse.Play(self.test_waveform0, self.d0)) - ref_sched = ref_sched.insert(800, pulse.Play(self.test_waveform0, self.d0)) - - self.assertScheduleEqual(block, ref_sched) - - def test_nested_alignment(self): - """Test nested block scheduling.""" - block_sub = pulse.ScheduleBlock(alignment_context=self.right_context) - block_sub = block_sub.append(pulse.Play(self.test_waveform0, self.d0)) - block_sub = block_sub.append(pulse.Play(self.test_waveform1, self.d1)) - - block_main = pulse.ScheduleBlock(alignment_context=self.sequential_context) - block_main = block_main.append(block_sub) - block_main = block_main.append(pulse.Delay(10, self.d0)) - block_main = block_main.append(block_sub) - - ref_sched = pulse.Schedule() - ref_sched = ref_sched.insert(0, pulse.Play(self.test_waveform1, self.d1)) - ref_sched = ref_sched.insert(100, pulse.Play(self.test_waveform0, self.d0)) - ref_sched = ref_sched.insert(200, pulse.Delay(10, self.d0)) - ref_sched = ref_sched.insert(210, pulse.Play(self.test_waveform1, self.d1)) - ref_sched = ref_sched.insert(310, pulse.Play(self.test_waveform0, self.d0)) - - self.assertScheduleEqual(block_main, ref_sched) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestBlockOperation(BaseTestBlock): - """Test fundamental operation on schedule block. - - Because ScheduleBlock adapts to the lazy scheduling, no uniitest for - overlap constraints is necessary. Test scheme becomes simpler than the schedule. - - Some tests have dependency on schedule conversion. - This operation should be tested in `test.python.pulse.test_block.TestTransformation`. - """ - - @ignore_pulse_deprecation_warnings - def setUp(self): - super().setUp() - - self.test_blocks = [ - pulse.Play(self.test_waveform0, self.d0), - pulse.Play(self.test_waveform1, self.d1), - pulse.Delay(50, self.d0), - pulse.Play(self.test_waveform1, self.d0), - ] - - def test_append_an_instruction_to_empty_block(self): - """Test append instructions to an empty block.""" - block = pulse.ScheduleBlock() - block = block.append(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.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.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.""" - block = pulse.ScheduleBlock() - block.append(pulse.Play(self.test_waveform0, self.d0), inplace=True) - - block_main = pulse.ScheduleBlock() - block_main = block_main.append(block) - - self.assertEqual(block_main.blocks[0], block) - - def test_append_an_instruction_to_block(self): - """Test append instructions to a non-empty block.""" - block = pulse.ScheduleBlock() - block = block.append(pulse.Delay(100, self.d0)) - - block = block.append(pulse.Delay(100, self.d0)) - - 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.""" - block = pulse.ScheduleBlock() - block = block.append(pulse.Delay(100, self.d0)) - - block.append(pulse.Delay(100, self.d0), inplace=True) - - self.assertEqual(len(block.blocks), 2) - - def test_duration(self): - """Test if correct duration is returned with implicit scheduling.""" - block = pulse.ScheduleBlock() - for inst in self.test_blocks: - block.append(inst) - - self.assertEqual(block.duration, 350) - - def test_channels(self): - """Test if all channels are returned.""" - block = pulse.ScheduleBlock() - for inst in self.test_blocks: - block.append(inst) - - self.assertEqual(len(block.channels), 2) - - def test_instructions(self): - """Test if all instructions are returned.""" - block = pulse.ScheduleBlock() - for inst in self.test_blocks: - block.append(inst) - - self.assertEqual(block.blocks, tuple(self.test_blocks)) - - def test_channel_duraction(self): - """Test if correct durations is calculated for each channel.""" - block = pulse.ScheduleBlock() - for inst in self.test_blocks: - block.append(inst) - - self.assertEqual(block.ch_duration(self.d0), 350) - self.assertEqual(block.ch_duration(self.d1), 200) - - def test_cannot_append_schedule(self): - """Test schedule cannot be appended. Schedule should be input as Call instruction.""" - block = pulse.ScheduleBlock() - - sched = pulse.Schedule() - sched += pulse.Delay(10, self.d0) - - with self.assertRaises(PulseError): - block.append(sched) - - def test_replace(self): - """Test replacing specific instruction.""" - block = pulse.ScheduleBlock() - for inst in self.test_blocks: - block.append(inst) - - replaced = pulse.Play(pulse.Constant(300, 0.1), self.d1) - target = pulse.Delay(50, self.d0) - - block_replaced = block.replace(target, replaced, inplace=False) - - # original schedule is not destroyed - 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)) - ref_sched = ref_sched.insert(0, pulse.Play(self.test_waveform1, self.d1)) - ref_sched = ref_sched.insert(200, replaced) - ref_sched = ref_sched.insert(100, pulse.Play(self.test_waveform1, self.d0)) - - self.assertScheduleEqual(block_replaced, ref_sched) - - def test_replace_inplace(self): - """Test replacing specific instruction with inplace.""" - block = pulse.ScheduleBlock() - for inst in self.test_blocks: - block.append(inst) - - replaced = pulse.Play(pulse.Constant(300, 0.1), self.d1) - target = pulse.Delay(50, self.d0) - - block.replace(target, replaced, inplace=True) - - ref_sched = pulse.Schedule() - ref_sched = ref_sched.insert(0, pulse.Play(self.test_waveform0, self.d0)) - ref_sched = ref_sched.insert(0, pulse.Play(self.test_waveform1, self.d1)) - ref_sched = ref_sched.insert(200, replaced) - ref_sched = ref_sched.insert(100, pulse.Play(self.test_waveform1, self.d0)) - - self.assertScheduleEqual(block, ref_sched) - - def test_replace_block_by_instruction(self): - """Test replacing block with instruction.""" - sub_block1 = pulse.ScheduleBlock() - sub_block1 = sub_block1.append(pulse.Delay(50, self.d0)) - sub_block1 = sub_block1.append(pulse.Play(self.test_waveform0, self.d0)) - - sub_block2 = pulse.ScheduleBlock() - sub_block2 = sub_block2.append(pulse.Delay(50, self.d0)) - sub_block2 = sub_block2.append(pulse.Play(self.test_waveform1, self.d1)) - - main_block = pulse.ScheduleBlock() - main_block = main_block.append(pulse.Delay(50, self.d0)) - main_block = main_block.append(pulse.Play(self.test_waveform0, self.d0)) - main_block = main_block.append(sub_block1) - main_block = main_block.append(sub_block2) - main_block = main_block.append(pulse.Play(self.test_waveform0, self.d1)) - - replaced = main_block.replace(sub_block1, pulse.Delay(100, self.d0)) - - ref_blocks = [ - pulse.Delay(50, self.d0), - pulse.Play(self.test_waveform0, self.d0), - pulse.Delay(100, self.d0), - sub_block2, - pulse.Play(self.test_waveform0, self.d1), - ] - - self.assertListEqual(list(replaced.blocks), ref_blocks) - - def test_replace_instruction_by_block(self): - """Test replacing instruction with block.""" - sub_block1 = pulse.ScheduleBlock() - sub_block1 = sub_block1.append(pulse.Delay(50, self.d0)) - sub_block1 = sub_block1.append(pulse.Play(self.test_waveform0, self.d0)) - - sub_block2 = pulse.ScheduleBlock() - sub_block2 = sub_block2.append(pulse.Delay(50, self.d0)) - sub_block2 = sub_block2.append(pulse.Play(self.test_waveform1, self.d1)) - - main_block = pulse.ScheduleBlock() - main_block = main_block.append(pulse.Delay(50, self.d0)) - main_block = main_block.append(pulse.Play(self.test_waveform0, self.d0)) - main_block = main_block.append(pulse.Delay(100, self.d0)) - main_block = main_block.append(sub_block2) - main_block = main_block.append(pulse.Play(self.test_waveform0, self.d1)) - - replaced = main_block.replace(pulse.Delay(100, self.d0), sub_block1) - - ref_blocks = [ - pulse.Delay(50, self.d0), - pulse.Play(self.test_waveform0, self.d0), - sub_block1, - sub_block2, - pulse.Play(self.test_waveform0, self.d1), - ] - - self.assertListEqual(list(replaced.blocks), ref_blocks) - - def test_len(self): - """Test __len__ method""" - block = pulse.ScheduleBlock() - self.assertEqual(len(block), 0) - - for j in range(1, 10): - block = block.append(pulse.Delay(10, self.d0)) - self.assertEqual(len(block), j) - - def test_inherit_from(self): - """Test creating schedule with another schedule.""" - ref_metadata = {"test": "value"} - ref_name = "test" - - base_sched = pulse.ScheduleBlock(name=ref_name, metadata=ref_metadata) - new_sched = pulse.ScheduleBlock.initialize_from(base_sched) - - self.assertEqual(new_sched.name, ref_name) - self.assertDictEqual(new_sched.metadata, ref_metadata) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestBlockEquality(BaseTestBlock): - """Test equality of blocks. - - Equality of instruction ordering is compared on DAG representation. - This should be tested for each transform. - """ - - def test_different_channels(self): - """Test equality is False if different channels.""" - block1 = pulse.ScheduleBlock() - block1 += pulse.Delay(10, self.d0) - - block2 = pulse.ScheduleBlock() - block2 += pulse.Delay(10, self.d1) - - self.assertNotEqual(block1, block2) - - def test_different_transform(self): - """Test equality is False if different transforms.""" - block1 = pulse.ScheduleBlock(alignment_context=self.left_context) - block1 += pulse.Delay(10, self.d0) - - block2 = pulse.ScheduleBlock(alignment_context=self.right_context) - block2 += pulse.Delay(10, self.d0) - - self.assertNotEqual(block1, block2) - - def test_different_transform_opts(self): - """Test equality is False if different transform options.""" - context1 = transforms.AlignEquispaced(duration=100) - context2 = transforms.AlignEquispaced(duration=500) - - block1 = pulse.ScheduleBlock(alignment_context=context1) - block1 += pulse.Delay(10, self.d0) - - block2 = pulse.ScheduleBlock(alignment_context=context2) - block2 += pulse.Delay(10, self.d0) - - self.assertNotEqual(block1, block2) - - def test_instruction_out_of_order_left(self): - """Test equality is True if two blocks have instructions in different order.""" - block1 = pulse.ScheduleBlock(alignment_context=self.left_context) - block1 += pulse.Play(self.test_waveform0, self.d0) - block1 += pulse.Play(self.test_waveform0, self.d1) - - block2 = pulse.ScheduleBlock(alignment_context=self.left_context) - block2 += pulse.Play(self.test_waveform0, self.d1) - block2 += pulse.Play(self.test_waveform0, self.d0) - - self.assertEqual(block1, block2) - - def test_instruction_in_order_left(self): - """Test equality is True if two blocks have instructions in same order.""" - block1 = pulse.ScheduleBlock(alignment_context=self.left_context) - block1 += pulse.Play(self.test_waveform0, self.d0) - block1 += pulse.Play(self.test_waveform0, self.d1) - - block2 = pulse.ScheduleBlock(alignment_context=self.left_context) - block2 += pulse.Play(self.test_waveform0, self.d0) - block2 += pulse.Play(self.test_waveform0, self.d1) - - self.assertEqual(block1, block2) - - def test_instruction_out_of_order_right(self): - """Test equality is True if two blocks have instructions in different order.""" - block1 = pulse.ScheduleBlock(alignment_context=self.right_context) - block1 += pulse.Play(self.test_waveform0, self.d0) - block1 += pulse.Play(self.test_waveform0, self.d1) - - block2 = pulse.ScheduleBlock(alignment_context=self.right_context) - block2 += pulse.Play(self.test_waveform0, self.d1) - block2 += pulse.Play(self.test_waveform0, self.d0) - - self.assertEqual(block1, block2) - - def test_instruction_in_order_right(self): - """Test equality is True if two blocks have instructions in same order.""" - block1 = pulse.ScheduleBlock(alignment_context=self.right_context) - block1 += pulse.Play(self.test_waveform0, self.d0) - block1 += pulse.Play(self.test_waveform0, self.d1) - - block2 = pulse.ScheduleBlock(alignment_context=self.right_context) - block2 += pulse.Play(self.test_waveform0, self.d0) - block2 += pulse.Play(self.test_waveform0, self.d1) - - self.assertEqual(block1, block2) - - def test_instruction_out_of_order_sequential(self): - """Test equality is False if two blocks have instructions in different order.""" - block1 = pulse.ScheduleBlock(alignment_context=self.sequential_context) - block1 += pulse.Play(self.test_waveform0, self.d0) - block1 += pulse.Play(self.test_waveform0, self.d1) - - block2 = pulse.ScheduleBlock(alignment_context=self.sequential_context) - block2 += pulse.Play(self.test_waveform0, self.d1) - block2 += pulse.Play(self.test_waveform0, self.d0) - - self.assertNotEqual(block1, block2) - - def test_instruction_out_of_order_sequential_more(self): - """Test equality is False if three blocks have instructions in different order. - - This could detect a particular bug as discussed in this thread: - https://github.com/Qiskit/qiskit-terra/pull/8005#discussion_r966191018 - """ - block1 = pulse.ScheduleBlock(alignment_context=self.sequential_context) - block1 += pulse.Play(self.test_waveform0, self.d0) - block1 += pulse.Play(self.test_waveform0, self.d0) - block1 += pulse.Play(self.test_waveform0, self.d1) - - block2 = pulse.ScheduleBlock(alignment_context=self.sequential_context) - block2 += pulse.Play(self.test_waveform0, self.d0) - block2 += pulse.Play(self.test_waveform0, self.d1) - block2 += pulse.Play(self.test_waveform0, self.d0) - - self.assertNotEqual(block1, block2) - - def test_instruction_in_order_sequential(self): - """Test equality is True if two blocks have instructions in same order.""" - block1 = pulse.ScheduleBlock(alignment_context=self.sequential_context) - block1 += pulse.Play(self.test_waveform0, self.d0) - block1 += pulse.Play(self.test_waveform0, self.d1) - - block2 = pulse.ScheduleBlock(alignment_context=self.sequential_context) - block2 += pulse.Play(self.test_waveform0, self.d0) - block2 += pulse.Play(self.test_waveform0, self.d1) - - self.assertEqual(block1, block2) - - def test_instruction_out_of_order_equispaced(self): - """Test equality is False if two blocks have instructions in different order.""" - block1 = pulse.ScheduleBlock(alignment_context=self.equispaced_context) - block1 += pulse.Play(self.test_waveform0, self.d0) - block1 += pulse.Play(self.test_waveform0, self.d1) - - block2 = pulse.ScheduleBlock(alignment_context=self.equispaced_context) - block2 += pulse.Play(self.test_waveform0, self.d1) - block2 += pulse.Play(self.test_waveform0, self.d0) - - self.assertNotEqual(block1, block2) - - def test_instruction_in_order_equispaced(self): - """Test equality is True if two blocks have instructions in same order.""" - block1 = pulse.ScheduleBlock(alignment_context=self.equispaced_context) - block1 += pulse.Play(self.test_waveform0, self.d0) - block1 += pulse.Play(self.test_waveform0, self.d1) - - block2 = pulse.ScheduleBlock(alignment_context=self.equispaced_context) - block2 += pulse.Play(self.test_waveform0, self.d0) - block2 += pulse.Play(self.test_waveform0, self.d1) - - self.assertEqual(block1, block2) - - def test_instruction_out_of_order_func(self): - """Test equality is False if two blocks have instructions in different order.""" - block1 = pulse.ScheduleBlock(alignment_context=self.func_context) - block1 += pulse.Play(self.test_waveform0, self.d0) - block1 += pulse.Play(self.test_waveform0, self.d1) - - block2 = pulse.ScheduleBlock(alignment_context=self.func_context) - block2 += pulse.Play(self.test_waveform0, self.d1) - block2 += pulse.Play(self.test_waveform0, self.d0) - - self.assertNotEqual(block1, block2) - - def test_instruction_in_order_func(self): - """Test equality is True if two blocks have instructions in same order.""" - block1 = pulse.ScheduleBlock(alignment_context=self.func_context) - block1 += pulse.Play(self.test_waveform0, self.d0) - block1 += pulse.Play(self.test_waveform0, self.d1) - - block2 = pulse.ScheduleBlock(alignment_context=self.func_context) - block2 += pulse.Play(self.test_waveform0, self.d0) - block2 += pulse.Play(self.test_waveform0, self.d1) - - self.assertEqual(block1, block2) - - def test_instrution_in_oder_but_different_node(self): - """Test equality is False if two blocks have different instructions.""" - block1 = pulse.ScheduleBlock(alignment_context=self.left_context) - block1 += pulse.Play(self.test_waveform0, self.d0) - block1 += pulse.Play(self.test_waveform1, self.d1) - - block2 = pulse.ScheduleBlock(alignment_context=self.left_context) - block2 += pulse.Play(self.test_waveform0, self.d0) - block2 += pulse.Play(self.test_waveform0, self.d1) - - self.assertNotEqual(block1, block2) - - def test_instruction_out_of_order_complex_equal(self): - """Test complex schedule equality can be correctly evaluated.""" - block1_a = pulse.ScheduleBlock(alignment_context=self.left_context) - block1_a += pulse.Delay(10, self.d0) - block1_a += pulse.Play(self.test_waveform1, self.d1) - block1_a += pulse.Play(self.test_waveform0, self.d0) - - block1_b = pulse.ScheduleBlock(alignment_context=self.left_context) - block1_b += pulse.Play(self.test_waveform1, self.d1) - block1_b += pulse.Delay(10, self.d0) - block1_b += pulse.Play(self.test_waveform0, self.d0) - - block2_a = pulse.ScheduleBlock(alignment_context=self.right_context) - block2_a += block1_a - block2_a += block1_b - block2_a += block1_a - - block2_b = pulse.ScheduleBlock(alignment_context=self.right_context) - block2_b += block1_a - block2_b += block1_a - block2_b += block1_b - - self.assertEqual(block2_a, block2_b) - - def test_instruction_out_of_order_complex_not_equal(self): - """Test complex schedule equality can be correctly evaluated.""" - block1_a = pulse.ScheduleBlock(alignment_context=self.left_context) - block1_a += pulse.Play(self.test_waveform0, self.d0) - block1_a += pulse.Play(self.test_waveform1, self.d1) - block1_a += pulse.Delay(10, self.d0) - - block1_b = pulse.ScheduleBlock(alignment_context=self.left_context) - block1_b += pulse.Play(self.test_waveform1, self.d1) - block1_b += pulse.Delay(10, self.d0) - block1_b += pulse.Play(self.test_waveform0, self.d0) - - block2_a = pulse.ScheduleBlock(alignment_context=self.right_context) - block2_a += block1_a - block2_a += block1_b - block2_a += block1_a - - block2_b = pulse.ScheduleBlock(alignment_context=self.right_context) - block2_b += block1_a - block2_b += block1_a - block2_b += block1_b - - self.assertNotEqual(block2_a, block2_b) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestParametrizedBlockOperation(BaseTestBlock): - """Test fundamental operation with parametrization.""" - - @ignore_pulse_deprecation_warnings - def setUp(self): - super().setUp() - - self.amp0 = circuit.Parameter("amp0") - self.amp1 = circuit.Parameter("amp1") - self.dur0 = circuit.Parameter("dur0") - self.dur1 = circuit.Parameter("dur1") - - self.test_par_waveform0 = pulse.Constant(self.dur0, self.amp0) - self.test_par_waveform1 = pulse.Constant(self.dur1, self.amp1) - - def test_report_parameter_assignment(self): - """Test duration assignment check.""" - block = pulse.ScheduleBlock() - block += pulse.Play(self.test_par_waveform0, self.d0) - - # check parameter evaluation mechanism - self.assertTrue(block.is_parameterized()) - self.assertFalse(block.is_schedulable()) - - # assign duration - block = block.assign_parameters({self.dur0: 200}) - self.assertTrue(block.is_parameterized()) - self.assertTrue(block.is_schedulable()) - - def test_cannot_get_duration_if_not_assigned(self): - """Test raise error when duration is not assigned.""" - block = pulse.ScheduleBlock() - block += pulse.Play(self.test_par_waveform0, self.d0) - - with self.assertRaises(PulseError): - # pylint: disable=pointless-statement - block.duration - - def test_get_assigend_duration(self): - """Test duration is correctly evaluated.""" - block = pulse.ScheduleBlock() - block += pulse.Play(self.test_par_waveform0, self.d0) - block += pulse.Play(self.test_waveform0, self.d0) - - block = block.assign_parameters({self.dur0: 300}) - - self.assertEqual(block.duration, 400) - - def test_equality_of_parametrized_channels(self): - """Test check equality of blocks involving parametrized channels.""" - par_ch = circuit.Parameter("ch") - - block1 = pulse.ScheduleBlock(alignment_context=self.left_context) - block1 += pulse.Play(self.test_waveform0, pulse.DriveChannel(par_ch)) - block1 += pulse.Play(self.test_par_waveform0, self.d0) - - block2 = pulse.ScheduleBlock(alignment_context=self.left_context) - block2 += pulse.Play(self.test_par_waveform0, self.d0) - block2 += pulse.Play(self.test_waveform0, pulse.DriveChannel(par_ch)) - - self.assertEqual(block1, block2) - - block1_assigned = block1.assign_parameters({par_ch: 1}) - block2_assigned = block2.assign_parameters({par_ch: 1}) - self.assertEqual(block1_assigned, block2_assigned) - - def test_replace_parametrized_instruction(self): - """Test parametrized instruction can updated with parameter table.""" - block = pulse.ScheduleBlock() - block += pulse.Play(self.test_par_waveform0, self.d0) - block += pulse.Delay(100, self.d0) - block += pulse.Play(self.test_waveform0, self.d0) - - replaced = block.replace( - pulse.Play(self.test_par_waveform0, self.d0), - pulse.Play(self.test_par_waveform1, self.d0), - ) - self.assertTrue(replaced.is_parameterized()) - - # check assign parameters - replaced_assigned = replaced.assign_parameters({self.dur1: 100, self.amp1: 0.1}) - self.assertFalse(replaced_assigned.is_parameterized()) - - def test_parametrized_context(self): - """Test parametrize context parameter.""" - duration = circuit.Parameter("dur") - param_context = transforms.AlignEquispaced(duration=duration) - - block = pulse.ScheduleBlock(alignment_context=param_context) - block += pulse.Delay(10, self.d0) - block += pulse.Delay(10, self.d0) - block += pulse.Delay(10, self.d0) - block += pulse.Delay(10, self.d0) - self.assertTrue(block.is_parameterized()) - self.assertFalse(block.is_schedulable()) - - block.assign_parameters({duration: 100}, inplace=True) - self.assertFalse(block.is_parameterized()) - self.assertTrue(block.is_schedulable()) - - ref_sched = pulse.Schedule() - ref_sched = ref_sched.insert(0, pulse.Delay(10, self.d0)) - ref_sched = ref_sched.insert(30, pulse.Delay(10, self.d0)) - ref_sched = ref_sched.insert(60, pulse.Delay(10, self.d0)) - ref_sched = ref_sched.insert(90, pulse.Delay(10, self.d0)) - - self.assertScheduleEqual(block, ref_sched) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestBlockFilter(BaseTestBlock): - """Test ScheduleBlock filtering methods.""" - - def test_filter_channels(self): - """Test filtering over channels.""" - with pulse.build() as blk: - pulse.play(self.test_waveform0, self.d0) - pulse.delay(10, self.d0) - pulse.play(self.test_waveform1, self.d1) - - filtered_blk = self._filter_and_test_consistency(blk, channels=[self.d0]) - self.assertEqual(len(filtered_blk.channels), 1) - self.assertTrue(self.d0 in filtered_blk.channels) - with pulse.build() as ref_blk: - pulse.play(self.test_waveform0, self.d0) - pulse.delay(10, self.d0) - self.assertEqual(filtered_blk, ref_blk) - - filtered_blk = self._filter_and_test_consistency(blk, channels=[self.d1]) - self.assertEqual(len(filtered_blk.channels), 1) - self.assertTrue(self.d1 in filtered_blk.channels) - with pulse.build() as ref_blk: - pulse.play(self.test_waveform1, self.d1) - self.assertEqual(filtered_blk, ref_blk) - - filtered_blk = self._filter_and_test_consistency(blk, channels=[self.d0, self.d1]) - self.assertEqual(len(filtered_blk.channels), 2) - for ch in [self.d0, self.d1]: - self.assertTrue(ch in filtered_blk.channels) - self.assertEqual(filtered_blk, blk) - - def test_filter_inst_types(self): - """Test filtering on instruction types.""" - with pulse.build() as blk: - pulse.acquire(5, pulse.AcquireChannel(0), pulse.MemorySlot(0)) - - with pulse.build() as blk_internal: - pulse.play(self.test_waveform1, self.d1) - - pulse.call(blk_internal) - pulse.reference(name="dummy_reference") - pulse.delay(10, self.d0) - pulse.play(self.test_waveform0, self.d0) - pulse.barrier(self.d0, self.d1, pulse.AcquireChannel(0), pulse.MemorySlot(0)) - pulse.set_frequency(10, self.d0) - pulse.shift_frequency(5, self.d1) - pulse.set_phase(3.14 / 4.0, self.d0) - pulse.shift_phase(-3.14 / 2.0, self.d1) - pulse.snapshot(label="dummy_snapshot") - - # test filtering Acquire - filtered_blk = self._filter_and_test_consistency(blk, instruction_types=[pulse.Acquire]) - self.assertEqual(len(filtered_blk.blocks), 1) - self.assertIsInstance(filtered_blk.blocks[0], pulse.Acquire) - self.assertEqual(len(filtered_blk.channels), 2) - - # test filtering Reference - filtered_blk = self._filter_and_test_consistency( - blk, instruction_types=[pulse.instructions.Reference] - ) - self.assertEqual(len(filtered_blk.blocks), 1) - self.assertIsInstance(filtered_blk.blocks[0], pulse.instructions.Reference) - - # test filtering Delay - filtered_blk = self._filter_and_test_consistency(blk, instruction_types=[pulse.Delay]) - self.assertEqual(len(filtered_blk.blocks), 1) - self.assertIsInstance(filtered_blk.blocks[0], pulse.Delay) - self.assertEqual(len(filtered_blk.channels), 1) - - # test filtering Play - filtered_blk = self._filter_and_test_consistency(blk, instruction_types=[pulse.Play]) - self.assertEqual(len(filtered_blk.blocks), 2) - self.assertIsInstance(filtered_blk.blocks[0].blocks[0], pulse.Play) - self.assertIsInstance(filtered_blk.blocks[1], pulse.Play) - self.assertEqual(len(filtered_blk.channels), 2) - - # test filtering RelativeBarrier - filtered_blk = self._filter_and_test_consistency( - blk, instruction_types=[pulse.instructions.RelativeBarrier] - ) - self.assertEqual(len(filtered_blk.blocks), 1) - self.assertIsInstance(filtered_blk.blocks[0], pulse.instructions.RelativeBarrier) - self.assertEqual(len(filtered_blk.channels), 4) - - # test filtering SetFrequency - filtered_blk = self._filter_and_test_consistency( - blk, instruction_types=[pulse.SetFrequency] - ) - self.assertEqual(len(filtered_blk.blocks), 1) - self.assertIsInstance(filtered_blk.blocks[0], pulse.SetFrequency) - self.assertEqual(len(filtered_blk.channels), 1) - - # test filtering ShiftFrequency - filtered_blk = self._filter_and_test_consistency( - blk, instruction_types=[pulse.ShiftFrequency] - ) - self.assertEqual(len(filtered_blk.blocks), 1) - self.assertIsInstance(filtered_blk.blocks[0], pulse.ShiftFrequency) - self.assertEqual(len(filtered_blk.channels), 1) - - # test filtering SetPhase - filtered_blk = self._filter_and_test_consistency(blk, instruction_types=[pulse.SetPhase]) - self.assertEqual(len(filtered_blk.blocks), 1) - self.assertIsInstance(filtered_blk.blocks[0], pulse.SetPhase) - self.assertEqual(len(filtered_blk.channels), 1) - - # test filtering ShiftPhase - filtered_blk = self._filter_and_test_consistency(blk, instruction_types=[pulse.ShiftPhase]) - self.assertEqual(len(filtered_blk.blocks), 1) - self.assertIsInstance(filtered_blk.blocks[0], pulse.ShiftPhase) - self.assertEqual(len(filtered_blk.channels), 1) - - # test filtering SnapShot - filtered_blk = self._filter_and_test_consistency(blk, instruction_types=[pulse.Snapshot]) - self.assertEqual(len(filtered_blk.blocks), 1) - self.assertIsInstance(filtered_blk.blocks[0], pulse.Snapshot) - self.assertEqual(len(filtered_blk.channels), 1) - - def test_filter_functionals(self): - """Test functional filtering.""" - with pulse.build() as blk: - pulse.play(self.test_waveform0, self.d0, "play0") - pulse.delay(10, self.d0, "delay0") - - with pulse.build() as blk_internal: - pulse.play(self.test_waveform1, self.d1, "play1") - - pulse.call(blk_internal) - pulse.play(self.test_waveform1, self.d1) - - def filter_with_inst_name(inst: pulse.Instruction) -> bool: - try: - if isinstance(inst.name, str): - match_obj = re.search(pattern="play", string=inst.name) - if match_obj is not None: - return True - except AttributeError: - pass - return False - - filtered_blk = self._filter_and_test_consistency(blk, filter_with_inst_name) - self.assertEqual(len(filtered_blk.blocks), 2) - self.assertIsInstance(filtered_blk.blocks[0], pulse.Play) - self.assertIsInstance(filtered_blk.blocks[1].blocks[0], pulse.Play) - self.assertEqual(len(filtered_blk.channels), 2) - - def test_filter_multiple(self): - """Test filter composition.""" - with pulse.build() as blk: - pulse.play(pulse.Constant(100, 0.1, name="play0"), self.d0) - pulse.delay(10, self.d0, "delay0") - - with pulse.build(name="internal_blk") as blk_internal: - pulse.play(pulse.Constant(50, 0.1, name="play1"), self.d0) - - pulse.call(blk_internal) - pulse.barrier(self.d0, self.d1) - pulse.play(pulse.Constant(100, 0.1, name="play2"), self.d1) - - def filter_with_pulse_name(inst: pulse.Instruction) -> bool: - try: - if isinstance(inst.pulse.name, str): - match_obj = re.search(pattern="play", string=inst.pulse.name) - if match_obj is not None: - return True - except AttributeError: - pass - return False - - filtered_blk = self._filter_and_test_consistency( - blk, filter_with_pulse_name, channels=[self.d1], instruction_types=[pulse.Play] - ) - self.assertEqual(len(filtered_blk.blocks), 1) - self.assertIsInstance(filtered_blk.blocks[0], pulse.Play) - self.assertEqual(len(filtered_blk.channels), 1) - - def _filter_and_test_consistency( - self, sched_blk: pulse.ScheduleBlock, *args: Any, **kwargs: Any - ) -> pulse.ScheduleBlock: - """ - Returns sched_blk.filter(*args, **kwargs), - including a test that sched_blk.filter | sched_blk.exclude == sched_blk - in terms of instructions. - """ - filtered = sched_blk.filter(*args, **kwargs) - excluded = sched_blk.exclude(*args, **kwargs) - - def list_instructions(blk: pulse.ScheduleBlock) -> List[pulse.Instruction]: - insts = [] - for element in blk.blocks: - if isinstance(element, pulse.ScheduleBlock): - inner_insts = list_instructions(element) - if len(inner_insts) != 0: - insts.extend(inner_insts) - elif isinstance(element, pulse.Instruction): - insts.append(element) - return insts - - sum_insts = list_instructions(filtered) + list_instructions(excluded) - ref_insts = list_instructions(sched_blk) - self.assertEqual(len(sum_insts), len(ref_insts)) - self.assertTrue(all(inst in ref_insts for inst in sum_insts)) - return filtered diff --git a/test/python/pulse/test_calibration_entries.py b/test/python/pulse/test_calibration_entries.py deleted file mode 100644 index 166b03303704..000000000000 --- a/test/python/pulse/test_calibration_entries.py +++ /dev/null @@ -1,274 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test for calibration entries.""" - -import numpy as np - -from qiskit.circuit.parameter import Parameter -from qiskit.pulse import ( - Schedule, - ScheduleBlock, - Play, - Constant, - DriveChannel, -) -from qiskit.pulse.calibration_entries import ( - ScheduleDef, - CallableDef, -) -from qiskit.pulse.exceptions import PulseError -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestSchedule(QiskitTestCase): - """Test case for the ScheduleDef.""" - - def test_add_schedule(self): - """Basic test pulse Schedule format.""" - program = Schedule() - program.insert( - 0, - Play(Constant(duration=10, amp=0.1, angle=0.0), DriveChannel(0)), - inplace=True, - ) - - entry = ScheduleDef() - entry.define(program) - - signature_to_test = list(entry.get_signature().parameters.keys()) - signature_ref = [] - self.assertListEqual(signature_to_test, signature_ref) - - schedule_to_test = entry.get_schedule() - schedule_ref = program - self.assertEqual(schedule_to_test, schedule_ref) - - def test_add_block(self): - """Basic test pulse Schedule format.""" - program = ScheduleBlock() - program.append( - Play(Constant(duration=10, amp=0.1, angle=0.0), DriveChannel(0)), - inplace=True, - ) - - entry = ScheduleDef() - entry.define(program) - - signature_to_test = list(entry.get_signature().parameters.keys()) - signature_ref = [] - self.assertListEqual(signature_to_test, signature_ref) - - schedule_to_test = entry.get_schedule() - schedule_ref = program - self.assertEqual(schedule_to_test, schedule_ref) - - def test_parameterized_schedule(self): - """Test adding and managing parameterized schedule.""" - param1 = Parameter("P1") - param2 = Parameter("P2") - - program = ScheduleBlock() - program.append( - Play(Constant(duration=param1, amp=param2, angle=0.0), DriveChannel(0)), - inplace=True, - ) - - entry = ScheduleDef() - entry.define(program) - - signature_to_test = list(entry.get_signature().parameters.keys()) - signature_ref = ["P1", "P2"] - self.assertListEqual(signature_to_test, signature_ref) - - schedule_to_test = entry.get_schedule(P1=10, P2=0.1) - schedule_ref = program.assign_parameters({param1: 10, param2: 0.1}, inplace=False) - self.assertEqual(schedule_to_test, schedule_ref) - - def test_parameterized_schedule_with_user_args(self): - """Test adding schedule with user signature. - - Bind parameters to a pulse schedule but expecting non-lexicographical order. - """ - theta = Parameter("theta") - lam = Parameter("lam") - phi = Parameter("phi") - - program = ScheduleBlock() - program.append( - Play(Constant(duration=10, amp=phi, angle=0.0), DriveChannel(0)), - inplace=True, - ) - program.append( - Play(Constant(duration=10, amp=theta, angle=0.0), DriveChannel(0)), - inplace=True, - ) - program.append( - Play(Constant(duration=10, amp=lam, angle=0.0), DriveChannel(0)), - inplace=True, - ) - - entry = ScheduleDef(arguments=["theta", "lam", "phi"]) - entry.define(program) - - signature_to_test = list(entry.get_signature().parameters.keys()) - signature_ref = ["theta", "lam", "phi"] - self.assertListEqual(signature_to_test, signature_ref) - - # Do not specify kwargs. This is order sensitive. - schedule_to_test = entry.get_schedule(0.1, 0.2, 0.3) - schedule_ref = program.assign_parameters( - {theta: 0.1, lam: 0.2, phi: 0.3}, - inplace=False, - ) - self.assertEqual(schedule_to_test, schedule_ref) - - def test_parameterized_schedule_with_wrong_signature(self): - """Test raising PulseError when signature doesn't match.""" - param1 = Parameter("P1") - - program = ScheduleBlock() - program.append( - Play(Constant(duration=10, amp=param1, angle=0.0), DriveChannel(0)), - inplace=True, - ) - - entry = ScheduleDef(arguments=["This_is_wrong_param_name"]) - - with self.assertRaises(PulseError): - entry.define(program) - - def test_equality(self): - """Test equality evaluation between the schedule entries.""" - program1 = Schedule() - program1.insert( - 0, - Play(Constant(duration=10, amp=0.1, angle=0.0), DriveChannel(0)), - inplace=True, - ) - - program2 = Schedule() - program2.insert( - 0, - Play(Constant(duration=10, amp=0.2, angle=0.0), DriveChannel(0)), - inplace=True, - ) - - entry1 = ScheduleDef() - entry1.define(program1) - - entry2 = ScheduleDef() - entry2.define(program2) - - entry3 = ScheduleDef() - entry3.define(program1) - - self.assertEqual(entry1, entry3) - self.assertNotEqual(entry1, entry2) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestCallable(QiskitTestCase): - """Test case for the CallableDef.""" - - def test_add_callable(self): - """Basic test callable format.""" - program = Schedule() - program.insert( - 0, - Play(Constant(duration=10, amp=0.1, angle=0.0), DriveChannel(0)), - inplace=True, - ) - - def factory(): - return program - - entry = CallableDef() - entry.define(factory) - - signature_to_test = list(entry.get_signature().parameters.keys()) - signature_ref = [] - self.assertListEqual(signature_to_test, signature_ref) - - schedule_to_test = entry.get_schedule() - schedule_ref = program - self.assertEqual(schedule_to_test, schedule_ref) - - def test_add_callable_with_argument(self): - """Basic test callable format.""" - - def factory(var1, var2): - program = Schedule() - if var1 > 0: - program.insert( - 0, - Play(Constant(duration=var2, amp=var1, angle=0.0), DriveChannel(0)), - inplace=True, - ) - else: - program.insert( - 0, - Play(Constant(duration=var2, amp=np.abs(var1), angle=np.pi), DriveChannel(0)), - inplace=True, - ) - return program - - entry = CallableDef() - entry.define(factory) - - signature_to_test = list(entry.get_signature().parameters.keys()) - signature_ref = ["var1", "var2"] - self.assertListEqual(signature_to_test, signature_ref) - - schedule_to_test = entry.get_schedule(0.1, 10) - schedule_ref = Schedule() - schedule_ref.insert( - 0, - Play(Constant(duration=10, amp=0.1, angle=0.0), DriveChannel(0)), - inplace=True, - ) - self.assertEqual(schedule_to_test, schedule_ref) - - schedule_to_test = entry.get_schedule(-0.1, 10) - schedule_ref = Schedule() - schedule_ref.insert( - 0, - Play(Constant(duration=10, amp=0.1, angle=np.pi), DriveChannel(0)), - inplace=True, - ) - self.assertEqual(schedule_to_test, schedule_ref) - - def test_equality(self): - """Test equality evaluation between the callable entries. - - This does NOT compare the code. Just object equality. - """ - - def factory1(): - return Schedule() - - def factory2(): - return Schedule() - - entry1 = CallableDef() - entry1.define(factory1) - - entry2 = CallableDef() - entry2.define(factory2) - - entry3 = CallableDef() - entry3.define(factory1) - - self.assertEqual(entry1, entry3) - self.assertNotEqual(entry1, entry2) diff --git a/test/python/pulse/test_channels.py b/test/python/pulse/test_channels.py deleted file mode 100644 index ed104bd6b3a1..000000000000 --- a/test/python/pulse/test_channels.py +++ /dev/null @@ -1,195 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test cases for the pulse channel group.""" - -import unittest - -from qiskit.pulse.channels import ( - AcquireChannel, - Channel, - ClassicalIOChannel, - ControlChannel, - DriveChannel, - MeasureChannel, - MemorySlot, - PulseChannel, - RegisterSlot, - SnapshotChannel, - PulseError, -) -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings - - -class TestChannel(QiskitTestCase): - """Test base channel.""" - - def test_cannot_be_instantiated(self): - """Test base channel cannot be instantiated.""" - with self.assertRaises(NotImplementedError): - Channel(0) - - -class TestPulseChannel(QiskitTestCase): - """Test base pulse channel.""" - - def test_cannot_be_instantiated(self): - """Test base pulse channel cannot be instantiated.""" - with self.assertRaises(NotImplementedError): - PulseChannel(0) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestAcquireChannel(QiskitTestCase): - """AcquireChannel tests.""" - - def test_default(self): - """Test default acquire channel.""" - acquire_channel = AcquireChannel(123) - - self.assertEqual(acquire_channel.index, 123) - self.assertEqual(acquire_channel.name, "a123") - - def test_channel_hash(self): - """Test hashing for acquire channel.""" - acq_channel_1 = AcquireChannel(123) - acq_channel_2 = AcquireChannel(123) - - hash_1 = hash(acq_channel_1) - hash_2 = hash(acq_channel_2) - - self.assertEqual(hash_1, hash_2) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestClassicalIOChannel(QiskitTestCase): - """Test base classical IO channel.""" - - def test_cannot_be_instantiated(self): - """Test base classical IO channel cannot be instantiated.""" - with self.assertRaises(NotImplementedError): - ClassicalIOChannel(0) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestMemorySlot(QiskitTestCase): - """MemorySlot tests.""" - - def test_default(self): - """Test default memory slot.""" - memory_slot = MemorySlot(123) - - self.assertEqual(memory_slot.index, 123) - self.assertEqual(memory_slot.name, "m123") - self.assertTrue(isinstance(memory_slot, ClassicalIOChannel)) - - def test_validation(self): - """Test channel validation""" - with self.assertRaises(PulseError): - MemorySlot(0.5) - with self.assertRaises(PulseError): - MemorySlot(-1) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestRegisterSlot(QiskitTestCase): - """RegisterSlot tests.""" - - def test_default(self): - """Test default register slot.""" - register_slot = RegisterSlot(123) - - self.assertEqual(register_slot.index, 123) - self.assertEqual(register_slot.name, "c123") - self.assertTrue(isinstance(register_slot, ClassicalIOChannel)) - - def test_validation(self): - """Test channel validation""" - with self.assertRaises(PulseError): - RegisterSlot(0.5) - with self.assertRaises(PulseError): - RegisterSlot(-1) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestSnapshotChannel(QiskitTestCase): - """SnapshotChannel tests.""" - - def test_default(self): - """Test default snapshot channel.""" - snapshot_channel = SnapshotChannel() - - self.assertEqual(snapshot_channel.index, 0) - self.assertEqual(snapshot_channel.name, "s0") - self.assertTrue(isinstance(snapshot_channel, ClassicalIOChannel)) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestDriveChannel(QiskitTestCase): - """DriveChannel tests.""" - - def test_default(self): - """Test default drive channel.""" - drive_channel = DriveChannel(123) - - self.assertEqual(drive_channel.index, 123) - self.assertEqual(drive_channel.name, "d123") - - def test_validation(self): - """Test channel validation""" - with self.assertRaises(PulseError): - DriveChannel(0.5) - with self.assertRaises(PulseError): - DriveChannel(-1) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestControlChannel(QiskitTestCase): - """ControlChannel tests.""" - - def test_default(self): - """Test default control channel.""" - control_channel = ControlChannel(123) - - self.assertEqual(control_channel.index, 123) - self.assertEqual(control_channel.name, "u123") - - def test_validation(self): - """Test channel validation""" - with self.assertRaises(PulseError): - ControlChannel(0.5) - with self.assertRaises(PulseError): - ControlChannel(-1) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestMeasureChannel(QiskitTestCase): - """MeasureChannel tests.""" - - def test_default(self): - """Test default measure channel.""" - measure_channel = MeasureChannel(123) - - self.assertEqual(measure_channel.index, 123) - self.assertEqual(measure_channel.name, "m123") - - def test_validation(self): - """Test channel validation""" - with self.assertRaises(PulseError): - MeasureChannel(0.5) - with self.assertRaises(PulseError): - MeasureChannel(-1) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/pulse/test_continuous_pulses.py b/test/python/pulse/test_continuous_pulses.py deleted file mode 100644 index affe949e7d9d..000000000000 --- a/test/python/pulse/test_continuous_pulses.py +++ /dev/null @@ -1,307 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# 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. - - -"""Tests continuous pulse functions.""" - -import numpy as np - -from qiskit.pulse.library import continuous -from test import QiskitTestCase # pylint: disable=wrong-import-order - - -class TestContinuousPulses(QiskitTestCase): - """Test continuous pulses.""" - - def test_constant(self): - """Test constant pulse.""" - amp = 0.5j - samples = 50 - times = np.linspace(0, 10, samples) - - constant_arr = continuous.constant(times, amp=amp) - - self.assertEqual(constant_arr.dtype, np.complex128) - np.testing.assert_equal(constant_arr, amp) - self.assertEqual(len(constant_arr), samples) - - def test_zero(self): - """Test constant pulse.""" - times = np.linspace(0, 10, 50) - zero_arr = continuous.zero(times) - - self.assertEqual(zero_arr.dtype, np.complex128) - np.testing.assert_equal(zero_arr, 0.0) - self.assertEqual(len(zero_arr), 50) - - def test_square(self): - """Test square wave.""" - amp = 0.5 - freq = 0.2 - samples = 100 - times = np.linspace(0, 10, samples) - square_arr = continuous.square(times, amp=amp, freq=freq) - # with new phase - square_arr_phased = continuous.square(times, amp=amp, freq=freq, phase=np.pi / 2) - - self.assertEqual(square_arr.dtype, np.complex128) - - self.assertAlmostEqual(square_arr[0], amp) - # test constant - self.assertAlmostEqual(square_arr[1] - square_arr[0], 0.0) - self.assertAlmostEqual(square_arr[25], -amp) - self.assertAlmostEqual(square_arr_phased[0], -amp) - # Assert bounded between -amp and amp - self.assertTrue(np.all((-amp <= square_arr) & (square_arr <= amp))) - self.assertEqual(len(square_arr), samples) - - def test_sawtooth(self): - """Test sawtooth wave.""" - amp = 0.5 - freq = 0.2 - samples = 101 - times, dt = np.linspace(0, 10, samples, retstep=True) - sawtooth_arr = continuous.sawtooth(times, amp=amp, freq=freq) - # with new phase - sawtooth_arr_phased = continuous.sawtooth(times, amp=amp, freq=freq, phase=np.pi / 2) - - self.assertEqual(sawtooth_arr.dtype, np.complex128) - - self.assertAlmostEqual(sawtooth_arr[0], 0.0) - # test slope - self.assertAlmostEqual((sawtooth_arr[1] - sawtooth_arr[0]) / dt, 2 * amp * freq) - self.assertAlmostEqual(sawtooth_arr[24], 0.48) - self.assertAlmostEqual(sawtooth_arr[50], 0.0) - self.assertAlmostEqual(sawtooth_arr[75], -amp) - self.assertAlmostEqual(sawtooth_arr_phased[0], -amp) - # Assert bounded between -amp and amp - self.assertTrue(np.all((-amp <= sawtooth_arr) & (sawtooth_arr <= amp))) - self.assertEqual(len(sawtooth_arr), samples) - - def test_triangle(self): - """Test triangle wave.""" - amp = 0.5 - freq = 0.2 - samples = 101 - times, dt = np.linspace(0, 10, samples, retstep=True) - triangle_arr = continuous.triangle(times, amp=amp, freq=freq) - # with new phase - triangle_arr_phased = continuous.triangle(times, amp=amp, freq=freq, phase=np.pi / 2) - - self.assertEqual(triangle_arr.dtype, np.complex128) - - self.assertAlmostEqual(triangle_arr[0], 0.0) - # test slope - self.assertAlmostEqual((triangle_arr[1] - triangle_arr[0]) / dt, 4 * amp * freq) - self.assertAlmostEqual(triangle_arr[12], 0.48) - self.assertAlmostEqual(triangle_arr[13], 0.48) - self.assertAlmostEqual(triangle_arr[50], 0.0) - self.assertAlmostEqual(triangle_arr_phased[0], amp) - # Assert bounded between -amp and amp - self.assertTrue(np.all((-amp <= triangle_arr) & (triangle_arr <= amp))) - self.assertEqual(len(triangle_arr), samples) - - def test_cos(self): - """Test cosine wave.""" - amp = 0.5 - period = 5 - freq = 1 / period - samples = 101 - times = np.linspace(0, 10, samples) - cos_arr = continuous.cos(times, amp=amp, freq=freq) - # with new phase - cos_arr_phased = continuous.cos(times, amp=amp, freq=freq, phase=np.pi / 2) - - self.assertEqual(cos_arr.dtype, np.complex128) - - # Assert starts at 1 - self.assertAlmostEqual(cos_arr[0], amp) - self.assertAlmostEqual(cos_arr[6], 0.3644, places=2) - self.assertAlmostEqual(cos_arr[25], -amp) - self.assertAlmostEqual(cos_arr[50], amp) - self.assertAlmostEqual(cos_arr_phased[0], 0.0) - # Assert bounded between -amp and amp - self.assertTrue(np.all((-amp <= cos_arr) & (cos_arr <= amp))) - self.assertEqual(len(cos_arr), samples) - - def test_sin(self): - """Test sine wave.""" - amp = 0.5 - period = 5 - freq = 1 / period - samples = 101 - times = np.linspace(0, 10, samples) - sin_arr = continuous.sin(times, amp=amp, freq=freq) - # with new phase - sin_arr_phased = continuous.sin(times, amp=0.5, freq=1 / 5, phase=np.pi / 2) - - self.assertEqual(sin_arr.dtype, np.complex128) - - # Assert starts at 1 - self.assertAlmostEqual(sin_arr[0], 0.0) - self.assertAlmostEqual(sin_arr[6], 0.3427, places=2) - self.assertAlmostEqual(sin_arr[25], 0.0) - self.assertAlmostEqual(sin_arr[13], amp, places=2) - self.assertAlmostEqual(sin_arr_phased[0], amp) - # Assert bounded between -amp and amp - self.assertTrue(np.all((-amp <= sin_arr) & (sin_arr <= amp))) - self.assertEqual(len(sin_arr), samples) - - def test_gaussian(self): - """Test gaussian pulse.""" - amp = 0.5 - duration = 20 - center = duration / 2 - sigma = 2 - times, dt = np.linspace(0, duration, 1001, retstep=True) - gaussian_arr = continuous.gaussian(times, amp, center, sigma) - gaussian_arr_zeroed = continuous.gaussian( - np.array([-1, center, duration + 1]), - amp, - center, - sigma, - zeroed_width=2 * (center + 1), - rescale_amp=True, - ) - - self.assertEqual(gaussian_arr.dtype, np.complex128) - - center_time = np.argmax(gaussian_arr) - self.assertAlmostEqual(times[center_time], center) - self.assertAlmostEqual(gaussian_arr[center_time], amp) - self.assertAlmostEqual(gaussian_arr_zeroed[0], 0.0, places=6) - self.assertAlmostEqual(gaussian_arr_zeroed[1], amp) - self.assertAlmostEqual(gaussian_arr_zeroed[2], 0.0, places=6) - self.assertAlmostEqual( - np.sum(gaussian_arr * dt), amp * np.sqrt(2 * np.pi * sigma**2), places=3 - ) - - def test_gaussian_deriv(self): - """Test gaussian derivative pulse.""" - amp = 0.5 - center = 10 - sigma = 2 - times, dt = np.linspace(0, 20, 1000, retstep=True) - deriv_prefactor = -(sigma**2) / (times - center) - - gaussian_deriv_arr = continuous.gaussian_deriv(times, amp, center, sigma) - gaussian_arr = gaussian_deriv_arr * deriv_prefactor - - self.assertEqual(gaussian_deriv_arr.dtype, np.complex128) - - self.assertAlmostEqual( - continuous.gaussian_deriv(np.array([0]), amp, center, sigma)[0], 0, places=5 - ) - self.assertAlmostEqual( - np.sum(gaussian_arr * dt), amp * np.sqrt(2 * np.pi * sigma**2), places=3 - ) - - def test_sech(self): - """Test sech pulse.""" - amp = 0.5 - duration = 40 - center = duration / 2 - sigma = 2 - times, dt = np.linspace(0, duration, 1001, retstep=True) - sech_arr = continuous.sech(times, amp, center, sigma) - sech_arr_zeroed = continuous.sech(np.array([-1, center, duration + 1]), amp, center, sigma) - - self.assertEqual(sech_arr.dtype, np.complex128) - - center_time = np.argmax(sech_arr) - self.assertAlmostEqual(times[center_time], center) - self.assertAlmostEqual(sech_arr[center_time], amp) - self.assertAlmostEqual(sech_arr_zeroed[0], 0.0, places=2) - self.assertAlmostEqual(sech_arr_zeroed[1], amp) - self.assertAlmostEqual(sech_arr_zeroed[2], 0.0, places=2) - self.assertAlmostEqual(np.sum(sech_arr * dt), amp * np.pi * sigma, places=3) - - def test_sech_deriv(self): - """Test sech derivative pulse.""" - amp = 0.5 - center = 20 - sigma = 2 - times = np.linspace(0, 40, 1000) - - sech_deriv_arr = continuous.sech_deriv(times, amp, center, sigma) - - self.assertEqual(sech_deriv_arr.dtype, np.complex128) - - self.assertAlmostEqual( - continuous.sech_deriv(np.array([0]), amp, center, sigma)[0], 0, places=3 - ) - - def test_gaussian_square(self): - """Test gaussian square pulse.""" - amp = 0.5 - center = 10 - width = 2 - sigma = 0.1 - times, dt = np.linspace(0, 20, 2001, retstep=True) - gaussian_square_arr = continuous.gaussian_square(times, amp, center, width, sigma) - - self.assertEqual(gaussian_square_arr.dtype, np.complex128) - - self.assertEqual(gaussian_square_arr[1000], amp) - # test half gaussian rise/fall - self.assertAlmostEqual( - np.sum(gaussian_square_arr[:900] * dt) * 2, - amp * np.sqrt(2 * np.pi * sigma**2), - places=2, - ) - self.assertAlmostEqual( - np.sum(gaussian_square_arr[1100:] * dt) * 2, - amp * np.sqrt(2 * np.pi * sigma**2), - places=2, - ) - # test for continuity at gaussian/square boundaries - gauss_rise_end_time = center - width / 2 - gauss_fall_start_time = center + width / 2 - epsilon = 0.01 - rise_times, dt_rise = np.linspace( - gauss_rise_end_time - epsilon, gauss_rise_end_time + epsilon, 1001, retstep=True - ) - fall_times, dt_fall = np.linspace( - gauss_fall_start_time - epsilon, gauss_fall_start_time + epsilon, 1001, retstep=True - ) - gaussian_square_rise_arr = continuous.gaussian_square(rise_times, amp, center, width, sigma) - gaussian_square_fall_arr = continuous.gaussian_square(fall_times, amp, center, width, sigma) - - # should be locally approximated by amp*dt^2/(2*sigma^2) - self.assertAlmostEqual( - amp * dt_rise**2 / (2 * sigma**2), - gaussian_square_rise_arr[500] - gaussian_square_rise_arr[499], - ) - self.assertAlmostEqual( - amp * dt_fall**2 / (2 * sigma**2), - gaussian_square_fall_arr[501] - gaussian_square_fall_arr[500], - ) - - def test_drag(self): - """Test drag pulse.""" - amp = 0.5 - center = 10 - sigma = 0.1 - beta = 0 - times = np.linspace(0, 20, 2001) - # test that we recover gaussian for beta=0 - gaussian_arr = continuous.gaussian( - times, amp, center, sigma, zeroed_width=2 * (center + 1), rescale_amp=True - ) - - drag_arr = continuous.drag( - times, amp, center, sigma, beta=beta, zeroed_width=2 * (center + 1), rescale_amp=True - ) - - self.assertEqual(drag_arr.dtype, np.complex128) - - np.testing.assert_equal(drag_arr, gaussian_arr) diff --git a/test/python/pulse/test_experiment_configurations.py b/test/python/pulse/test_experiment_configurations.py deleted file mode 100644 index 8702ba2d1cf5..000000000000 --- a/test/python/pulse/test_experiment_configurations.py +++ /dev/null @@ -1,206 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test cases for the experimental conditions for pulse.""" -import unittest -import numpy as np - -from qiskit.pulse.channels import DriveChannel, MeasureChannel, AcquireChannel -from qiskit.pulse.exceptions import PulseError -from qiskit.pulse import LoConfig, LoRange, Kernel, Discriminator -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings - - -class TestLoRange(QiskitTestCase): - """Test LO LoRange.""" - - def test_properties_includes_and_eq(self): - """Test creation of LoRange. Test upper/lower bounds and includes. - Test __eq__ for two same and different LoRange's. - """ - lo_range_1 = LoRange(lower_bound=-0.1, upper_bound=+0.1) - - self.assertEqual(lo_range_1.lower_bound, -0.1) - self.assertEqual(lo_range_1.upper_bound, +0.1) - self.assertTrue(lo_range_1.includes(0.0)) - - lo_range_2 = LoRange(lower_bound=-0.1, upper_bound=+0.1) - lo_range_3 = LoRange(lower_bound=-0.2, upper_bound=+0.2) - - self.assertTrue(lo_range_1 == lo_range_2) - self.assertFalse(lo_range_1 == lo_range_3) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestLoConfig(QiskitTestCase): - """LoConfig tests.""" - - def test_can_create_empty_user_lo_config(self): - """Test if a LoConfig can be created without no arguments.""" - user_lo_config = LoConfig() - self.assertEqual({}, user_lo_config.qubit_los) - self.assertEqual({}, user_lo_config.meas_los) - - def test_can_create_valid_user_lo_config(self): - """Test if a LoConfig can be created with valid user_los.""" - channel1 = DriveChannel(0) - channel2 = MeasureChannel(0) - user_lo_config = LoConfig({channel1: 1.4, channel2: 3.6}) - self.assertEqual(1.4, user_lo_config.qubit_los[channel1]) - self.assertEqual(3.6, user_lo_config.meas_los[channel2]) - - def test_fail_to_create_with_out_of_range_user_lo(self): - """Test if a LoConfig cannot be created with invalid user_los.""" - channel = DriveChannel(0) - with self.assertRaises(PulseError): - LoConfig({channel: 3.3}, {channel: (1.0, 2.0)}) - - def test_fail_to_create_with_invalid_channel(self): - """Test if a LoConfig cannot be created with invalid channel.""" - channel = AcquireChannel(0) - with self.assertRaises(PulseError): - LoConfig({channel: 1.0}) - - def test_keep_dict_unchanged_after_updating_the_dict_used_in_construction(self): - """Test if a LoConfig keeps its dictionary unchanged even after - the dictionary used in construction is updated. - """ - channel = DriveChannel(0) - original = {channel: 3.4} - user_lo_config = LoConfig(original) - self.assertEqual(3.4, user_lo_config.qubit_los[channel]) - original[channel] = 5.6 - self.assertEqual(3.4, user_lo_config.qubit_los[channel]) - - def test_get_channel_lo(self): - """Test retrieving channel lo from LO config.""" - channel = DriveChannel(0) - lo_config = LoConfig({channel: 1.0}) - self.assertEqual(lo_config.channel_lo(channel), 1.0) - - channel = MeasureChannel(0) - lo_config = LoConfig({channel: 2.0}) - self.assertEqual(lo_config.channel_lo(channel), 2.0) - - with self.assertRaises(PulseError): - lo_config.channel_lo(MeasureChannel(1)) - - -class TestKernel(QiskitTestCase): - """Test Kernel.""" - - def test_eq(self): - """Test if two kernels are equal.""" - kernel_a = Kernel( - "kernel_test", - kernel={"real": np.zeros(10), "imag": np.zeros(10)}, - bias=[0, 0], - ) - kernel_b = Kernel( - "kernel_test", - kernel={"real": np.zeros(10), "imag": np.zeros(10)}, - bias=[0, 0], - ) - self.assertTrue(kernel_a == kernel_b) - - def test_neq_name(self): - """Test if two kernels with different names are not equal.""" - kernel_a = Kernel( - "kernel_test", - kernel={"real": np.zeros(10), "imag": np.zeros(10)}, - bias=[0, 0], - ) - kernel_b = Kernel( - "kernel_test_2", - kernel={"real": np.zeros(10), "imag": np.zeros(10)}, - bias=[0, 0], - ) - self.assertFalse(kernel_a == kernel_b) - - def test_neq_params(self): - """Test if two kernels with different parameters are not equal.""" - kernel_a = Kernel( - "kernel_test", - kernel={"real": np.zeros(10), "imag": np.zeros(10)}, - bias=[0, 0], - ) - kernel_b = Kernel( - "kernel_test", - kernel={"real": np.zeros(10), "imag": np.zeros(10)}, - bias=[1, 0], - ) - self.assertFalse(kernel_a == kernel_b) - - def test_neq_nested_params(self): - """Test if two kernels with different nested parameters are not equal.""" - kernel_a = Kernel( - "kernel_test", - kernel={"real": np.zeros(10), "imag": np.zeros(10)}, - bias=[0, 0], - ) - kernel_b = Kernel( - "kernel_test", - kernel={"real": np.ones(10), "imag": np.zeros(10)}, - bias=[0, 0], - ) - self.assertFalse(kernel_a == kernel_b) - - -class TestDiscriminator(QiskitTestCase): - """Test Discriminator.""" - - def test_eq(self): - """Test if two discriminators are equal.""" - discriminator_a = Discriminator( - "discriminator_test", - discriminator_type="linear", - params=[1, 0], - ) - discriminator_b = Discriminator( - "discriminator_test", - discriminator_type="linear", - params=[1, 0], - ) - self.assertTrue(discriminator_a == discriminator_b) - - def test_neq_name(self): - """Test if two discriminators with different names are not equal.""" - discriminator_a = Discriminator( - "discriminator_test", - discriminator_type="linear", - params=[1, 0], - ) - discriminator_b = Discriminator( - "discriminator_test_2", - discriminator_type="linear", - params=[1, 0], - ) - self.assertFalse(discriminator_a == discriminator_b) - - def test_neq_params(self): - """Test if two discriminators with different parameters are not equal.""" - discriminator_a = Discriminator( - "discriminator_test", - discriminator_type="linear", - params=[1, 0], - ) - discriminator_b = Discriminator( - "discriminator_test", - discriminator_type="non-linear", - params=[0, 0], - ) - self.assertFalse(discriminator_a == discriminator_b) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/pulse/test_instructions.py b/test/python/pulse/test_instructions.py deleted file mode 100644 index 1eaf0f928499..000000000000 --- a/test/python/pulse/test_instructions.py +++ /dev/null @@ -1,336 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Unit tests for pulse instructions.""" - -import numpy as np - -from qiskit import circuit -from qiskit.pulse import channels, configuration, instructions, library, exceptions -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestAcquire(QiskitTestCase): - """Acquisition tests.""" - - def test_can_construct_valid_acquire_command(self): - """Test if valid acquire command can be constructed.""" - kernel_opts = {"start_window": 0, "stop_window": 10} - kernel = configuration.Kernel(name="boxcar", **kernel_opts) - - discriminator_opts = { - "neighborhoods": [{"qubits": 1, "channels": 1}], - "cal": "coloring", - "resample": False, - } - discriminator = configuration.Discriminator( - name="linear_discriminator", **discriminator_opts - ) - - acq = instructions.Acquire( - 10, - channels.AcquireChannel(0), - channels.MemorySlot(0), - kernel=kernel, - discriminator=discriminator, - name="acquire", - ) - - self.assertEqual(acq.duration, 10) - self.assertEqual(acq.discriminator.name, "linear_discriminator") - self.assertEqual(acq.discriminator.params, discriminator_opts) - self.assertEqual(acq.kernel.name, "boxcar") - self.assertEqual(acq.kernel.params, kernel_opts) - self.assertIsInstance(acq.id, int) - self.assertEqual(acq.name, "acquire") - self.assertEqual( - acq.operands, - ( - 10, - channels.AcquireChannel(0), - channels.MemorySlot(0), - None, - kernel, - discriminator, - ), - ) - - def test_instructions_hash(self): - """Test hashing for acquire instruction.""" - acq_1 = instructions.Acquire( - 10, - channels.AcquireChannel(0), - channels.MemorySlot(0), - name="acquire", - ) - acq_2 = instructions.Acquire( - 10, - channels.AcquireChannel(0), - channels.MemorySlot(0), - name="acquire", - ) - - hash_1 = hash(acq_1) - hash_2 = hash(acq_2) - - self.assertEqual(hash_1, hash_2) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestDelay(QiskitTestCase): - """Delay tests.""" - - def test_delay(self): - """Test delay.""" - delay = instructions.Delay(10, channels.DriveChannel(0), name="test_name") - - self.assertIsInstance(delay.id, int) - self.assertEqual(delay.name, "test_name") - self.assertEqual(delay.duration, 10) - self.assertIsInstance(delay.duration, int) - self.assertEqual(delay.operands, (10, channels.DriveChannel(0))) - self.assertEqual(delay, instructions.Delay(10, channels.DriveChannel(0))) - self.assertNotEqual(delay, instructions.Delay(11, channels.DriveChannel(1))) - self.assertEqual(repr(delay), "Delay(10, DriveChannel(0), name='test_name')") - - # Test numpy int for duration - delay = instructions.Delay(np.int32(10), channels.DriveChannel(0), name="test_name2") - self.assertEqual(delay.duration, 10) - self.assertIsInstance(delay.duration, np.integer) - - def test_operator_delay(self): - """Test Operator(delay).""" - from qiskit.circuit import QuantumCircuit - from qiskit.quantum_info import Operator - - circ = QuantumCircuit(1) - circ.delay(10) - op_delay = Operator(circ) - - expected = QuantumCircuit(1) - expected.id(0) - op_identity = Operator(expected) - self.assertEqual(op_delay, op_identity) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestSetFrequency(QiskitTestCase): - """Set frequency tests.""" - - def test_freq(self): - """Test set frequency basic functionality.""" - set_freq = instructions.SetFrequency(4.5e9, channels.DriveChannel(1), name="test") - - self.assertIsInstance(set_freq.id, int) - self.assertEqual(set_freq.duration, 0) - self.assertEqual(set_freq.frequency, 4.5e9) - self.assertEqual(set_freq.operands, (4.5e9, channels.DriveChannel(1))) - self.assertEqual( - set_freq, instructions.SetFrequency(4.5e9, channels.DriveChannel(1), name="test") - ) - self.assertNotEqual( - set_freq, instructions.SetFrequency(4.5e8, channels.DriveChannel(1), name="test") - ) - self.assertEqual(repr(set_freq), "SetFrequency(4500000000.0, DriveChannel(1), name='test')") - - def test_freq_non_pulse_channel(self): - """Test set frequency constructor with illegal channel""" - with self.assertRaises(exceptions.PulseError): - instructions.SetFrequency(4.5e9, channels.RegisterSlot(1), name="test") - - def test_parameter_expression(self): - """Test getting all parameters assigned by expression.""" - p1 = circuit.Parameter("P1") - p2 = circuit.Parameter("P2") - expr = p1 + p2 - - instr = instructions.SetFrequency(expr, channel=channels.DriveChannel(0)) - self.assertSetEqual(instr.parameters, {p1, p2}) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestShiftFrequency(QiskitTestCase): - """Shift frequency tests.""" - - def test_shift_freq(self): - """Test shift frequency basic functionality.""" - shift_freq = instructions.ShiftFrequency(4.5e9, channels.DriveChannel(1), name="test") - - self.assertIsInstance(shift_freq.id, int) - self.assertEqual(shift_freq.duration, 0) - self.assertEqual(shift_freq.frequency, 4.5e9) - self.assertEqual(shift_freq.operands, (4.5e9, channels.DriveChannel(1))) - self.assertEqual( - shift_freq, instructions.ShiftFrequency(4.5e9, channels.DriveChannel(1), name="test") - ) - self.assertNotEqual( - shift_freq, instructions.ShiftFrequency(4.5e8, channels.DriveChannel(1), name="test") - ) - self.assertEqual( - repr(shift_freq), "ShiftFrequency(4500000000.0, DriveChannel(1), name='test')" - ) - - def test_freq_non_pulse_channel(self): - """Test shift frequency constructor with illegal channel""" - with self.assertRaises(exceptions.PulseError): - instructions.ShiftFrequency(4.5e9, channels.RegisterSlot(1), name="test") - - def test_parameter_expression(self): - """Test getting all parameters assigned by expression.""" - p1 = circuit.Parameter("P1") - p2 = circuit.Parameter("P2") - expr = p1 + p2 - - instr = instructions.ShiftFrequency(expr, channel=channels.DriveChannel(0)) - self.assertSetEqual(instr.parameters, {p1, p2}) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestSetPhase(QiskitTestCase): - """Test the instruction construction.""" - - def test_default(self): - """Test basic SetPhase.""" - set_phase = instructions.SetPhase(1.57, channels.DriveChannel(0)) - - self.assertIsInstance(set_phase.id, int) - self.assertEqual(set_phase.name, None) - self.assertEqual(set_phase.duration, 0) - self.assertEqual(set_phase.phase, 1.57) - self.assertEqual(set_phase.operands, (1.57, channels.DriveChannel(0))) - self.assertEqual( - set_phase, instructions.SetPhase(1.57, channels.DriveChannel(0), name="test") - ) - self.assertNotEqual( - set_phase, instructions.SetPhase(1.57j, channels.DriveChannel(0), name="test") - ) - self.assertEqual(repr(set_phase), "SetPhase(1.57, DriveChannel(0))") - - def test_set_phase_non_pulse_channel(self): - """Test shift phase constructor with illegal channel""" - with self.assertRaises(exceptions.PulseError): - instructions.SetPhase(1.57, channels.RegisterSlot(1), name="test") - - def test_parameter_expression(self): - """Test getting all parameters assigned by expression.""" - p1 = circuit.Parameter("P1") - p2 = circuit.Parameter("P2") - expr = p1 + p2 - - instr = instructions.SetPhase(expr, channel=channels.DriveChannel(0)) - self.assertSetEqual(instr.parameters, {p1, p2}) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestShiftPhase(QiskitTestCase): - """Test the instruction construction.""" - - def test_default(self): - """Test basic ShiftPhase.""" - shift_phase = instructions.ShiftPhase(1.57, channels.DriveChannel(0)) - - self.assertIsInstance(shift_phase.id, int) - self.assertEqual(shift_phase.name, None) - self.assertEqual(shift_phase.duration, 0) - self.assertEqual(shift_phase.phase, 1.57) - self.assertEqual(shift_phase.operands, (1.57, channels.DriveChannel(0))) - self.assertEqual( - shift_phase, instructions.ShiftPhase(1.57, channels.DriveChannel(0), name="test") - ) - self.assertNotEqual( - shift_phase, instructions.ShiftPhase(1.57j, channels.DriveChannel(0), name="test") - ) - self.assertEqual(repr(shift_phase), "ShiftPhase(1.57, DriveChannel(0))") - - def test_shift_phase_non_pulse_channel(self): - """Test shift phase constructor with illegal channel""" - with self.assertRaises(exceptions.PulseError): - instructions.ShiftPhase(1.57, channels.RegisterSlot(1), name="test") - - def test_parameter_expression(self): - """Test getting all parameters assigned by expression.""" - p1 = circuit.Parameter("P1") - p2 = circuit.Parameter("P2") - expr = p1 + p2 - - instr = instructions.ShiftPhase(expr, channel=channels.DriveChannel(0)) - self.assertSetEqual(instr.parameters, {p1, p2}) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestSnapshot(QiskitTestCase): - """Snapshot tests.""" - - def test_default(self): - """Test default snapshot.""" - snapshot = instructions.Snapshot(label="test_name", snapshot_type="state") - - self.assertIsInstance(snapshot.id, int) - self.assertEqual(snapshot.name, "test_name") - self.assertEqual(snapshot.type, "state") - self.assertEqual(snapshot.duration, 0) - self.assertNotEqual(snapshot, instructions.Delay(10, channels.DriveChannel(0))) - self.assertEqual(repr(snapshot), "Snapshot(test_name, state, name='test_name')") - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestPlay(QiskitTestCase): - """Play tests.""" - - @ignore_pulse_deprecation_warnings - def setUp(self): - """Setup play tests.""" - super().setUp() - self.duration = 4 - self.pulse_op = library.Waveform([1.0] * self.duration, name="test") - - def test_play(self): - """Test basic play instruction.""" - play = instructions.Play(self.pulse_op, channels.DriveChannel(1)) - - self.assertIsInstance(play.id, int) - self.assertEqual(play.name, self.pulse_op.name) - self.assertEqual(play.duration, self.duration) - self.assertEqual( - repr(play), - "Play(Waveform(array([1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j]), name='test')," - " DriveChannel(1), name='test')", - ) - - def test_play_non_pulse_ch_raises(self): - """Test that play instruction on non-pulse channel raises a pulse error.""" - with self.assertRaises(exceptions.PulseError): - instructions.Play(self.pulse_op, channels.AcquireChannel(0)) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestDirectives(QiskitTestCase): - """Test pulse directives.""" - - def test_relative_barrier(self): - """Test the relative barrier directive.""" - a0 = channels.AcquireChannel(0) - d0 = channels.DriveChannel(0) - m0 = channels.MeasureChannel(0) - u0 = channels.ControlChannel(0) - mem0 = channels.MemorySlot(0) - reg0 = channels.RegisterSlot(0) - chans = (a0, d0, m0, u0, mem0, reg0) - name = "barrier" - barrier = instructions.RelativeBarrier(*chans, name=name) - - self.assertEqual(barrier.name, name) - self.assertEqual(barrier.duration, 0) - self.assertEqual(barrier.channels, chans) - self.assertEqual(barrier.operands, chans) diff --git a/test/python/pulse/test_parameter_manager.py b/test/python/pulse/test_parameter_manager.py deleted file mode 100644 index 32c0e5a9d907..000000000000 --- a/test/python/pulse/test_parameter_manager.py +++ /dev/null @@ -1,716 +0,0 @@ -# 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. - -# pylint: disable=invalid-name - -"""Test cases for parameter manager.""" - -from copy import deepcopy -from unittest.mock import patch - -import ddt -import numpy as np - -from qiskit import pulse -from qiskit.circuit import Parameter, ParameterVector -from qiskit.pulse.exceptions import PulseError, UnassignedDurationError -from qiskit.pulse.parameter_manager import ParameterGetter, ParameterSetter -from qiskit.pulse.transforms import AlignEquispaced, AlignLeft, inline_subroutines -from qiskit.pulse.utils import format_parameter_value -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class ParameterTestBase(QiskitTestCase): - """A base class for parameter manager unittest, providing test schedule.""" - - @ignore_pulse_deprecation_warnings - def setUp(self): - """Just some useful, reusable Parameters, constants, schedules.""" - super().setUp() - - self.amp1_1 = Parameter("amp1_1") - self.amp1_2 = Parameter("amp1_2") - self.amp2 = Parameter("amp2") - self.amp3 = Parameter("amp3") - - self.dur1 = Parameter("dur1") - self.dur2 = Parameter("dur2") - self.dur3 = Parameter("dur3") - - self.parametric_waveform1 = pulse.Gaussian( - duration=self.dur1, amp=self.amp1_1 + self.amp1_2, sigma=self.dur1 / 4 - ) - - self.parametric_waveform2 = pulse.Gaussian( - duration=self.dur2, amp=self.amp2, sigma=self.dur2 / 5 - ) - - self.parametric_waveform3 = pulse.Gaussian( - duration=self.dur3, amp=self.amp3, sigma=self.dur3 / 6 - ) - - self.ch1 = Parameter("ch1") - self.ch2 = Parameter("ch2") - self.ch3 = Parameter("ch3") - - self.d1 = pulse.DriveChannel(self.ch1) - self.d2 = pulse.DriveChannel(self.ch2) - self.d3 = pulse.DriveChannel(self.ch3) - - self.phi1 = Parameter("phi1") - self.phi2 = Parameter("phi2") - self.phi3 = Parameter("phi3") - - self.meas_dur = Parameter("meas_dur") - self.mem1 = Parameter("s1") - self.reg1 = Parameter("m1") - - self.context_dur = Parameter("context_dur") - - # schedule under test - subroutine = pulse.ScheduleBlock(alignment_context=AlignLeft()) - subroutine += pulse.ShiftPhase(self.phi1, self.d1) - subroutine += pulse.Play(self.parametric_waveform1, self.d1) - - long_schedule = pulse.ScheduleBlock( - alignment_context=AlignEquispaced(self.context_dur), name="long_schedule" - ) - - long_schedule += subroutine - long_schedule += pulse.ShiftPhase(self.phi2, self.d2) - long_schedule += pulse.Play(self.parametric_waveform2, self.d2) - long_schedule += pulse.ShiftPhase(self.phi3, self.d3) - long_schedule += pulse.Play(self.parametric_waveform3, self.d3) - - long_schedule += pulse.Acquire( - self.meas_dur, - pulse.AcquireChannel(self.ch1), - mem_slot=pulse.MemorySlot(self.mem1), - reg_slot=pulse.RegisterSlot(self.reg1), - ) - - self.test_sched = long_schedule - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestParameterGetter(ParameterTestBase): - """Test getting parameters.""" - - def test_get_parameter_from_channel(self): - """Test get parameters from channel.""" - test_obj = pulse.DriveChannel(self.ch1 + self.ch2) - - visitor = ParameterGetter() - visitor.visit(test_obj) - - ref_params = {self.ch1, self.ch2} - - self.assertSetEqual(visitor.parameters, ref_params) - - def test_get_parameter_from_pulse(self): - """Test get parameters from pulse instruction.""" - test_obj = self.parametric_waveform1 - - visitor = ParameterGetter() - visitor.visit(test_obj) - - ref_params = {self.amp1_1, self.amp1_2, self.dur1} - - self.assertSetEqual(visitor.parameters, ref_params) - - def test_get_parameter_from_acquire(self): - """Test get parameters from acquire instruction.""" - test_obj = pulse.Acquire(16000, pulse.AcquireChannel(self.ch1), pulse.MemorySlot(self.ch1)) - - visitor = ParameterGetter() - visitor.visit(test_obj) - - ref_params = {self.ch1} - - self.assertSetEqual(visitor.parameters, ref_params) - - def test_get_parameter_from_inst(self): - """Test get parameters from instruction.""" - test_obj = pulse.ShiftPhase(self.phi1 + self.phi2, pulse.DriveChannel(0)) - - visitor = ParameterGetter() - visitor.visit(test_obj) - - ref_params = {self.phi1, self.phi2} - - self.assertSetEqual(visitor.parameters, ref_params) - - def test_with_function(self): - """Test ParameterExpressions formed trivially in a function.""" - - def get_shift(variable): - return variable - 1 - - test_obj = pulse.ShiftPhase(get_shift(self.phi1), self.d1) - - visitor = ParameterGetter() - visitor.visit(test_obj) - - ref_params = {self.phi1, self.ch1} - - self.assertSetEqual(visitor.parameters, ref_params) - - def test_get_parameter_from_alignment_context(self): - """Test get parameters from alignment context.""" - test_obj = AlignEquispaced(duration=self.context_dur + self.dur1) - - visitor = ParameterGetter() - visitor.visit(test_obj) - - ref_params = {self.context_dur, self.dur1} - - self.assertSetEqual(visitor.parameters, ref_params) - - def test_get_parameter_from_complex_schedule(self): - """Test get parameters from complicated schedule.""" - test_block = deepcopy(self.test_sched) - - visitor = ParameterGetter() - visitor.visit(test_block) - - self.assertEqual(len(visitor.parameters), 17) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestParameterSetter(ParameterTestBase): - """Test setting parameters.""" - - def test_set_parameter_to_channel(self): - """Test set parameters from channel.""" - test_obj = pulse.DriveChannel(self.ch1 + self.ch2) - - value_dict = {self.ch1: 1, self.ch2: 2} - - visitor = ParameterSetter(param_map=value_dict) - assigned = visitor.visit(test_obj) - - ref_obj = pulse.DriveChannel(3) - - self.assertEqual(assigned, ref_obj) - - def test_set_parameter_to_pulse(self): - """Test set parameters from pulse instruction.""" - test_obj = self.parametric_waveform1 - - value_dict = {self.amp1_1: 0.1, self.amp1_2: 0.2, self.dur1: 160} - - visitor = ParameterSetter(param_map=value_dict) - assigned = visitor.visit(test_obj) - - ref_obj = pulse.Gaussian(duration=160, amp=0.3, sigma=40) - - self.assertEqual(assigned, ref_obj) - - def test_set_parameter_to_acquire(self): - """Test set parameters to acquire instruction.""" - test_obj = pulse.Acquire(16000, pulse.AcquireChannel(self.ch1), pulse.MemorySlot(self.ch1)) - - value_dict = {self.ch1: 2} - - visitor = ParameterSetter(param_map=value_dict) - assigned = visitor.visit(test_obj) - - ref_obj = pulse.Acquire(16000, pulse.AcquireChannel(2), pulse.MemorySlot(2)) - - self.assertEqual(assigned, ref_obj) - - def test_set_parameter_to_inst(self): - """Test get parameters from instruction.""" - test_obj = pulse.ShiftPhase(self.phi1 + self.phi2, pulse.DriveChannel(0)) - - value_dict = {self.phi1: 0.123, self.phi2: 0.456} - - visitor = ParameterSetter(param_map=value_dict) - assigned = visitor.visit(test_obj) - - ref_obj = pulse.ShiftPhase(0.579, pulse.DriveChannel(0)) - - self.assertEqual(assigned, ref_obj) - - def test_with_function(self): - """Test ParameterExpressions formed trivially in a function.""" - - def get_shift(variable): - return variable - 1 - - test_obj = pulse.ShiftPhase(get_shift(self.phi1), self.d1) - - value_dict = {self.phi1: 2.0, self.ch1: 2} - - visitor = ParameterSetter(param_map=value_dict) - assigned = visitor.visit(test_obj) - - ref_obj = pulse.ShiftPhase(1.0, pulse.DriveChannel(2)) - - self.assertEqual(assigned, ref_obj) - - def test_set_parameter_to_alignment_context(self): - """Test get parameters from alignment context.""" - test_obj = AlignEquispaced(duration=self.context_dur + self.dur1) - - value_dict = {self.context_dur: 1000, self.dur1: 100} - - visitor = ParameterSetter(param_map=value_dict) - assigned = visitor.visit(test_obj) - - ref_obj = AlignEquispaced(duration=1100) - - self.assertEqual(assigned, ref_obj) - - def test_nested_assignment_partial_bind(self): - """Test nested schedule with call instruction. - Inline the schedule and partially bind parameters.""" - context = AlignEquispaced(duration=self.context_dur) - subroutine = pulse.ScheduleBlock(alignment_context=context) - subroutine += pulse.Play(self.parametric_waveform1, self.d1) - - nested_block = pulse.ScheduleBlock() - - nested_block += subroutine - - test_obj = pulse.ScheduleBlock() - test_obj += nested_block - - test_obj = inline_subroutines(test_obj) - - value_dict = {self.context_dur: 1000, self.dur1: 200, self.ch1: 1} - - visitor = ParameterSetter(param_map=value_dict) - assigned = visitor.visit(test_obj) - - ref_context = AlignEquispaced(duration=1000) - ref_subroutine = pulse.ScheduleBlock(alignment_context=ref_context) - ref_subroutine += pulse.Play( - pulse.Gaussian(200, self.amp1_1 + self.amp1_2, 50), pulse.DriveChannel(1) - ) - - ref_nested_block = pulse.ScheduleBlock() - ref_nested_block += ref_subroutine - - ref_obj = pulse.ScheduleBlock() - ref_obj += ref_nested_block - - self.assertEqual(assigned, ref_obj) - - def test_complex_valued_parameter(self): - """Test complex valued parameter can be casted to a complex value, - but raises PendingDeprecationWarning..""" - amp = Parameter("amp") - test_obj = pulse.Constant(duration=160, amp=1j * amp) - - value_dict = {amp: 0.1} - - visitor = ParameterSetter(param_map=value_dict) - with self.assertWarns(PendingDeprecationWarning): - assigned = visitor.visit(test_obj) - - self.assertEqual(assigned.amp, 0.1j) - - def test_complex_value_to_parameter(self): - """Test complex value can be assigned to parameter object, - but raises PendingDeprecationWarning.""" - amp = Parameter("amp") - test_obj = pulse.Constant(duration=160, amp=amp) - - value_dict = {amp: 0.1j} - - visitor = ParameterSetter(param_map=value_dict) - with self.assertWarns(PendingDeprecationWarning): - assigned = visitor.visit(test_obj) - - self.assertEqual(assigned.amp, 0.1j) - - def test_complex_parameter_expression(self): - """Test assignment of complex-valued parameter expression to parameter, - but raises PendingDeprecationWarning.""" - amp = Parameter("amp") - - mag = Parameter("A") - phi = Parameter("phi") - - test_obj = pulse.Constant(duration=160, amp=amp) - test_obj_copy = deepcopy(test_obj) - # generate parameter expression - value_dict = {amp: mag * np.exp(1j * phi)} - visitor = ParameterSetter(param_map=value_dict) - assigned = visitor.visit(test_obj) - - # generate complex value - value_dict = {mag: 0.1, phi: 0.5} - visitor = ParameterSetter(param_map=value_dict) - with self.assertWarns(PendingDeprecationWarning): - assigned = visitor.visit(assigned) - - # evaluated parameter expression: 0.0877582561890373 + 0.0479425538604203*I - value_dict = {amp: 0.1 * np.exp(0.5j)} - - visitor = ParameterSetter(param_map=value_dict) - with self.assertWarns(PendingDeprecationWarning): - ref_obj = visitor.visit(test_obj_copy) - self.assertEqual(assigned, ref_obj) - - def test_invalid_pulse_amplitude(self): - """Test that invalid parameters are still checked upon assignment.""" - amp = Parameter("amp") - - test_sched = pulse.ScheduleBlock() - test_sched.append( - pulse.Play( - pulse.Constant(160, amp=2 * amp), - pulse.DriveChannel(0), - ), - inplace=True, - ) - with self.assertRaises(PulseError): - test_sched.assign_parameters({amp: 0.6}, inplace=False) - - def test_disable_validation_parameter_assignment(self): - """Test that pulse validation can be disabled on the class level. - - Tests for representative examples. - """ - sig = Parameter("sigma") - test_sched = pulse.ScheduleBlock() - test_sched.append( - pulse.Play( - pulse.Gaussian(duration=100, amp=0.5, sigma=sig, angle=0.0), pulse.DriveChannel(0) - ), - inplace=True, - ) - with self.assertRaises(PulseError): - test_sched.assign_parameters({sig: -1.0}, inplace=False) - with patch( - "qiskit.pulse.library.symbolic_pulses.SymbolicPulse.disable_validation", new=True - ): - test_sched = pulse.ScheduleBlock() - test_sched.append( - pulse.Play( - pulse.Gaussian(duration=100, amp=0.5, sigma=sig, angle=0.0), - pulse.DriveChannel(0), - ), - inplace=True, - ) - binded_sched = test_sched.assign_parameters({sig: -1.0}, inplace=False) - self.assertLess(binded_sched.instructions[0][1].pulse.sigma, 0) - - def test_set_parameter_to_complex_schedule(self): - """Test get parameters from complicated schedule.""" - test_block = deepcopy(self.test_sched) - - value_dict = { - self.amp1_1: 0.1, - self.amp1_2: 0.2, - self.amp2: 0.3, - self.amp3: 0.4, - self.dur1: 100, - self.dur2: 125, - self.dur3: 150, - self.ch1: 0, - self.ch2: 2, - self.ch3: 4, - self.phi1: 1.0, - self.phi2: 2.0, - self.phi3: 3.0, - self.meas_dur: 300, - self.mem1: 3, - self.reg1: 0, - self.context_dur: 1000, - } - - visitor = ParameterSetter(param_map=value_dict) - assigned = visitor.visit(test_block) - - # create ref schedule - subroutine = pulse.ScheduleBlock(alignment_context=AlignLeft()) - subroutine += pulse.ShiftPhase(1.0, pulse.DriveChannel(0)) - subroutine += pulse.Play(pulse.Gaussian(100, 0.3, 25), pulse.DriveChannel(0)) - - ref_obj = pulse.ScheduleBlock(alignment_context=AlignEquispaced(1000), name="long_schedule") - - ref_obj += subroutine - ref_obj += pulse.ShiftPhase(2.0, pulse.DriveChannel(2)) - ref_obj += pulse.Play(pulse.Gaussian(125, 0.3, 25), pulse.DriveChannel(2)) - ref_obj += pulse.ShiftPhase(3.0, pulse.DriveChannel(4)) - ref_obj += pulse.Play(pulse.Gaussian(150, 0.4, 25), pulse.DriveChannel(4)) - - ref_obj += pulse.Acquire( - 300, pulse.AcquireChannel(0), pulse.MemorySlot(3), pulse.RegisterSlot(0) - ) - - self.assertEqual(assigned, ref_obj) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestAssignFromProgram(QiskitTestCase): - """Test managing parameters from programs. Parameter manager is implicitly called.""" - - def test_attribute_parameters(self): - """Test the ``parameter`` attributes.""" - sigma = Parameter("sigma") - amp = Parameter("amp") - - waveform = pulse.library.Gaussian(duration=128, sigma=sigma, amp=amp) - - block = pulse.ScheduleBlock() - block += pulse.Play(waveform, pulse.DriveChannel(10)) - - ref_set = {amp, sigma} - - self.assertSetEqual(set(block.parameters), ref_set) - - def test_parametric_pulses(self): - """Test Parametric Pulses with parameters determined by ParameterExpressions - in the Play instruction.""" - sigma = Parameter("sigma") - amp = Parameter("amp") - - waveform = pulse.library.Gaussian(duration=128, sigma=sigma, amp=amp) - - block = pulse.ScheduleBlock() - block += pulse.Play(waveform, pulse.DriveChannel(10)) - block.assign_parameters({amp: 0.2, sigma: 4}, inplace=True) - - self.assertEqual(block.blocks[0].pulse.amp, 0.2) - self.assertEqual(block.blocks[0].pulse.sigma, 4.0) - - def test_parametric_pulses_with_parameter_vector(self): - """Test Parametric Pulses with parameters determined by a ParameterVector - in the Play instruction.""" - param_vec = ParameterVector("param_vec", 3) - param = Parameter("param") - - waveform = pulse.library.Gaussian(duration=128, sigma=param_vec[0], amp=param_vec[1]) - - block = pulse.ScheduleBlock() - block += pulse.Play(waveform, pulse.DriveChannel(10)) - block += pulse.ShiftPhase(param_vec[2], pulse.DriveChannel(10)) - block1 = block.assign_parameters({param_vec: [4, 0.2, 0.1]}, inplace=False) - block2 = block.assign_parameters({param_vec: [4, param, 0.1]}, inplace=False) - self.assertEqual(block1.blocks[0].pulse.amp, 0.2) - self.assertEqual(block1.blocks[0].pulse.sigma, 4.0) - self.assertEqual(block1.blocks[1].phase, 0.1) - self.assertEqual(block2.blocks[0].pulse.amp, param) - self.assertEqual(block2.blocks[0].pulse.sigma, 4.0) - self.assertEqual(block2.blocks[1].phase, 0.1) - - sched = pulse.Schedule() - sched += pulse.Play(waveform, pulse.DriveChannel(10)) - sched += pulse.ShiftPhase(param_vec[2], pulse.DriveChannel(10)) - sched1 = sched.assign_parameters({param_vec: [4, 0.2, 0.1]}, inplace=False) - sched2 = sched.assign_parameters({param_vec: [4, param, 0.1]}, inplace=False) - self.assertEqual(sched1.instructions[0][1].pulse.amp, 0.2) - self.assertEqual(sched1.instructions[0][1].pulse.sigma, 4.0) - self.assertEqual(sched1.instructions[1][1].phase, 0.1) - self.assertEqual(sched2.instructions[0][1].pulse.amp, param) - self.assertEqual(sched2.instructions[0][1].pulse.sigma, 4.0) - self.assertEqual(sched2.instructions[1][1].phase, 0.1) - - def test_pulse_assignment_with_parameter_names(self): - """Test pulse assignment with parameter names.""" - sigma = Parameter("sigma") - amp = Parameter("amp") - param_vec = ParameterVector("param_vec", 2) - - waveform = pulse.library.Gaussian(duration=128, sigma=sigma, amp=amp) - waveform2 = pulse.library.Gaussian(duration=128, sigma=40, amp=amp) - block = pulse.ScheduleBlock() - block += pulse.Play(waveform, pulse.DriveChannel(10)) - block += pulse.Play(waveform2, pulse.DriveChannel(10)) - block += pulse.ShiftPhase(param_vec[0], pulse.DriveChannel(10)) - block += pulse.ShiftPhase(param_vec[1], pulse.DriveChannel(10)) - block1 = block.assign_parameters( - {"amp": 0.2, "sigma": 4, "param_vec": [3.14, 1.57]}, inplace=False - ) - - self.assertEqual(block1.blocks[0].pulse.amp, 0.2) - self.assertEqual(block1.blocks[0].pulse.sigma, 4.0) - self.assertEqual(block1.blocks[1].pulse.amp, 0.2) - self.assertEqual(block1.blocks[2].phase, 3.14) - self.assertEqual(block1.blocks[3].phase, 1.57) - - sched = pulse.Schedule() - sched += pulse.Play(waveform, pulse.DriveChannel(10)) - sched += pulse.Play(waveform2, pulse.DriveChannel(10)) - sched += pulse.ShiftPhase(param_vec[0], pulse.DriveChannel(10)) - sched += pulse.ShiftPhase(param_vec[1], pulse.DriveChannel(10)) - sched1 = sched.assign_parameters( - {"amp": 0.2, "sigma": 4, "param_vec": [3.14, 1.57]}, inplace=False - ) - - self.assertEqual(sched1.instructions[0][1].pulse.amp, 0.2) - self.assertEqual(sched1.instructions[0][1].pulse.sigma, 4.0) - self.assertEqual(sched1.instructions[1][1].pulse.amp, 0.2) - self.assertEqual(sched1.instructions[2][1].phase, 3.14) - self.assertEqual(sched1.instructions[3][1].phase, 1.57) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestScheduleTimeslots(QiskitTestCase): - """Test for edge cases of timing overlap on parametrized channels. - - Note that this test is dedicated to `Schedule` since `ScheduleBlock` implicitly - assigns instruction time t0 that doesn't overlap with existing instructions. - """ - - def test_overlapping_pulses(self): - """Test that an error is still raised when overlapping instructions are assigned.""" - param_idx = Parameter("q") - - schedule = pulse.Schedule() - schedule |= pulse.Play(pulse.Waveform([1, 1, 1, 1]), pulse.DriveChannel(param_idx)) - with self.assertRaises(PulseError): - schedule |= pulse.Play( - pulse.Waveform([0.5, 0.5, 0.5, 0.5]), pulse.DriveChannel(param_idx) - ) - - def test_overlapping_on_assignment(self): - """Test that assignment will catch against existing instructions.""" - param_idx = Parameter("q") - - schedule = pulse.Schedule() - schedule |= pulse.Play(pulse.Waveform([1, 1, 1, 1]), pulse.DriveChannel(1)) - schedule |= pulse.Play(pulse.Waveform([1, 1, 1, 1]), pulse.DriveChannel(param_idx)) - with self.assertRaises(PulseError): - schedule.assign_parameters({param_idx: 1}) - - def test_overlapping_on_expression_assigment_to_zero(self): - """Test constant*zero expression conflict.""" - param_idx = Parameter("q") - - schedule = pulse.Schedule() - schedule |= pulse.Play(pulse.Waveform([1, 1, 1, 1]), pulse.DriveChannel(param_idx)) - schedule |= pulse.Play(pulse.Waveform([1, 1, 1, 1]), pulse.DriveChannel(2 * param_idx)) - with self.assertRaises(PulseError): - schedule.assign_parameters({param_idx: 0}) - - def test_merging_upon_assignment(self): - """Test that schedule can match instructions on a channel.""" - param_idx = Parameter("q") - - schedule = pulse.Schedule() - schedule |= pulse.Play(pulse.Waveform([1, 1, 1, 1]), pulse.DriveChannel(1)) - schedule = schedule.insert( - 4, pulse.Play(pulse.Waveform([1, 1, 1, 1]), pulse.DriveChannel(param_idx)) - ) - schedule.assign_parameters({param_idx: 1}) - - self.assertEqual(schedule.ch_duration(pulse.DriveChannel(1)), 8) - self.assertEqual(schedule.channels, (pulse.DriveChannel(1),)) - - def test_overlapping_on_multiple_assignment(self): - """Test that assigning one qubit then another raises error when overlapping.""" - param_idx1 = Parameter("q1") - param_idx2 = Parameter("q2") - - schedule = pulse.Schedule() - schedule |= pulse.Play(pulse.Waveform([1, 1, 1, 1]), pulse.DriveChannel(param_idx1)) - schedule |= pulse.Play(pulse.Waveform([1, 1, 1, 1]), pulse.DriveChannel(param_idx2)) - schedule.assign_parameters({param_idx1: 2}) - - with self.assertRaises(PulseError): - schedule.assign_parameters({param_idx2: 2}) - - def test_cannot_build_schedule_with_unassigned_duration(self): - """Test we cannot build schedule with parameterized instructions""" - dur = Parameter("dur") - ch = pulse.DriveChannel(0) - - test_play = pulse.Play(pulse.Gaussian(dur, 0.1, dur / 4), ch) - - sched = pulse.Schedule() - with self.assertRaises(UnassignedDurationError): - sched.insert(0, test_play) - - -@ddt.ddt -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestFormatParameter(QiskitTestCase): - """Test format_parameter_value function.""" - - def test_format_unassigned(self): - """Format unassigned parameter expression.""" - p1 = Parameter("P1") - p2 = Parameter("P2") - expr = p1 + p2 - - self.assertEqual(format_parameter_value(expr), expr) - - def test_partly_unassigned(self): - """Format partly assigned parameter expression.""" - p1 = Parameter("P1") - p2 = Parameter("P2") - expr = (p1 + p2).assign(p1, 3.0) - - self.assertEqual(format_parameter_value(expr), expr) - - @ddt.data(1, 1.0, 1.00000000001, np.int64(1)) - def test_integer(self, value): - """Format integer parameter expression.""" - p1 = Parameter("P1") - expr = p1.assign(p1, value) - out = format_parameter_value(expr) - self.assertIsInstance(out, int) - self.assertEqual(out, 1) - - @ddt.data(1.2, np.float64(1.2)) - def test_float(self, value): - """Format float parameter expression.""" - p1 = Parameter("P1") - expr = p1.assign(p1, value) - out = format_parameter_value(expr) - self.assertIsInstance(out, float) - self.assertEqual(out, 1.2) - - @ddt.data(1.2 + 3.4j, np.complex128(1.2 + 3.4j)) - def test_complex(self, value): - """Format float parameter expression.""" - p1 = Parameter("P1") - expr = p1.assign(p1, value) - out = format_parameter_value(expr) - self.assertIsInstance(out, complex) - self.assertEqual(out, 1.2 + 3.4j) - - def test_complex_rounding_error(self): - """Format float parameter expression.""" - p1 = Parameter("P1") - expr = p1.assign(p1, 1.2 + 1j * 1e-20) - out = format_parameter_value(expr) - self.assertIsInstance(out, float) - self.assertEqual(out, 1.2) - - def test_builtin_float(self): - """Format float parameter expression.""" - expr = 1.23 - out = format_parameter_value(expr) - self.assertIsInstance(out, float) - self.assertEqual(out, 1.23) - - @ddt.data(15482812500000, 8465625000000, 4255312500000) - def test_edge_case(self, edge_case_val): - """Format integer parameter expression with - a particular integer number that causes rounding error at typecast.""" - - # Numbers to be tested here are chosen randomly. - # These numbers had caused mis-typecast into float before qiskit/#11972. - - p1 = Parameter("P1") - expr = p1.assign(p1, edge_case_val) - out = format_parameter_value(expr) - self.assertIsInstance(out, int) - self.assertEqual(out, edge_case_val) diff --git a/test/python/pulse/test_parser.py b/test/python/pulse/test_parser.py deleted file mode 100644 index d9a3f7dd9a7a..000000000000 --- a/test/python/pulse/test_parser.py +++ /dev/null @@ -1,234 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# 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. - -"""Parser Test.""" - -from qiskit.pulse.parser import parse_string_expr -from qiskit.pulse.exceptions import PulseError -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestInstructionToQobjConverter(QiskitTestCase): - """Expression parser test.""" - - def test_valid_expression1(self): - """Parsing valid expression.""" - - expr = "1+1*2*3.2+8*cos(0)**2" - parsed_expr = parse_string_expr(expr) - - self.assertEqual(parsed_expr.params, []) - self.assertEqual(parsed_expr(), 15.4 + 0j) - - def test_valid_expression2(self): - """Parsing valid expression.""" - - expr = "pi*2" - parsed_expr = parse_string_expr(expr) - - self.assertEqual(parsed_expr.params, []) - self.assertEqual(parsed_expr(), 6.283185307179586 + 0j) - - def test_valid_expression3(self): - """Parsing valid expression.""" - - expr = "-P1*cos(P2)" - parsed_expr = parse_string_expr(expr) - - self.assertEqual(parsed_expr.params, ["P1", "P2"]) - self.assertEqual(parsed_expr(P1=1.0, P2=2.0), 0.4161468365471424 + 0j) - - def test_valid_expression4(self): - """Parsing valid expression.""" - - expr = "-P1*P2*P3" - parsed_expr = parse_string_expr(expr) - - self.assertEqual(parsed_expr.params, ["P1", "P2", "P3"]) - self.assertEqual(parsed_expr(P1=1.0, P2=2.0, P3=3.0), -6.0 + 0j) - - def test_valid_expression5(self): - """Parsing valid expression.""" - - expr = "-(P1)" - parsed_expr = parse_string_expr(expr) - - self.assertEqual(parsed_expr.params, ["P1"]) - self.assertEqual(parsed_expr(P1=1.0), -1.0 + 0j) - - def test_valid_expression6(self): - """Parsing valid expression.""" - - expr = "-1.*P1" - parsed_expr = parse_string_expr(expr) - - self.assertEqual(parsed_expr.params, ["P1"]) - self.assertEqual(parsed_expr(P1=1.0), -1.0 + 0j) - - def test_valid_expression7(self): - """Parsing valid expression.""" - - expr = "-1.*P1*P2" - parsed_expr = parse_string_expr(expr) - - self.assertEqual(parsed_expr.params, ["P1", "P2"]) - self.assertEqual(parsed_expr(P1=1.0, P2=2.0), -2.0 + 0j) - - def test_valid_expression8(self): - """Parsing valid expression.""" - - expr = "P3-P2*(4+P1)" - parsed_expr = parse_string_expr(expr) - - self.assertEqual(parsed_expr.params, ["P1", "P2", "P3"]) - self.assertEqual(parsed_expr(P1=1, P2=2, P3=3), -7.0 + 0j) - - def test_invalid_expressions1(self): - """Parsing invalid expressions.""" - - expr = "2***2" - with self.assertRaises(PulseError): - parsed_expr = parse_string_expr(expr) - parsed_expr() - - def test_invalid_expressions2(self): - """Parsing invalid expressions.""" - - expr = "avdfd*3" - with self.assertRaises(PulseError): - parsed_expr = parse_string_expr(expr) - parsed_expr() - - def test_invalid_expressions3(self): - """Parsing invalid expressions.""" - - expr = "Cos(1+2)" - with self.assertRaises(PulseError): - parsed_expr = parse_string_expr(expr) - parsed_expr() - - def test_invalid_expressions4(self): - """Parsing invalid expressions.""" - - expr = "hello_world" - with self.assertRaises(PulseError): - parsed_expr = parse_string_expr(expr) - parsed_expr() - - def test_invalid_expressions5(self): - """Parsing invalid expressions.""" - - expr = "1.1.1.1" - with self.assertRaises(PulseError): - parsed_expr = parse_string_expr(expr) - parsed_expr() - - def test_invalid_expressions6(self): - """Parsing invalid expressions.""" - - expr = "abc.1" - with self.assertRaises(PulseError): - parsed_expr = parse_string_expr(expr) - parsed_expr() - - def test_malicious_expressions1(self): - """Parsing malicious expressions.""" - - expr = '__import__("sys").stdout.write("unsafe input.")' - with self.assertRaises(PulseError): - parsed_expr = parse_string_expr(expr) - parsed_expr() - - def test_malicious_expressions2(self): - """Parsing malicious expressions.""" - - expr = "INSERT INTO students VALUES (?,?)" - with self.assertRaises(PulseError): - parsed_expr = parse_string_expr(expr) - parsed_expr() - - def test_malicious_expressions3(self): - """Parsing malicious expressions.""" - - expr = "import math" - with self.assertRaises(PulseError): - parsed_expr = parse_string_expr(expr) - parsed_expr() - - def test_malicious_expressions4(self): - """Parsing malicious expressions.""" - - expr = "complex" - with self.assertRaises(PulseError): - parsed_expr = parse_string_expr(expr) - parsed_expr() - - def test_malicious_expressions5(self): - """Parsing malicious expressions.""" - - expr = "print(1.0)" - with self.assertRaises(PulseError): - parsed_expr = parse_string_expr(expr) - parsed_expr() - - def test_malicious_expressions6(self): - """Parsing malicious expressions.""" - - expr = 'eval("()._" + "_class_" + "_._" + "_bases_" + "_[0]")' - with self.assertRaises(PulseError): - parsed_expr = parse_string_expr(expr) - parsed_expr() - - def test_partial_binding(self): - """Test partial binding of parameters.""" - - expr = "P1 * P2 + P3 / P4 - P5" - - parsed_expr = parse_string_expr(expr, partial_binding=True) - self.assertEqual(parsed_expr.params, ["P1", "P2", "P3", "P4", "P5"]) - - bound_three = parsed_expr(P1=1, P2=2, P3=3) - self.assertEqual(bound_three.params, ["P4", "P5"]) - - self.assertEqual(bound_three(P4=4, P5=5), -2.25) - self.assertEqual(bound_three(4, 5), -2.25) - - bound_four = bound_three(P4=4) - self.assertEqual(bound_four.params, ["P5"]) - self.assertEqual(bound_four(P5=5), -2.25) - self.assertEqual(bound_four(5), -2.25) - - bound_four_new = bound_three(P4=40) - self.assertEqual(bound_four_new.params, ["P5"]) - self.assertEqual(bound_four_new(P5=5), -2.925) - self.assertEqual(bound_four_new(5), -2.925) - - def test_argument_duplication(self): - """Test duplication of *args and **kwargs.""" - - expr = "P1+P2" - parsed_expr = parse_string_expr(expr, partial_binding=True) - - with self.assertRaises(PulseError): - parsed_expr(1, P1=1) - - self.assertEqual(parsed_expr(1, P2=2), 3.0) - - def test_unexpected_argument(self): - """Test unexpected argument error.""" - expr = "P1+P2" - parsed_expr = parse_string_expr(expr, partial_binding=True) - - with self.assertRaises(PulseError): - parsed_expr(1, 2, P3=3) diff --git a/test/python/pulse/test_pulse_lib.py b/test/python/pulse/test_pulse_lib.py deleted file mode 100644 index fa60c2adb3e5..000000000000 --- a/test/python/pulse/test_pulse_lib.py +++ /dev/null @@ -1,899 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Unit tests for pulse waveforms.""" - -import unittest -from unittest.mock import patch -import numpy as np -import symengine as sym - -from qiskit.circuit import Parameter -from qiskit.pulse.library import ( - SymbolicPulse, - ScalableSymbolicPulse, - Waveform, - Constant, - Gaussian, - GaussianSquare, - GaussianSquareDrag, - gaussian_square_echo, - GaussianDeriv, - Drag, - Sin, - Cos, - Sawtooth, - Triangle, - Square, - Sech, - SechDeriv, -) -from qiskit.pulse import functional_pulse, PulseError -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestWaveform(QiskitTestCase): - """Waveform tests.""" - - def test_sample_pulse(self): - """Test pulse initialization.""" - n_samples = 100 - samples = np.linspace(0, 1.0, n_samples, dtype=np.complex128) - name = "test" - sample_pulse = Waveform(samples, name=name) - - self.assertEqual(sample_pulse.samples.dtype, np.complex128) - np.testing.assert_almost_equal(sample_pulse.samples, samples) - - self.assertEqual(sample_pulse.duration, n_samples) - self.assertEqual(sample_pulse.name, name) - - def test_waveform_hashing(self): - """Test waveform hashing.""" - n_samples = 100 - samples = np.linspace(0, 1.0, n_samples, dtype=np.complex128) - name = "test" - sample_pulse = Waveform(samples, name=name) - sample_pulse2 = Waveform(samples, name="test2") - - self.assertEqual({sample_pulse, sample_pulse2}, {sample_pulse}) - - def test_type_casting(self): - """Test casting of input samples to numpy array.""" - n_samples = 100 - samples_f64 = np.linspace(0, 1.0, n_samples, dtype=np.float64) - - sample_pulse_f64 = Waveform(samples_f64) - self.assertEqual(sample_pulse_f64.samples.dtype, np.complex128) - - samples_c64 = np.linspace(0, 1.0, n_samples, dtype=np.complex64) - - sample_pulse_c64 = Waveform(samples_c64) - self.assertEqual(sample_pulse_c64.samples.dtype, np.complex128) - - samples_list = np.linspace(0, 1.0, n_samples).tolist() - - sample_pulse_list = Waveform(samples_list) - self.assertEqual(sample_pulse_list.samples.dtype, np.complex128) - - def test_pulse_limits(self): - """Test that limits of pulse norm of one are enforced properly.""" - - # test norm is correct for complex128 numpy data - unit_pulse_c128 = np.exp(1j * 2 * np.pi * np.linspace(0, 1, 1000), dtype=np.complex128) - # test does not raise error - try: - Waveform(unit_pulse_c128) - except PulseError: - self.fail("Waveform incorrectly failed on approximately unit norm samples.") - - invalid_const = 1.1 - with self.assertRaises(PulseError): - Waveform(invalid_const * np.exp(1j * 2 * np.pi * np.linspace(0, 1, 1000))) - - with patch("qiskit.pulse.library.pulse.Pulse.limit_amplitude", new=False): - wave = Waveform(invalid_const * np.exp(1j * 2 * np.pi * np.linspace(0, 1, 1000))) - self.assertGreater(np.max(np.abs(wave.samples)), 1.0) - - # Test case where data is converted to python types with complex as a list - # with form [re, im] and back to a numpy array. - # This is how the transport layer handles samples in the qobj so it is important - # to test. - unit_pulse_c64 = np.exp(1j * 2 * np.pi * np.linspace(0, 1, 1000), dtype=np.complex64) - sample_components = np.stack( - np.transpose([np.real(unit_pulse_c64), np.imag(unit_pulse_c64)]) - ) - pulse_list = sample_components.tolist() - recombined_pulse = [sample[0] + sample[1] * 1j for sample in pulse_list] - - # test does not raise error - try: - Waveform(recombined_pulse) - except PulseError: - self.fail("Waveform incorrectly failed to approximately unit norm samples.") - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestSymbolicPulses(QiskitTestCase): - """Tests for all subclasses of SymbolicPulse.""" - - def test_construction(self): - """Test that symbolic pulses can be constructed without error.""" - Gaussian(duration=25, sigma=4, amp=0.5, angle=np.pi / 2) - GaussianSquare(duration=150, amp=0.2, sigma=8, width=140) - GaussianSquare(duration=150, amp=0.2, sigma=8, risefall_sigma_ratio=2.5) - Constant(duration=150, amp=0.5, angle=np.pi * 0.23) - Drag(duration=25, amp=0.6, sigma=7.8, beta=4, angle=np.pi * 0.54) - GaussianDeriv(duration=150, amp=0.2, sigma=8) - Sin(duration=25, amp=0.5, freq=0.1, phase=0.5, angle=0.5) - Cos(duration=30, amp=0.5, freq=0.1, phase=-0.5) - Sawtooth(duration=40, amp=0.5, freq=0.2, phase=3.14) - Triangle(duration=50, amp=0.5, freq=0.01, phase=0.5) - Square(duration=50, amp=0.5, freq=0.01, phase=0.5) - Sech(duration=50, amp=0.5, sigma=10) - Sech(duration=50, amp=0.5, sigma=10, zero_ends=False) - SechDeriv(duration=50, amp=0.5, sigma=10) - - def test_gauss_square_extremes(self): - """Test that the gaussian square pulse can build a gaussian.""" - duration = 125 - sigma = 4 - amp = 0.5 - angle = np.pi / 2 - gaus_square = GaussianSquare(duration=duration, sigma=sigma, amp=amp, width=0, angle=angle) - gaus = Gaussian(duration=duration, sigma=sigma, amp=amp, angle=angle) - np.testing.assert_almost_equal( - gaus_square.get_waveform().samples, gaus.get_waveform().samples - ) - gaus_square = GaussianSquare( - duration=duration, sigma=sigma, amp=amp, width=121, angle=angle - ) - const = Constant(duration=duration, amp=amp, angle=angle) - np.testing.assert_almost_equal( - gaus_square.get_waveform().samples[2:-2], const.get_waveform().samples[2:-2] - ) - - def test_gauss_square_passes_validation_after_construction(self): - """Test that parameter validation is consistent before and after construction. - - This previously used to raise an exception: see gh-7882.""" - pulse = GaussianSquare(duration=125, sigma=4, amp=0.5, width=100, angle=np.pi / 2) - pulse.validate_parameters() - - def test_gaussian_square_drag_pulse(self): - """Test that GaussianSquareDrag sample pulse matches expectations. - - Test that the real part of the envelop matches GaussianSquare and that - the rise and fall match Drag. - """ - risefall = 32 - sigma = 4 - amp = 0.5 - width = 100 - beta = 1 - duration = width + 2 * risefall - - gsd = GaussianSquareDrag(duration=duration, sigma=sigma, amp=amp, width=width, beta=beta) - gsd_samples = gsd.get_waveform().samples - - gs_pulse = GaussianSquare(duration=duration, sigma=sigma, amp=amp, width=width) - np.testing.assert_almost_equal( - np.real(gsd_samples), - np.real(gs_pulse.get_waveform().samples), - ) - gsd2 = GaussianSquareDrag( - duration=duration, - sigma=sigma, - amp=amp, - beta=beta, - risefall_sigma_ratio=risefall / sigma, - ) - np.testing.assert_almost_equal( - gsd_samples, - gsd2.get_waveform().samples, - ) - - drag_pulse = Drag(duration=2 * risefall, amp=amp, sigma=sigma, beta=beta) - np.testing.assert_almost_equal( - gsd_samples[:risefall], - drag_pulse.get_waveform().samples[:risefall], - ) - np.testing.assert_almost_equal( - gsd_samples[-risefall:], - drag_pulse.get_waveform().samples[-risefall:], - ) - - def test_gauss_square_drag_extreme(self): - """Test that the gaussian square drag pulse can build a drag pulse.""" - duration = 125 - sigma = 4 - amp = 0.5 - angle = 1.5 - beta = 1 - gsd = GaussianSquareDrag( - duration=duration, sigma=sigma, amp=amp, width=0, beta=beta, angle=angle - ) - drag = Drag(duration=duration, sigma=sigma, amp=amp, beta=beta, angle=angle) - np.testing.assert_almost_equal(gsd.get_waveform().samples, drag.get_waveform().samples) - - def test_gaussian_square_drag_validation(self): - """Test drag beta parameter validation.""" - - GaussianSquareDrag(duration=50, width=0, sigma=16, amp=1, beta=2) - GaussianSquareDrag(duration=50, width=0, sigma=16, amp=1, beta=4) - GaussianSquareDrag(duration=50, width=0, sigma=16, amp=0.5, beta=20) - GaussianSquareDrag(duration=50, width=0, sigma=16, amp=-1, beta=2) - GaussianSquareDrag(duration=50, width=0, sigma=16, amp=1, beta=-2) - GaussianSquareDrag(duration=50, width=0, sigma=16, amp=1, beta=6) - GaussianSquareDrag(duration=50, width=0, sigma=16, amp=-0.5, beta=25, angle=1.5) - with self.assertRaises(PulseError): - GaussianSquareDrag(duration=50, width=0, sigma=16, amp=1, beta=20) - with self.assertRaises(PulseError): - GaussianSquareDrag(duration=50, width=0, sigma=4, amp=0.8, beta=20) - with self.assertRaises(PulseError): - GaussianSquareDrag(duration=50, width=0, sigma=4, amp=0.8, beta=-20) - - def test_gaussian_square_echo_pulse(self): - """Test that gaussian_square_echo sample pulse matches expectations. - - Test that the real part of the envelop matches GaussianSquare with - given amplitude and phase active for half duration with another - GaussianSquare active for the other half duration with opposite - amplitude and a GaussianSquare active on the entire duration with - its own amplitude and phase - """ - risefall = 32 - sigma = 4 - amp = 0.5 - width = 100 - duration = width + 2 * risefall - active_amp = 0.1 - width_echo = (duration - 2 * (duration - width)) / 2 - - gse = gaussian_square_echo( - duration=duration, sigma=sigma, amp=amp, width=width, active_amp=active_amp - ) - gse_samples = gse.get_waveform().samples - - gs_echo_pulse_pos = GaussianSquare( - duration=duration / 2, sigma=sigma, amp=amp, width=width_echo - ) - gs_echo_pulse_neg = GaussianSquare( - duration=duration / 2, sigma=sigma, amp=-amp, width=width_echo - ) - gs_active_pulse = GaussianSquare( - duration=duration, sigma=sigma, amp=active_amp, width=width - ) - gs_echo_pulse_pos_samples = np.array( - gs_echo_pulse_pos.get_waveform().samples.tolist() + [0] * int(duration / 2) - ) - gs_echo_pulse_neg_samples = np.array( - [0] * int(duration / 2) + gs_echo_pulse_neg.get_waveform().samples.tolist() - ) - gs_active_pulse_samples = gs_active_pulse.get_waveform().samples - - np.testing.assert_almost_equal( - gse_samples, - gs_echo_pulse_pos_samples + gs_echo_pulse_neg_samples + gs_active_pulse_samples, - ) - - def test_gaussian_square_echo_active_amp_validation(self): - """Test gaussian square echo active amp parameter validation.""" - - gaussian_square_echo(duration=50, width=0, sigma=16, amp=0.1, active_amp=0.2) - gaussian_square_echo(duration=50, width=0, sigma=16, amp=0.1, active_amp=0.4) - gaussian_square_echo(duration=50, width=0, sigma=16, amp=0.5, active_amp=0.3) - gaussian_square_echo(duration=50, width=0, sigma=16, amp=-0.1, active_amp=0.2) - gaussian_square_echo(duration=50, width=0, sigma=16, amp=0.1, active_amp=-0.2) - gaussian_square_echo(duration=50, width=0, sigma=16, amp=0.1, active_amp=0.6) - gaussian_square_echo(duration=50, width=0, sigma=16, amp=-0.5, angle=1.5, active_amp=0.25) - with self.assertRaises(PulseError): - gaussian_square_echo(duration=50, width=0, sigma=16, amp=0.1, active_amp=1.1) - with self.assertRaises(PulseError): - gaussian_square_echo(duration=50, width=0, sigma=4, amp=-0.8, active_amp=-0.3) - - def test_drag_validation(self): - """Test drag parameter validation, specifically the beta validation.""" - duration = 25 - sigma = 4 - amp = 0.5 - angle = np.pi / 2 - beta = 1 - wf = Drag(duration=duration, sigma=sigma, amp=amp, beta=beta, angle=angle) - samples = wf.get_waveform().samples - self.assertTrue(max(np.abs(samples)) <= 1) - with self.assertRaises(PulseError): - wf = Drag(duration=duration, sigma=sigma, amp=1.2, beta=beta) - beta = sigma**2 - with self.assertRaises(PulseError): - wf = Drag(duration=duration, sigma=sigma, amp=amp, beta=beta, angle=angle) - # If sigma is high enough, side peaks fall out of range and norm restriction is met - sigma = 100 - wf = Drag(duration=duration, sigma=sigma, amp=amp, beta=beta, angle=angle) - - def test_drag_beta_validation(self): - """Test drag beta parameter validation.""" - - def check_drag(duration, sigma, amp, beta, angle=0): - wf = Drag(duration=duration, sigma=sigma, amp=amp, beta=beta, angle=angle) - samples = wf.get_waveform().samples - self.assertTrue(max(np.abs(samples)) <= 1) - - check_drag(duration=50, sigma=16, amp=1, beta=2) - check_drag(duration=50, sigma=16, amp=1, beta=4) - check_drag(duration=50, sigma=16, amp=0.5, beta=20) - check_drag(duration=50, sigma=16, amp=-1, beta=2) - check_drag(duration=50, sigma=16, amp=1, beta=-2) - check_drag(duration=50, sigma=16, amp=1, beta=6) - check_drag(duration=50, sigma=16, amp=0.5, beta=25, angle=-np.pi / 2) - with self.assertRaises(PulseError): - check_drag(duration=50, sigma=16, amp=1, beta=20) - with self.assertRaises(PulseError): - check_drag(duration=50, sigma=4, amp=0.8, beta=20) - with self.assertRaises(PulseError): - check_drag(duration=50, sigma=4, amp=0.8, beta=-20) - - def test_sin_pulse(self): - """Test that Sin creation""" - duration = 100 - amp = 0.5 - freq = 0.1 - phase = 0 - - Sin(duration=duration, amp=amp, freq=freq, phase=phase) - - with self.assertRaises(PulseError): - Sin(duration=duration, amp=amp, freq=5, phase=phase) - - def test_cos_pulse(self): - """Test that Cos creation""" - duration = 100 - amp = 0.5 - freq = 0.1 - phase = 0 - cos_pulse = Cos(duration=duration, amp=amp, freq=freq, phase=phase) - - shifted_sin_pulse = Sin(duration=duration, amp=amp, freq=freq, phase=phase + np.pi / 2) - np.testing.assert_almost_equal( - shifted_sin_pulse.get_waveform().samples, cos_pulse.get_waveform().samples - ) - with self.assertRaises(PulseError): - Cos(duration=duration, amp=amp, freq=5, phase=phase) - - def test_square_pulse(self): - """Test that Square pulse creation""" - duration = 100 - amp = 0.5 - freq = 0.1 - phase = 0.3 - Square(duration=duration, amp=amp, freq=freq, phase=phase) - - with self.assertRaises(PulseError): - Square(duration=duration, amp=amp, freq=5, phase=phase) - - def test_sawtooth_pulse(self): - """Test that Sawtooth pulse creation""" - duration = 100 - amp = 0.5 - freq = 0.1 - phase = 0.5 - sawtooth_pulse = Sawtooth(duration=duration, amp=amp, freq=freq, phase=phase) - - sawtooth_pulse_2 = Sawtooth(duration=duration, amp=amp, freq=freq, phase=phase + 2 * np.pi) - np.testing.assert_almost_equal( - sawtooth_pulse.get_waveform().samples, sawtooth_pulse_2.get_waveform().samples - ) - - with self.assertRaises(PulseError): - Sawtooth(duration=duration, amp=amp, freq=5, phase=phase) - - def test_triangle_pulse(self): - """Test that Triangle pulse creation""" - duration = 100 - amp = 0.5 - freq = 0.1 - phase = 0.5 - triangle_pulse = Triangle(duration=duration, amp=amp, freq=freq, phase=phase) - - triangle_pulse_2 = Triangle(duration=duration, amp=amp, freq=freq, phase=phase + 2 * np.pi) - np.testing.assert_almost_equal( - triangle_pulse.get_waveform().samples, triangle_pulse_2.get_waveform().samples - ) - - with self.assertRaises(PulseError): - Triangle(duration=duration, amp=amp, freq=5, phase=phase) - - def test_gaussian_deriv_pulse(self): - """Test that GaussianDeriv pulse creation""" - duration = 300 - amp = 0.5 - sigma = 100 - GaussianDeriv(duration=duration, amp=amp, sigma=sigma) - - with self.assertRaises(PulseError): - Sech(duration=duration, amp=amp, sigma=0) - - def test_sech_pulse(self): - """Test that Sech pulse creation""" - duration = 100 - amp = 0.5 - sigma = 10 - # Zero ends = True - Sech(duration=duration, amp=amp, sigma=sigma) - - # Zero ends = False - Sech(duration=duration, amp=amp, sigma=sigma, zero_ends=False) - - with self.assertRaises(PulseError): - Sech(duration=duration, amp=amp, sigma=-5) - - def test_sech_deriv_pulse(self): - """Test that SechDeriv pulse creation""" - duration = 100 - amp = 0.5 - sigma = 10 - SechDeriv(duration=duration, amp=amp, sigma=sigma) - - with self.assertRaises(PulseError): - SechDeriv(duration=duration, amp=amp, sigma=-5) - - def test_constant_samples(self): - """Test the constant pulse and its sampled construction.""" - amp = 0.6 - angle = np.pi * 0.7 - const = Constant(duration=150, amp=amp, angle=angle) - self.assertEqual(const.get_waveform().samples[0], amp * np.exp(1j * angle)) - self.assertEqual(len(const.get_waveform().samples), 150) - - def test_parameters(self): - """Test that the parameters can be extracted as a dict through the `parameters` - attribute.""" - drag = Drag(duration=25, amp=0.2, sigma=7.8, beta=4, angle=0.2) - self.assertEqual(set(drag.parameters.keys()), {"duration", "amp", "sigma", "beta", "angle"}) - const = Constant(duration=150, amp=1) - self.assertEqual(set(const.parameters.keys()), {"duration", "amp", "angle"}) - - def test_repr(self): - """Test the repr methods for symbolic pulses.""" - gaus = Gaussian(duration=25, amp=0.7, sigma=4, angle=0.3) - self.assertEqual(repr(gaus), "Gaussian(duration=25, sigma=4, amp=0.7, angle=0.3)") - gaus_square = GaussianSquare(duration=20, sigma=30, amp=1.0, width=3) - self.assertEqual( - repr(gaus_square), "GaussianSquare(duration=20, sigma=30, width=3, amp=1.0, angle=0.0)" - ) - gaus_square = GaussianSquare( - duration=20, sigma=30, amp=1.0, angle=0.2, risefall_sigma_ratio=0.1 - ) - self.assertEqual( - repr(gaus_square), - "GaussianSquare(duration=20, sigma=30, width=14.0, amp=1.0, angle=0.2)", - ) - gsd = GaussianSquareDrag(duration=20, sigma=30, amp=1.0, width=3, beta=1) - self.assertEqual( - repr(gsd), - "GaussianSquareDrag(duration=20, sigma=30, width=3, beta=1, amp=1.0, angle=0.0)", - ) - gsd = GaussianSquareDrag(duration=20, sigma=30, amp=1.0, risefall_sigma_ratio=0.1, beta=1) - self.assertEqual( - repr(gsd), - "GaussianSquareDrag(duration=20, sigma=30, width=14.0, beta=1, amp=1.0, angle=0.0)", - ) - gse = gaussian_square_echo(duration=20, sigma=30, amp=1.0, width=3) - self.assertEqual( - repr(gse), - ( - "gaussian_square_echo(duration=20, amp=1.0, angle=0.0, sigma=30, width=3," - " active_amp=0.0, active_angle=0.0)" - ), - ) - gse = gaussian_square_echo(duration=20, sigma=30, amp=1.0, risefall_sigma_ratio=0.1) - self.assertEqual( - repr(gse), - ( - "gaussian_square_echo(duration=20, amp=1.0, angle=0.0, sigma=30, width=14.0," - " active_amp=0.0, active_angle=0.0)" - ), - ) - drag = Drag(duration=5, amp=0.5, sigma=7, beta=1) - self.assertEqual(repr(drag), "Drag(duration=5, sigma=7, beta=1, amp=0.5, angle=0.0)") - const = Constant(duration=150, amp=0.1, angle=0.3) - self.assertEqual(repr(const), "Constant(duration=150, amp=0.1, angle=0.3)") - sin_pulse = Sin(duration=150, amp=0.1, angle=0.3, freq=0.2, phase=0) - self.assertEqual( - repr(sin_pulse), "Sin(duration=150, freq=0.2, phase=0, amp=0.1, angle=0.3)" - ) - cos_pulse = Cos(duration=150, amp=0.1, angle=0.3, freq=0.2, phase=0) - self.assertEqual( - repr(cos_pulse), "Cos(duration=150, freq=0.2, phase=0, amp=0.1, angle=0.3)" - ) - triangle_pulse = Triangle(duration=150, amp=0.1, angle=0.3, freq=0.2, phase=0) - self.assertEqual( - repr(triangle_pulse), "Triangle(duration=150, freq=0.2, phase=0, amp=0.1, angle=0.3)" - ) - sawtooth_pulse = Sawtooth(duration=150, amp=0.1, angle=0.3, freq=0.2, phase=0) - self.assertEqual( - repr(sawtooth_pulse), "Sawtooth(duration=150, freq=0.2, phase=0, amp=0.1, angle=0.3)" - ) - sech_pulse = Sech(duration=150, amp=0.1, angle=0.3, sigma=10) - self.assertEqual(repr(sech_pulse), "Sech(duration=150, sigma=10, amp=0.1, angle=0.3)") - sech_deriv_pulse = SechDeriv(duration=150, amp=0.1, angle=0.3, sigma=10) - self.assertEqual( - repr(sech_deriv_pulse), "SechDeriv(duration=150, sigma=10, amp=0.1, angle=0.3)" - ) - gaussian_deriv_pulse = GaussianDeriv(duration=150, amp=0.1, angle=0.3, sigma=10) - self.assertEqual( - repr(gaussian_deriv_pulse), "GaussianDeriv(duration=150, sigma=10, amp=0.1, angle=0.3)" - ) - - def test_param_validation(self): - """Test that symbolic pulse parameters are validated when initialized.""" - with self.assertRaises(PulseError): - Gaussian(duration=25, sigma=0, amp=0.5, angle=np.pi / 2) - with self.assertRaises(PulseError): - GaussianSquare(duration=150, amp=0.2, sigma=8) - with self.assertRaises(PulseError): - GaussianSquare(duration=150, amp=0.2, sigma=8, width=100, risefall_sigma_ratio=5) - with self.assertRaises(PulseError): - GaussianSquare(duration=150, amp=0.2, sigma=8, width=160) - with self.assertRaises(PulseError): - GaussianSquare(duration=150, amp=0.2, sigma=8, risefall_sigma_ratio=10) - - with self.assertRaises(PulseError): - GaussianSquareDrag(duration=150, amp=0.2, sigma=8, beta=1) - with self.assertRaises(PulseError): - GaussianSquareDrag(duration=150, amp=0.2, sigma=8, width=160, beta=1) - with self.assertRaises(PulseError): - GaussianSquareDrag(duration=150, amp=0.2, sigma=8, risefall_sigma_ratio=10, beta=1) - - with self.assertRaises(PulseError): - gaussian_square_echo( - duration=150, - amp=0.2, - sigma=8, - ) - with self.assertRaises(PulseError): - gaussian_square_echo(duration=150, amp=0.2, sigma=8, width=160) - with self.assertRaises(PulseError): - gaussian_square_echo(duration=150, amp=0.2, sigma=8, risefall_sigma_ratio=10) - - with self.assertRaises(PulseError): - Constant(duration=150, amp=1.5, angle=np.pi * 0.8) - with self.assertRaises(PulseError): - Drag(duration=25, amp=0.5, sigma=-7.8, beta=4, angle=np.pi / 3) - - def test_class_level_limit_amplitude(self): - """Test that the check for amplitude less than or equal to 1 can - be disabled on the class level. - - Tests for representative examples. - """ - with self.assertRaises(PulseError): - Gaussian(duration=100, sigma=1.0, amp=1.7, angle=np.pi * 1.1) - - with patch("qiskit.pulse.library.pulse.Pulse.limit_amplitude", new=False): - waveform = Gaussian(duration=100, sigma=1.0, amp=1.7, angle=np.pi * 1.1) - self.assertGreater(np.abs(waveform.amp), 1.0) - waveform = GaussianSquare(duration=100, sigma=1.0, amp=1.5, width=10, angle=np.pi / 5) - self.assertGreater(np.abs(waveform.amp), 1.0) - waveform = GaussianSquareDrag(duration=100, sigma=1.0, amp=1.1, beta=0.1, width=10) - self.assertGreater(np.abs(waveform.amp), 1.0) - - def test_class_level_disable_validation(self): - """Test that pulse validation can be disabled on the class level. - - Tests for representative examples. - """ - with self.assertRaises(PulseError): - Gaussian(duration=100, sigma=-1.0, amp=0.5, angle=np.pi * 1.1) - - with patch( - "qiskit.pulse.library.symbolic_pulses.SymbolicPulse.disable_validation", new=True - ): - waveform = Gaussian(duration=100, sigma=-1.0, amp=0.5, angle=np.pi * 1.1) - self.assertLess(waveform.sigma, 0) - waveform = GaussianSquare(duration=100, sigma=1.0, amp=0.5, width=1000, angle=np.pi / 5) - self.assertGreater(waveform.width, waveform.duration) - waveform = GaussianSquareDrag(duration=100, sigma=1.0, amp=1.1, beta=0.1, width=-1) - self.assertLess(waveform.width, 0) - - def test_gaussian_limit_amplitude_per_instance(self): - """Test limit amplitude option per Gaussian instance.""" - with self.assertRaises(PulseError): - Gaussian(duration=100, sigma=1.0, amp=1.6, angle=np.pi / 2.5) - - waveform = Gaussian( - duration=100, sigma=1.0, amp=1.6, angle=np.pi / 2.5, limit_amplitude=False - ) - self.assertGreater(np.abs(waveform.amp), 1.0) - - def test_gaussian_square_limit_amplitude_per_instance(self): - """Test limit amplitude option per GaussianSquare instance.""" - with self.assertRaises(PulseError): - GaussianSquare(duration=100, sigma=1.0, amp=1.5, width=10, angle=np.pi / 3) - - waveform = GaussianSquare( - duration=100, sigma=1.0, amp=1.5, width=10, angle=np.pi / 3, limit_amplitude=False - ) - self.assertGreater(np.abs(waveform.amp), 1.0) - - def test_gaussian_square_drag_limit_amplitude_per_instance(self): - """Test limit amplitude option per GaussianSquareDrag instance.""" - with self.assertRaises(PulseError): - GaussianSquareDrag(duration=100, sigma=1.0, amp=1.1, beta=0.1, width=10) - - waveform = GaussianSquareDrag( - duration=100, sigma=1.0, amp=1.1, beta=0.1, width=10, limit_amplitude=False - ) - self.assertGreater(np.abs(waveform.amp), 1.0) - - def test_gaussian_square_echo_limit_amplitude_per_instance(self): - """Test limit amplitude option per GaussianSquareEcho instance.""" - with self.assertRaises(PulseError): - gaussian_square_echo(duration=1000, sigma=4.0, amp=1.01, width=100) - - waveform = gaussian_square_echo( - duration=1000, sigma=4.0, amp=1.01, width=100, limit_amplitude=False - ) - self.assertGreater(np.abs(waveform.amp), 1.0) - - def test_drag_limit_amplitude_per_instance(self): - """Test limit amplitude option per DRAG instance.""" - with self.assertRaises(PulseError): - Drag(duration=100, sigma=1.0, beta=1.0, amp=1.8, angle=np.pi * 0.3) - - waveform = Drag( - duration=100, sigma=1.0, beta=1.0, amp=1.8, angle=np.pi * 0.3, limit_amplitude=False - ) - self.assertGreater(np.abs(waveform.amp), 1.0) - - def test_constant_limit_amplitude_per_instance(self): - """Test limit amplitude option per Constant instance.""" - with self.assertRaises(PulseError): - Constant(duration=100, amp=1.6, angle=0.5) - - waveform = Constant(duration=100, amp=1.6, angle=0.5, limit_amplitude=False) - self.assertGreater(np.abs(waveform.amp), 1.0) - - def test_sin_limit_amplitude_per_instance(self): - """Test limit amplitude option per Sin instance.""" - with self.assertRaises(PulseError): - Sin(duration=100, amp=1.1, phase=0) - - waveform = Sin(duration=100, amp=1.1, phase=0, limit_amplitude=False) - self.assertGreater(np.abs(waveform.amp), 1.0) - - def test_sawtooth_limit_amplitude_per_instance(self): - """Test limit amplitude option per Sawtooth instance.""" - with self.assertRaises(PulseError): - Sawtooth(duration=100, amp=1.1, phase=0) - - waveform = Sawtooth(duration=100, amp=1.1, phase=0, limit_amplitude=False) - self.assertGreater(np.abs(waveform.amp), 1.0) - - def test_triangle_limit_amplitude_per_instance(self): - """Test limit amplitude option per Triangle instance.""" - with self.assertRaises(PulseError): - Triangle(duration=100, amp=1.1, phase=0) - - waveform = Triangle(duration=100, amp=1.1, phase=0, limit_amplitude=False) - self.assertGreater(np.abs(waveform.amp), 1.0) - - def test_square_limit_amplitude_per_instance(self): - """Test limit amplitude option per Square instance.""" - with self.assertRaises(PulseError): - Square(duration=100, amp=1.1, phase=0) - - waveform = Square(duration=100, amp=1.1, phase=0, limit_amplitude=False) - self.assertGreater(np.abs(waveform.amp), 1.0) - - def test_gaussian_deriv_limit_amplitude_per_instance(self): - """Test limit amplitude option per GaussianDeriv instance.""" - with self.assertRaises(PulseError): - GaussianDeriv(duration=100, amp=5, sigma=1) - - waveform = GaussianDeriv(duration=100, amp=5, sigma=1, limit_amplitude=False) - self.assertGreater(np.abs(waveform.amp / waveform.sigma), np.exp(0.5)) - - def test_sech_limit_amplitude_per_instance(self): - """Test limit amplitude option per Sech instance.""" - with self.assertRaises(PulseError): - Sech(duration=100, amp=5, sigma=1) - - waveform = Sech(duration=100, amp=5, sigma=1, limit_amplitude=False) - self.assertGreater(np.abs(waveform.amp), 1.0) - - def test_sech_deriv_limit_amplitude_per_instance(self): - """Test limit amplitude option per SechDeriv instance.""" - with self.assertRaises(PulseError): - SechDeriv(duration=100, amp=5, sigma=1) - - waveform = SechDeriv(duration=100, amp=5, sigma=1, limit_amplitude=False) - self.assertGreater(np.abs(waveform.amp) / waveform.sigma, 2.0) - - def test_get_parameters(self): - """Test getting pulse parameters as attribute.""" - drag_pulse = Drag(duration=100, amp=0.1, sigma=40, beta=3) - self.assertEqual(drag_pulse.duration, 100) - self.assertEqual(drag_pulse.amp, 0.1) - self.assertEqual(drag_pulse.sigma, 40) - self.assertEqual(drag_pulse.beta, 3) - - with self.assertRaises(AttributeError): - _ = drag_pulse.non_existing_parameter - - def test_envelope_cache(self): - """Test speed up of instantiation with lambdify envelope cache.""" - drag_instance1 = Drag(duration=100, amp=0.1, sigma=40, beta=3) - drag_instance2 = Drag(duration=100, amp=0.1, sigma=40, beta=3) - self.assertTrue(drag_instance1._envelope_lam is drag_instance2._envelope_lam) - - def test_constraints_cache(self): - """Test speed up of instantiation with lambdify constraints cache.""" - drag_instance1 = Drag(duration=100, amp=0.1, sigma=40, beta=3) - drag_instance2 = Drag(duration=100, amp=0.1, sigma=40, beta=3) - self.assertTrue(drag_instance1._constraints_lam is drag_instance2._constraints_lam) - - def test_deepcopy(self): - """Test deep copying instance.""" - import copy - - drag = Drag(duration=100, amp=0.1, sigma=40, beta=3) - drag_copied = copy.deepcopy(drag) - - self.assertNotEqual(id(drag), id(drag_copied)) - - orig_wf = drag.get_waveform() - copied_wf = drag_copied.get_waveform() - - np.testing.assert_almost_equal(orig_wf.samples, copied_wf.samples) - - def test_fully_parametrized_pulse(self): - """Test instantiating a pulse with parameters.""" - amp = Parameter("amp") - duration = Parameter("duration") - sigma = Parameter("sigma") - beta = Parameter("beta") - - # doesn't raise an error - drag = Drag(duration=duration, amp=amp, sigma=sigma, beta=beta) - - with self.assertRaises(PulseError): - drag.get_waveform() - - # pylint: disable=invalid-name - def test_custom_pulse(self): - """Test defining a custom pulse which is not in the form of amp * F(t).""" - t, t1, t2, amp1, amp2 = sym.symbols("t, t1, t2, amp1, amp2") - envelope = sym.Piecewise((amp1, sym.And(t > t1, t < t2)), (amp2, sym.true)) - - custom_pulse = SymbolicPulse( - pulse_type="Custom", - duration=100, - parameters={"t1": 30, "t2": 80, "amp1": 0.1j, "amp2": -0.1}, - envelope=envelope, - ) - waveform = custom_pulse.get_waveform() - reference = np.concatenate([-0.1 * np.ones(30), 0.1j * np.ones(50), -0.1 * np.ones(20)]) - np.testing.assert_array_almost_equal(waveform.samples, reference) - - def test_gaussian_deprecated_type_check(self): - """Test isinstance check works with deprecation.""" - gaussian_pulse = Gaussian(160, 0.1, 40) - - self.assertTrue(isinstance(gaussian_pulse, SymbolicPulse)) - self.assertTrue(isinstance(gaussian_pulse, Gaussian)) - self.assertFalse(isinstance(gaussian_pulse, GaussianSquare)) - self.assertFalse(isinstance(gaussian_pulse, Drag)) - self.assertFalse(isinstance(gaussian_pulse, Constant)) - - def test_gaussian_square_deprecated_type_check(self): - """Test isinstance check works with deprecation.""" - gaussian_square_pulse = GaussianSquare(800, 0.1, 64, 544) - - self.assertTrue(isinstance(gaussian_square_pulse, SymbolicPulse)) - self.assertFalse(isinstance(gaussian_square_pulse, Gaussian)) - self.assertTrue(isinstance(gaussian_square_pulse, GaussianSquare)) - self.assertFalse(isinstance(gaussian_square_pulse, Drag)) - self.assertFalse(isinstance(gaussian_square_pulse, Constant)) - - def test_drag_deprecated_type_check(self): - """Test isinstance check works with deprecation.""" - drag_pulse = Drag(160, 0.1, 40, 1.5) - - self.assertTrue(isinstance(drag_pulse, SymbolicPulse)) - self.assertFalse(isinstance(drag_pulse, Gaussian)) - self.assertFalse(isinstance(drag_pulse, GaussianSquare)) - self.assertTrue(isinstance(drag_pulse, Drag)) - self.assertFalse(isinstance(drag_pulse, Constant)) - - def test_constant_deprecated_type_check(self): - """Test isinstance check works with deprecation.""" - constant_pulse = Constant(160, 0.1, 40, 1.5) - - self.assertTrue(isinstance(constant_pulse, SymbolicPulse)) - self.assertFalse(isinstance(constant_pulse, Gaussian)) - self.assertFalse(isinstance(constant_pulse, GaussianSquare)) - self.assertFalse(isinstance(constant_pulse, Drag)) - self.assertTrue(isinstance(constant_pulse, Constant)) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestFunctionalPulse(QiskitTestCase): - """Waveform tests.""" - - # pylint: disable=invalid-name - def test_gaussian(self): - """Test gaussian pulse.""" - - @functional_pulse - def local_gaussian(duration, amp, t0, sig): - x = np.linspace(0, duration - 1, duration) - return amp * np.exp(-((x - t0) ** 2) / sig**2) - - pulse_wf_inst = local_gaussian(duration=10, amp=1, t0=5, sig=1, name="test_pulse") - _y = 1 * np.exp(-((np.linspace(0, 9, 10) - 5) ** 2) / 1**2) - - self.assertListEqual(list(pulse_wf_inst.samples), list(_y)) - - # check name - self.assertEqual(pulse_wf_inst.name, "test_pulse") - - # check duration - self.assertEqual(pulse_wf_inst.duration, 10) - - # pylint: disable=invalid-name - def test_variable_duration(self): - """Test generation of sample pulse with variable duration.""" - - @functional_pulse - def local_gaussian(duration, amp, t0, sig): - x = np.linspace(0, duration - 1, duration) - return amp * np.exp(-((x - t0) ** 2) / sig**2) - - _durations = np.arange(10, 15, 1) - - for _duration in _durations: - pulse_wf_inst = local_gaussian(duration=_duration, amp=1, t0=5, sig=1) - self.assertEqual(len(pulse_wf_inst.samples), _duration) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestScalableSymbolicPulse(QiskitTestCase): - """ScalableSymbolicPulse tests""" - - def test_scalable_comparison(self): - """Test equating of pulses""" - # amp,angle comparison - gaussian_negamp = Gaussian(duration=25, sigma=4, amp=-0.5, angle=0) - gaussian_piphase = Gaussian(duration=25, sigma=4, amp=0.5, angle=np.pi) - self.assertEqual(gaussian_negamp, gaussian_piphase) - - # Parameterized library pulses - amp = Parameter("amp") - gaussian1 = Gaussian(duration=25, sigma=4, amp=amp, angle=0) - gaussian2 = Gaussian(duration=25, sigma=4, amp=amp, angle=0) - self.assertEqual(gaussian1, gaussian2) - - # pulses with different parameters - gaussian1._params["sigma"] = 10 - self.assertNotEqual(gaussian1, gaussian2) - - def test_complex_amp_error(self): - """Test that initializing a pulse with complex amp raises an error""" - with self.assertRaises(PulseError): - ScalableSymbolicPulse("test", duration=100, amp=0.1j, angle=0.0) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/pulse/test_reference.py b/test/python/pulse/test_reference.py deleted file mode 100644 index 94e4215d1b58..000000000000 --- a/test/python/pulse/test_reference.py +++ /dev/null @@ -1,641 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test schedule block subroutine reference mechanism.""" - -import numpy as np - -from qiskit import circuit, pulse -from qiskit.pulse import ScheduleBlock, builder -from qiskit.pulse.transforms import inline_subroutines -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestReference(QiskitTestCase): - """Test for basic behavior of reference mechanism.""" - - def test_append_schedule(self): - """Test appending schedule without calling. - - Appended schedules are not subroutines. - These are directly exposed to the outer block. - """ - with pulse.build() as sched_x1: - pulse.play(pulse.Constant(100, 0.1, name="x1"), pulse.DriveChannel(0)) - - with pulse.build() as sched_y1: - builder.append_schedule(sched_x1) - - with pulse.build() as sched_z1: - builder.append_schedule(sched_y1) - - self.assertEqual(len(sched_z1.references), 0) - - def test_refer_schedule(self): - """Test refer to schedule by name. - - Outer block is only aware of its inner reference. - Nested reference is not directly exposed to the most outer block. - """ - with pulse.build() as sched_x1: - pulse.play(pulse.Constant(100, 0.1, name="x1"), pulse.DriveChannel(0)) - - with pulse.build() as sched_y1: - builder.reference("x1", "d0") - - with pulse.build() as sched_z1: - builder.reference("y1", "d0") - - sched_y1.assign_references({("x1", "d0"): sched_x1}) - sched_z1.assign_references({("y1", "d0"): sched_y1}) - - self.assertEqual(len(sched_z1.references), 1) - self.assertEqual(sched_z1.references[("y1", "d0")], sched_y1) - - self.assertEqual(len(sched_y1.references), 1) - self.assertEqual(sched_y1.references[("x1", "d0")], sched_x1) - - def test_refer_schedule_parameter_scope(self): - """Test refer to schedule by name. - - Parameter in the called schedule has the scope of called schedule. - """ - param = circuit.Parameter("name") - - with pulse.build() as sched_x1: - pulse.play(pulse.Constant(100, param, name="x1"), pulse.DriveChannel(0)) - - with pulse.build() as sched_y1: - builder.reference("x1", "d0") - - with pulse.build() as sched_z1: - builder.reference("y1", "d0") - - sched_y1.assign_references({("x1", "d0"): sched_x1}) - sched_z1.assign_references({("y1", "d0"): sched_y1}) - - self.assertEqual(sched_z1.parameters, sched_x1.parameters) - self.assertEqual(sched_z1.parameters, sched_y1.parameters) - - def test_refer_schedule_parameter_assignment(self): - """Test assigning to parameter in referenced schedule""" - param = circuit.Parameter("name") - - with pulse.build() as sched_x1: - pulse.play(pulse.Constant(100, param, name="x1"), pulse.DriveChannel(0)) - - with pulse.build() as sched_y1: - builder.reference("x1", "d0") - - with pulse.build() as sched_z1: - builder.reference("y1", "d0") - - sched_y1.assign_references({("x1", "d0"): sched_x1}) - sched_z1.assign_references({("y1", "d0"): sched_y1}) - - assigned_z1 = sched_z1.assign_parameters({param: 0.5}, inplace=False) - - assigned_x1 = sched_x1.assign_parameters({param: 0.5}, inplace=False) - ref_assigned_y1 = ScheduleBlock() - ref_assigned_y1.append(assigned_x1) - ref_assigned_z1 = ScheduleBlock() - ref_assigned_z1.append(ref_assigned_y1) - - # Test that assignment was successful and resolved references - self.assertEqual(assigned_z1, ref_assigned_z1) - - # Test that inplace=False for sched_z1 also did not modify sched_z1 or subroutine sched_x1 - self.assertEqual(sched_z1.parameters, {param}) - self.assertEqual(sched_x1.parameters, {param}) - self.assertEqual(assigned_z1.parameters, set()) - - # Now test inplace=True - sched_z1.assign_parameters({param: 0.5}, inplace=True) - self.assertEqual(sched_z1, assigned_z1) - # assign_references copies the subroutine, so the original subschedule - # is still not modified here: - self.assertNotEqual(sched_x1, assigned_x1) - - def test_call_schedule(self): - """Test call schedule. - - Outer block is only aware of its inner reference. - Nested reference is not directly exposed to the most outer block. - """ - with pulse.build() as sched_x1: - pulse.play(pulse.Constant(100, 0.1, name="x1"), pulse.DriveChannel(0)) - - with pulse.build() as sched_y1: - builder.call(sched_x1, name="x1") - - with pulse.build() as sched_z1: - builder.call(sched_y1, name="y1") - - self.assertEqual(len(sched_z1.references), 1) - self.assertEqual(sched_z1.references[("y1",)], sched_y1) - - self.assertEqual(len(sched_y1.references), 1) - self.assertEqual(sched_y1.references[("x1",)], sched_x1) - - def test_call_schedule_parameter_scope(self): - """Test call schedule. - - Parameter in the called schedule has the scope of called schedule. - """ - param = circuit.Parameter("name") - - with pulse.build() as sched_x1: - pulse.play(pulse.Constant(100, param, name="x1"), pulse.DriveChannel(0)) - - with pulse.build() as sched_y1: - builder.call(sched_x1, name="x1") - - with pulse.build() as sched_z1: - builder.call(sched_y1, name="y1") - - self.assertEqual(sched_z1.parameters, sched_x1.parameters) - self.assertEqual(sched_z1.parameters, sched_y1.parameters) - - def test_append_and_call_schedule(self): - """Test call and append schedule. - - Reference is copied to the outer schedule by appending. - Original reference remains unchanged. - """ - with pulse.build() as sched_x1: - pulse.play(pulse.Constant(100, 0.1, name="x1"), pulse.DriveChannel(0)) - - with pulse.build() as sched_y1: - builder.call(sched_x1, name="x1") - - with pulse.build() as sched_z1: - builder.append_schedule(sched_y1) - - self.assertEqual(len(sched_z1.references), 1) - self.assertEqual(sched_z1.references[("x1",)], sched_x1) - - # blocks[0] is sched_y1 and its reference is now point to outer block reference - self.assertIs(sched_z1.blocks[0].references, sched_z1.references) - - # however the original program is protected to prevent unexpected mutation - self.assertIsNot(sched_y1.references, sched_z1.references) - - # appended schedule is preserved - self.assertEqual(len(sched_y1.references), 1) - self.assertEqual(sched_y1.references[("x1",)], sched_x1) - - def test_calling_similar_schedule(self): - """Test calling schedules with the same representation. - - sched_x1 and sched_y1 are the different subroutines, but same representation. - Two references should be created. - """ - param1 = circuit.Parameter("param") - param2 = circuit.Parameter("param") - - with pulse.build() as sched_x1: - pulse.play(pulse.Constant(100, param1, name="p"), pulse.DriveChannel(0)) - - with pulse.build() as sched_y1: - pulse.play(pulse.Constant(100, param2, name="p"), pulse.DriveChannel(0)) - - with pulse.build() as sched_z1: - pulse.call(sched_x1) - pulse.call(sched_y1) - - self.assertEqual(len(sched_z1.references), 2) - - def test_calling_same_schedule(self): - """Test calling same schedule twice. - - Because it calls the same schedule, no duplication should occur in reference table. - """ - param = circuit.Parameter("param") - - with pulse.build() as sched_x1: - pulse.play(pulse.Constant(100, param, name="x1"), pulse.DriveChannel(0)) - - with pulse.build() as sched_z1: - pulse.call(sched_x1, name="same_sched") - pulse.call(sched_x1, name="same_sched") - - self.assertEqual(len(sched_z1.references), 1) - - def test_calling_same_schedule_with_different_assignment(self): - """Test calling same schedule twice but with different parameters. - - Same schedule is called twice but with different assignment. - Two references should be created. - """ - param = circuit.Parameter("param") - - with pulse.build() as sched_x1: - pulse.play(pulse.Constant(100, param, name="x1"), pulse.DriveChannel(0)) - - with pulse.build() as sched_z1: - pulse.call(sched_x1, param=0.1) - pulse.call(sched_x1, param=0.2) - - self.assertEqual(len(sched_z1.references), 2) - - def test_alignment_context(self): - """Test nested alignment context. - - Inline alignment is identical to append_schedule operation. - Thus scope is not newly generated. - """ - with pulse.build(name="x1") as sched_x1: - with pulse.align_right(): - with pulse.align_left(): - pulse.play(pulse.Constant(100, 0.1, name="x1"), pulse.DriveChannel(0)) - - self.assertEqual(len(sched_x1.references), 0) - - def test_appending_child_block(self): - """Test for edge case. - - User can append blocks which is an element of another schedule block. - But this is not standard use case. - - In this case, references may contain subroutines which don't exist in the context. - This is because all references within the program are centrally - managed in the most outer block. - """ - with pulse.build() as sched_x1: - pulse.play(pulse.Constant(100, 0.1, name="x1"), pulse.DriveChannel(0)) - - with pulse.build() as sched_y1: - pulse.play(pulse.Constant(100, 0.2, name="y1"), pulse.DriveChannel(0)) - - with pulse.build() as sched_x2: - builder.call(sched_x1, name="x1") - self.assertEqual(list(sched_x2.references.keys()), [("x1",)]) - - with pulse.build() as sched_y2: - builder.call(sched_y1, name="y1") - self.assertEqual(list(sched_y2.references.keys()), [("y1",)]) - - with pulse.build() as sched_z1: - builder.append_schedule(sched_x2) - builder.append_schedule(sched_y2) - self.assertEqual(list(sched_z1.references.keys()), [("x1",), ("y1",)]) - - # child block references point to its parent, i.e. sched_z1 - self.assertIs(sched_z1.blocks[0].references, sched_z1._reference_manager) - self.assertIs(sched_z1.blocks[1].references, sched_z1._reference_manager) - - with pulse.build() as sched_z2: - # Append child block - # The reference of this block is sched_z1.reference thus it contains both x1 and y1. - # However, y1 doesn't exist in the context, so only x1 should be added. - - # Usually, user will append sched_x2 directly here, rather than sched_z1.blocks[0] - # This is why this situation is an edge case. - builder.append_schedule(sched_z1.blocks[0]) - - self.assertEqual(len(sched_z2.references), 1) - self.assertEqual(sched_z2.references[("x1",)], sched_x1) - - def test_replacement(self): - """Test nested alignment context. - - Replacing schedule block with schedule block. - Removed block contains own reference, that should be removed with replacement. - New block also contains reference, that should be passed to the current reference. - """ - with pulse.build() as sched_x1: - pulse.play(pulse.Constant(100, 0.1, name="x1"), pulse.DriveChannel(0)) - - with pulse.build() as sched_y1: - pulse.play(pulse.Constant(100, 0.2, name="y1"), pulse.DriveChannel(0)) - - with pulse.build() as sched_x2: - builder.call(sched_x1, name="x1") - - with pulse.build() as sched_y2: - builder.call(sched_y1, name="y1") - - with pulse.build() as sched_z1: - builder.append_schedule(sched_x2) - builder.append_schedule(sched_y2) - self.assertEqual(len(sched_z1.references), 2) - self.assertEqual(sched_z1.references[("x1",)], sched_x1) - self.assertEqual(sched_z1.references[("y1",)], sched_y1) - - # Define schedule to replace - with pulse.build() as sched_r1: - pulse.play(pulse.Constant(100, 0.1, name="r1"), pulse.DriveChannel(0)) - - with pulse.build() as sched_r2: - pulse.call(sched_r1, name="r1") - - sched_z2 = sched_z1.replace(sched_x2, sched_r2) - self.assertEqual(len(sched_z2.references), 2) - self.assertEqual(sched_z2.references[("r1",)], sched_r1) - self.assertEqual(sched_z2.references[("y1",)], sched_y1) - - def test_parameter_in_multiple_scope(self): - """Test that using parameter in multiple scopes causes no error""" - param = circuit.Parameter("name") - - with pulse.build() as sched_x1: - pulse.play(pulse.Constant(100, param), pulse.DriveChannel(0)) - - with pulse.build() as sched_y1: - pulse.play(pulse.Constant(100, param), pulse.DriveChannel(1)) - - with pulse.build() as sched_z1: - pulse.call(sched_x1, name="x1") - pulse.call(sched_y1, name="y1") - - self.assertEqual(len(sched_z1.parameters), 1) - self.assertEqual(sched_z1.parameters, {param}) - - def test_parallel_alignment_equality(self): - """Testcase for potential edge case. - - In parallel alignment context, reference instruction is broadcasted to - all channels. When new channel is added after reference, this should be - connected with reference node. - """ - - with pulse.build() as subroutine: - pulse.reference("unassigned") - - with pulse.build() as sched1: - with pulse.align_left(): - pulse.delay(10, pulse.DriveChannel(0)) - pulse.call(subroutine) # This should be broadcasted to d1 as well - pulse.delay(10, pulse.DriveChannel(1)) - - with pulse.build() as sched2: - with pulse.align_left(): - pulse.delay(10, pulse.DriveChannel(0)) - pulse.delay(10, pulse.DriveChannel(1)) - pulse.call(subroutine) - - self.assertNotEqual(sched1, sched2) - - def test_subroutine_conflict(self): - """Test for edge case of appending two schedule blocks having the - references with conflicting reference key. - - This operation should fail because one of references will be gone after assignment. - """ - with pulse.build() as sched_x1: - pulse.play(pulse.Constant(100, 0.1), pulse.DriveChannel(0)) - - with pulse.build() as sched_x2: - pulse.call(sched_x1, name="conflict_name") - - self.assertEqual(sched_x2.references[("conflict_name",)], sched_x1) - - with pulse.build() as sched_y1: - pulse.play(pulse.Constant(100, 0.2), pulse.DriveChannel(0)) - - with pulse.build() as sched_y2: - pulse.call(sched_y1, name="conflict_name") - - self.assertEqual(sched_y2.references[("conflict_name",)], sched_y1) - - with self.assertRaises(pulse.exceptions.PulseError): - with pulse.build(): - builder.append_schedule(sched_x2) - builder.append_schedule(sched_y2) - - def test_assign_existing_reference(self): - """Test for explicitly assign existing reference. - - This operation should fail because overriding reference is not allowed. - """ - with pulse.build() as sched_x1: - pulse.play(pulse.Constant(100, 0.1), pulse.DriveChannel(0)) - - with pulse.build() as sched_y1: - pulse.play(pulse.Constant(100, 0.2), pulse.DriveChannel(0)) - - with pulse.build() as sched_z1: - pulse.call(sched_x1, name="conflict_name") - - with self.assertRaises(pulse.exceptions.PulseError): - sched_z1.assign_references({("conflict_name",): sched_y1}) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestSubroutineWithCXGate(QiskitTestCase): - """Test called program scope with practical example of building fully parametrized CX gate.""" - - @ignore_pulse_deprecation_warnings - def setUp(self): - super().setUp() - - # parameters of X pulse - self.xp_dur = circuit.Parameter("dur") - self.xp_amp = circuit.Parameter("amp") - self.xp_sigma = circuit.Parameter("sigma") - self.xp_beta = circuit.Parameter("beta") - - # amplitude of SX pulse - self.sxp_amp = circuit.Parameter("amp") - - # parameters of CR pulse - self.cr_dur = circuit.Parameter("dur") - self.cr_amp = circuit.Parameter("amp") - self.cr_sigma = circuit.Parameter("sigma") - self.cr_risefall = circuit.Parameter("risefall") - - # channels - self.control_ch = circuit.Parameter("ctrl") - self.target_ch = circuit.Parameter("tgt") - self.cr_ch = circuit.Parameter("cr") - - # echo pulse on control qubit - with pulse.build(name="xp") as xp_sched_q0: - pulse.play( - pulse.Drag( - duration=self.xp_dur, - amp=self.xp_amp, - sigma=self.xp_sigma, - beta=self.xp_beta, - ), - channel=pulse.DriveChannel(self.control_ch), - ) - self.xp_sched = xp_sched_q0 - - # local rotation on target qubit - with pulse.build(name="sx") as sx_sched_q1: - pulse.play( - pulse.Drag( - duration=self.xp_dur, - amp=self.sxp_amp, - sigma=self.xp_sigma, - beta=self.xp_beta, - ), - channel=pulse.DriveChannel(self.target_ch), - ) - self.sx_sched = sx_sched_q1 - - # cross resonance - with pulse.build(name="cr") as cr_sched: - pulse.play( - pulse.GaussianSquare( - duration=self.cr_dur, - amp=self.cr_amp, - sigma=self.cr_sigma, - risefall_sigma_ratio=self.cr_risefall, - ), - channel=pulse.ControlChannel(self.cr_ch), - ) - self.cr_sched = cr_sched - - def test_lazy_ecr(self): - """Test lazy subroutines through ECR schedule construction.""" - - with pulse.build(name="lazy_ecr") as sched: - with pulse.align_sequential(): - pulse.reference("cr", "q0", "q1") - pulse.reference("xp", "q0") - with pulse.phase_offset(np.pi, pulse.ControlChannel(self.cr_ch)): - pulse.reference("cr", "q0", "q1") - pulse.reference("xp", "q0") - - # Schedule has references - self.assertTrue(sched.is_referenced()) - - # Schedule is not schedulable because of unassigned references - self.assertFalse(sched.is_schedulable()) - - # Two references cr and xp are called - self.assertEqual(len(sched.references), 2) - - # Parameters in the current scope are Parameter("cr") which is used in phase_offset - # References are not assigned yet. - params = {p.name for p in sched.parameters} - self.assertSetEqual(params, {"cr"}) - - # Assign CR and XP schedule to the empty reference - sched.assign_references({("cr", "q0", "q1"): self.cr_sched}) - sched.assign_references({("xp", "q0"): self.xp_sched}) - - # Check updated references - assigned_refs = sched.references - self.assertEqual(assigned_refs[("cr", "q0", "q1")], self.cr_sched) - self.assertEqual(assigned_refs[("xp", "q0")], self.xp_sched) - - # Parameter added from subroutines - ref_params = {self.cr_ch} | self.cr_sched.parameters | self.xp_sched.parameters - self.assertSetEqual(sched.parameters, ref_params) - - # Get parameter without scope, cr amp and xp amp are hit. - params = sched.get_parameters(parameter_name="amp") - self.assertEqual(len(params), 2) - - def test_cnot(self): - """Integration test with CNOT schedule construction.""" - # echoed cross resonance - with pulse.build(name="ecr", default_alignment="sequential") as ecr_sched: - pulse.call(self.cr_sched, name="cr") - pulse.call(self.xp_sched, name="xp") - with pulse.phase_offset(np.pi, pulse.ControlChannel(self.cr_ch)): - pulse.call(self.cr_sched, name="cr") - pulse.call(self.xp_sched, name="xp") - - # cnot gate, locally equivalent to ecr - with pulse.build(name="cx", default_alignment="sequential") as cx_sched: - pulse.shift_phase(np.pi / 2, pulse.DriveChannel(self.control_ch)) - pulse.call(self.sx_sched, name="sx") - pulse.call(ecr_sched, name="ecr") - - # assign parameters - assigned_cx = cx_sched.assign_parameters( - value_dict={ - self.cr_ch: 0, - self.control_ch: 0, - self.target_ch: 1, - self.sxp_amp: 0.1, - self.xp_amp: 0.2, - self.xp_dur: 160, - self.xp_sigma: 40, - self.xp_beta: 3.0, - self.cr_amp: 0.5, - self.cr_dur: 800, - self.cr_sigma: 64, - self.cr_risefall: 2, - }, - inplace=True, - ) - flatten_cx = inline_subroutines(assigned_cx) - - with pulse.build(default_alignment="sequential") as ref_cx: - # sz - pulse.shift_phase(np.pi / 2, pulse.DriveChannel(0)) - with pulse.align_left(): - # sx - pulse.play( - pulse.Drag( - duration=160, - amp=0.1, - sigma=40, - beta=3.0, - ), - channel=pulse.DriveChannel(1), - ) - with pulse.align_sequential(): - # cr - with pulse.align_left(): - pulse.play( - pulse.GaussianSquare( - duration=800, - amp=0.5, - sigma=64, - risefall_sigma_ratio=2, - ), - channel=pulse.ControlChannel(0), - ) - # xp - with pulse.align_left(): - pulse.play( - pulse.Drag( - duration=160, - amp=0.2, - sigma=40, - beta=3.0, - ), - channel=pulse.DriveChannel(0), - ) - with pulse.phase_offset(np.pi, pulse.ControlChannel(0)): - # cr - with pulse.align_left(): - pulse.play( - pulse.GaussianSquare( - duration=800, - amp=0.5, - sigma=64, - risefall_sigma_ratio=2, - ), - channel=pulse.ControlChannel(0), - ) - # xp - with pulse.align_left(): - pulse.play( - pulse.Drag( - duration=160, - amp=0.2, - sigma=40, - beta=3.0, - ), - channel=pulse.DriveChannel(0), - ) - - self.assertEqual(flatten_cx, ref_cx) diff --git a/test/python/pulse/test_samplers.py b/test/python/pulse/test_samplers.py deleted file mode 100644 index 59a8805c0c06..000000000000 --- a/test/python/pulse/test_samplers.py +++ /dev/null @@ -1,96 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# 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. - - -"""Tests pulse function samplers.""" - -import numpy as np - -from qiskit.pulse import library -from qiskit.pulse.library import samplers -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings - - -def linear(times: np.ndarray, m: float, b: float = 0.1) -> np.ndarray: - """Linear test function - Args: - times: Input times. - m: Slope. - b: Intercept - Returns: - np.ndarray - """ - return m * times + b - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestSampler(QiskitTestCase): - """Test continuous pulse function samplers.""" - - def test_left_sampler(self): - """Test left sampler.""" - m = 0.1 - b = 0.1 - duration = 2 - left_linear_pulse_fun = samplers.left(linear) - reference = np.array([0.1, 0.2], dtype=complex) - - pulse = left_linear_pulse_fun(duration, m=m, b=b) - self.assertIsInstance(pulse, library.Waveform) - np.testing.assert_array_almost_equal(pulse.samples, reference) - - def test_right_sampler(self): - """Test right sampler.""" - m = 0.1 - b = 0.1 - duration = 2 - right_linear_pulse_fun = samplers.right(linear) - reference = np.array([0.2, 0.3], dtype=complex) - - pulse = right_linear_pulse_fun(duration, m=m, b=b) - self.assertIsInstance(pulse, library.Waveform) - np.testing.assert_array_almost_equal(pulse.samples, reference) - - def test_midpoint_sampler(self): - """Test midpoint sampler.""" - m = 0.1 - b = 0.1 - duration = 2 - midpoint_linear_pulse_fun = samplers.midpoint(linear) - reference = np.array([0.15, 0.25], dtype=complex) - - pulse = midpoint_linear_pulse_fun(duration, m=m, b=b) - self.assertIsInstance(pulse, library.Waveform) - np.testing.assert_array_almost_equal(pulse.samples, reference) - - def test_sampler_name(self): - """Test that sampler setting of pulse name works.""" - m = 0.1 - b = 0.1 - duration = 2 - left_linear_pulse_fun = samplers.left(linear) - - pulse = left_linear_pulse_fun(duration, m=m, b=b, name="test") - self.assertIsInstance(pulse, library.Waveform) - self.assertEqual(pulse.name, "test") - - def test_default_arg_sampler(self): - """Test that default arguments work with sampler.""" - m = 0.1 - duration = 2 - left_linear_pulse_fun = samplers.left(linear) - reference = np.array([0.1, 0.2], dtype=complex) - - pulse = left_linear_pulse_fun(duration, m=m) - self.assertIsInstance(pulse, library.Waveform) - np.testing.assert_array_almost_equal(pulse.samples, reference) diff --git a/test/python/pulse/test_schedule.py b/test/python/pulse/test_schedule.py deleted file mode 100644 index fa59b8353c19..000000000000 --- a/test/python/pulse/test_schedule.py +++ /dev/null @@ -1,469 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2019. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test cases for the pulse schedule.""" -import unittest -from unittest.mock import patch - -import numpy as np - -from qiskit.pulse import ( - Play, - Waveform, - ShiftPhase, - Acquire, - Snapshot, - Delay, - Gaussian, - Drag, - GaussianSquare, - Constant, - functional_pulse, -) -from qiskit.pulse.channels import ( - MemorySlot, - DriveChannel, - ControlChannel, - AcquireChannel, - SnapshotChannel, - MeasureChannel, -) -from qiskit.pulse.exceptions import PulseError -from qiskit.pulse.schedule import Schedule, _overlaps, _find_insertion_index -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class BaseTestSchedule(QiskitTestCase): - """Schedule tests.""" - - @ignore_pulse_deprecation_warnings - def setUp(self): - super().setUp() - - @functional_pulse - def linear(duration, slope, intercept): - x = np.linspace(0, duration - 1, duration) - return slope * x + intercept - - self.linear = linear - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestScheduleBuilding(BaseTestSchedule): - """Test construction of schedules.""" - - def test_empty_schedule(self): - """Test empty schedule.""" - sched = Schedule() - self.assertEqual(0, sched.start_time) - self.assertEqual(0, sched.stop_time) - self.assertEqual(0, sched.duration) - self.assertEqual(0, len(sched)) - self.assertEqual((), sched.children) - self.assertEqual({}, sched.timeslots) - self.assertEqual([], list(sched.instructions)) - self.assertFalse(sched) - - def test_overlapping_schedules(self): - """Test overlapping schedules.""" - - def my_test_make_schedule(acquire: int, memoryslot: int, shift: int): - sched1 = Acquire(acquire, AcquireChannel(0), MemorySlot(memoryslot)) - sched2 = Acquire(acquire, AcquireChannel(1), MemorySlot(memoryslot)).shift(shift) - - return Schedule(sched1, sched2) - - self.assertIsInstance(my_test_make_schedule(4, 0, 4), Schedule) - self.assertRaisesRegex( - PulseError, r".*MemorySlot\(0\).*overlaps .*", my_test_make_schedule, 4, 0, 2 - ) - self.assertRaisesRegex( - PulseError, r".*MemorySlot\(1\).*overlaps .*", my_test_make_schedule, 4, 1, 0 - ) - - @patch("qiskit.utils.is_main_process", return_value=True) - def test_auto_naming(self, is_main_process_mock): - """Test that a schedule gets a default name, incremented per instance""" - - del is_main_process_mock - - sched_0 = Schedule() - sched_0_name_count = int(sched_0.name[len("sched") :]) - - sched_1 = Schedule() - sched_1_name_count = int(sched_1.name[len("sched") :]) - self.assertEqual(sched_1_name_count, sched_0_name_count + 1) - - sched_2 = Schedule() - sched_2_name_count = int(sched_2.name[len("sched") :]) - self.assertEqual(sched_2_name_count, sched_1_name_count + 1) - - def test_parametric_commands_in_sched(self): - """Test that schedules can be built with parametric commands.""" - sched = Schedule(name="test_parametric") - sched += Play(Gaussian(duration=25, sigma=4, amp=0.5, angle=np.pi / 2), DriveChannel(0)) - sched += Play(Drag(duration=25, amp=0.4, angle=0.5, sigma=7.8, beta=4), DriveChannel(1)) - sched += Play(Constant(duration=25, amp=1), DriveChannel(2)) - sched_duration = sched.duration - sched += ( - Play(GaussianSquare(duration=1500, amp=0.2, sigma=8, width=140), MeasureChannel(0)) - << sched_duration - ) - self.assertEqual(sched.duration, 1525) - self.assertTrue("sigma" in sched.instructions[0][1].pulse.parameters) - - def test_numpy_integer_input(self): - """Test that mixed integer duration types can build a schedule (#5754).""" - sched = Schedule() - sched += Delay(np.int32(25), DriveChannel(0)) - sched += Play(Constant(duration=30, amp=0.1), DriveChannel(0)) - self.assertEqual(sched.duration, 55) - - def test_negative_time_raises(self): - """Test that a negative time will raise an error.""" - sched = Schedule() - sched += Delay(1, DriveChannel(0)) - with self.assertRaises(PulseError): - sched.shift(-10) - - def test_shift_float_time_raises(self): - """Test that a floating time will raise an error with shift.""" - sched = Schedule() - sched += Delay(1, DriveChannel(0)) - with self.assertRaises(PulseError): - sched.shift(0.1) - - def test_insert_float_time_raises(self): - """Test that a floating time will raise an error with insert.""" - sched = Schedule() - sched += Delay(1, DriveChannel(0)) - with self.assertRaises(PulseError): - sched.insert(10.1, sched) - - def test_shift_unshift(self): - """Test shift and then unshifting of schedule""" - reference_sched = Schedule() - reference_sched += Delay(10, DriveChannel(0)) - shifted_sched = reference_sched.shift(10).shift(-10) - self.assertEqual(shifted_sched, reference_sched) - - def test_duration(self): - """Test schedule.duration.""" - reference_sched = Schedule() - reference_sched = reference_sched.insert(10, Delay(10, DriveChannel(0))) - reference_sched = reference_sched.insert(10, Delay(50, DriveChannel(1))) - reference_sched = reference_sched.insert(10, ShiftPhase(0.1, DriveChannel(0))) - - reference_sched = reference_sched.insert(100, ShiftPhase(0.1, DriveChannel(1))) - - self.assertEqual(reference_sched.duration, 100) - self.assertEqual(reference_sched.duration, 100) - - def test_ch_duration(self): - """Test schedule.ch_duration.""" - reference_sched = Schedule() - reference_sched = reference_sched.insert(10, Delay(10, DriveChannel(0))) - reference_sched = reference_sched.insert(10, Delay(50, DriveChannel(1))) - reference_sched = reference_sched.insert(10, ShiftPhase(0.1, DriveChannel(0))) - - reference_sched = reference_sched.insert(100, ShiftPhase(0.1, DriveChannel(1))) - - self.assertEqual(reference_sched.ch_duration(DriveChannel(0)), 20) - self.assertEqual(reference_sched.ch_duration(DriveChannel(1)), 100) - self.assertEqual( - reference_sched.ch_duration(*reference_sched.channels), reference_sched.duration - ) - - def test_ch_start_time(self): - """Test schedule.ch_start_time.""" - reference_sched = Schedule() - reference_sched = reference_sched.insert(10, Delay(10, DriveChannel(0))) - reference_sched = reference_sched.insert(10, Delay(50, DriveChannel(1))) - reference_sched = reference_sched.insert(10, ShiftPhase(0.1, DriveChannel(0))) - - reference_sched = reference_sched.insert(100, ShiftPhase(0.1, DriveChannel(1))) - - self.assertEqual(reference_sched.ch_start_time(DriveChannel(0)), 10) - self.assertEqual(reference_sched.ch_start_time(DriveChannel(1)), 10) - - def test_ch_stop_time(self): - """Test schedule.ch_stop_time.""" - reference_sched = Schedule() - reference_sched = reference_sched.insert(10, Delay(10, DriveChannel(0))) - reference_sched = reference_sched.insert(10, Delay(50, DriveChannel(1))) - reference_sched = reference_sched.insert(10, ShiftPhase(0.1, DriveChannel(0))) - - reference_sched = reference_sched.insert(100, ShiftPhase(0.1, DriveChannel(1))) - - self.assertEqual(reference_sched.ch_stop_time(DriveChannel(0)), 20) - self.assertEqual(reference_sched.ch_stop_time(DriveChannel(1)), 100) - - def test_timeslots(self): - """Test schedule.timeslots.""" - reference_sched = Schedule() - reference_sched = reference_sched.insert(10, Delay(10, DriveChannel(0))) - reference_sched = reference_sched.insert(10, Delay(50, DriveChannel(1))) - reference_sched = reference_sched.insert(10, ShiftPhase(0.1, DriveChannel(0))) - - reference_sched = reference_sched.insert(100, ShiftPhase(0.1, DriveChannel(1))) - - self.assertEqual(reference_sched.timeslots[DriveChannel(0)], [(10, 10), (10, 20)]) - self.assertEqual(reference_sched.timeslots[DriveChannel(1)], [(10, 60), (100, 100)]) - - def test_inherit_from(self): - """Test creating schedule with another schedule.""" - ref_metadata = {"test": "value"} - ref_name = "test" - - base_sched = Schedule(name=ref_name, metadata=ref_metadata) - new_sched = Schedule.initialize_from(base_sched) - - self.assertEqual(new_sched.name, ref_name) - self.assertDictEqual(new_sched.metadata, ref_metadata) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestReplace(BaseTestSchedule): - """Test schedule replacement.""" - - def test_replace_instruction(self): - """Test replacement of simple instruction""" - old = Play(Constant(100, 1.0), DriveChannel(0)) - new = Play(Constant(100, 0.1), DriveChannel(0)) - - sched = Schedule(old) - new_sched = sched.replace(old, new) - - self.assertEqual(new_sched, Schedule(new)) - - # test replace inplace - sched.replace(old, new, inplace=True) - self.assertEqual(sched, Schedule(new)) - - def test_replace_schedule(self): - """Test replacement of schedule.""" - - old = Schedule( - Delay(10, DriveChannel(0)), - Delay(100, DriveChannel(1)), - ) - new = Schedule( - Play(Constant(10, 1.0), DriveChannel(0)), - Play(Constant(100, 0.1), DriveChannel(1)), - ) - const = Play(Constant(100, 1.0), DriveChannel(0)) - - sched = Schedule() - sched += const - sched += old - - new_sched = sched.replace(old, new) - - ref_sched = Schedule() - ref_sched += const - ref_sched += new - self.assertEqual(new_sched, ref_sched) - - # test replace inplace - sched.replace(old, new, inplace=True) - self.assertEqual(sched, ref_sched) - - def test_replace_fails_on_overlap(self): - """Test that replacement fails on overlap.""" - old = Play(Constant(20, 1.0), DriveChannel(0)) - new = Play(Constant(100, 0.1), DriveChannel(0)) - - sched = Schedule() - sched += old - sched += Delay(100, DriveChannel(0)) - - with self.assertRaises(PulseError): - sched.replace(old, new) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestDelay(BaseTestSchedule): - """Test Delay Instruction""" - - def setUp(self): - super().setUp() - self.delay_time = 10 - - def test_delay_snapshot_channel(self): - """Test Delay on DriveChannel""" - - snapshot_ch = SnapshotChannel() - snapshot = Snapshot(label="test") - # should pass as is an append - sched = Delay(self.delay_time, snapshot_ch) + snapshot - self.assertIsInstance(sched, Schedule) - # should fail due to overlap - with self.assertRaises(PulseError): - sched = Delay(self.delay_time, snapshot_ch) | snapshot << 5 - self.assertIsInstance(sched, Schedule) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestScheduleFilter(BaseTestSchedule): - """Test Schedule filtering methods""" - - def test_filter_exclude_name(self): - """Test the name of the schedules after applying filter and exclude functions.""" - sched = Schedule(name="test-schedule") - sched = sched.insert(10, Acquire(5, AcquireChannel(0), MemorySlot(0))) - sched = sched.insert(10, Acquire(5, AcquireChannel(1), MemorySlot(1))) - excluded = sched.exclude(channels=[AcquireChannel(0)]) - filtered = sched.filter(channels=[AcquireChannel(1)]) - - # check if the excluded and filtered schedule have the same name as sched - self.assertEqual(sched.name, filtered.name) - self.assertEqual(sched.name, excluded.name) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestScheduleEquality(BaseTestSchedule): - """Test equality of schedules.""" - - def test_different_channels(self): - """Test equality is False if different channels.""" - self.assertNotEqual( - Schedule(ShiftPhase(0, DriveChannel(0))), Schedule(ShiftPhase(0, DriveChannel(1))) - ) - - def test_same_time_equal(self): - """Test equal if instruction at same time.""" - - self.assertEqual( - Schedule((0, ShiftPhase(0, DriveChannel(1)))), - Schedule((0, ShiftPhase(0, DriveChannel(1)))), - ) - - def test_different_time_not_equal(self): - """Test that not equal if instruction at different time.""" - self.assertNotEqual( - Schedule((0, ShiftPhase(0, DriveChannel(1)))), - Schedule((1, ShiftPhase(0, DriveChannel(1)))), - ) - - def test_single_channel_out_of_order(self): - """Test that schedule with single channel equal when out of order.""" - instructions = [ - (0, ShiftPhase(0, DriveChannel(0))), - (15, Play(Waveform(np.ones(10)), DriveChannel(0))), - (5, Play(Waveform(np.ones(10)), DriveChannel(0))), - ] - - self.assertEqual(Schedule(*instructions), Schedule(*reversed(instructions))) - - def test_multiple_channels_out_of_order(self): - """Test that schedule with multiple channels equal when out of order.""" - instructions = [ - (0, ShiftPhase(0, DriveChannel(1))), - (1, Acquire(10, AcquireChannel(0), MemorySlot(1))), - ] - - self.assertEqual(Schedule(*instructions), Schedule(*reversed(instructions))) - - def test_same_commands_on_two_channels_at_same_time_out_of_order(self): - """Test that schedule with same commands on two channels at the same time equal - when out of order.""" - sched1 = Schedule() - sched1 = sched1.append(Delay(100, DriveChannel(1))) - sched1 = sched1.append(Delay(100, ControlChannel(1))) - sched2 = Schedule() - sched2 = sched2.append(Delay(100, ControlChannel(1))) - sched2 = sched2.append(Delay(100, DriveChannel(1))) - self.assertEqual(sched1, sched2) - - def test_different_name_equal(self): - """Test that names are ignored when checking equality.""" - - self.assertEqual( - Schedule((0, ShiftPhase(0, DriveChannel(1), name="fc1")), name="s1"), - Schedule((0, ShiftPhase(0, DriveChannel(1), name="fc2")), name="s2"), - ) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestTimingUtils(QiskitTestCase): - """Test the Schedule helper functions.""" - - def test_overlaps(self): - """Test the `_overlaps` function.""" - a = (0, 1) - b = (1, 4) - c = (2, 3) - d = (3, 5) - self.assertFalse(_overlaps(a, b)) - self.assertFalse(_overlaps(b, a)) - self.assertFalse(_overlaps(a, d)) - self.assertTrue(_overlaps(b, c)) - self.assertTrue(_overlaps(c, b)) - self.assertTrue(_overlaps(b, d)) - self.assertTrue(_overlaps(d, b)) - - def test_overlaps_zero_duration(self): - """Test the `_overlaps` function for intervals with duration zero.""" - a = 0 - b = 1 - self.assertFalse(_overlaps((a, a), (a, a))) - self.assertFalse(_overlaps((a, a), (a, b))) - self.assertFalse(_overlaps((a, b), (a, a))) - self.assertFalse(_overlaps((a, b), (b, b))) - self.assertFalse(_overlaps((b, b), (a, b))) - self.assertTrue(_overlaps((a, a + 2), (a + 1, a + 1))) - self.assertTrue(_overlaps((a + 1, a + 1), (a, a + 2))) - - def test_find_insertion_index(self): - """Test the `_find_insertion_index` function.""" - intervals = [(1, 2), (4, 5)] - self.assertEqual(_find_insertion_index(intervals, (2, 3)), 1) - self.assertEqual(_find_insertion_index(intervals, (3, 4)), 1) - self.assertEqual(intervals, [(1, 2), (4, 5)]) - intervals = [(1, 2), (4, 5), (6, 7)] - self.assertEqual(_find_insertion_index(intervals, (2, 3)), 1) - self.assertEqual(_find_insertion_index(intervals, (0, 1)), 0) - self.assertEqual(_find_insertion_index(intervals, (5, 6)), 2) - self.assertEqual(_find_insertion_index(intervals, (8, 9)), 3) - - longer_intervals = [(1, 2), (2, 3), (4, 5), (5, 6), (7, 9), (11, 11)] - self.assertEqual(_find_insertion_index(longer_intervals, (4, 4)), 2) - self.assertEqual(_find_insertion_index(longer_intervals, (5, 5)), 3) - self.assertEqual(_find_insertion_index(longer_intervals, (3, 4)), 2) - self.assertEqual(_find_insertion_index(longer_intervals, (3, 4)), 2) - - # test when two identical zero duration timeslots are present - intervals = [(0, 10), (73, 73), (73, 73), (90, 101)] - self.assertEqual(_find_insertion_index(intervals, (42, 73)), 1) - self.assertEqual(_find_insertion_index(intervals, (73, 81)), 3) - - def test_find_insertion_index_when_overlapping(self): - """Test that `_find_insertion_index` raises an error when the new_interval _overlaps.""" - intervals = [(10, 20), (44, 55), (60, 61), (80, 1000)] - with self.assertRaises(PulseError): - _find_insertion_index(intervals, (60, 62)) - with self.assertRaises(PulseError): - _find_insertion_index(intervals, (100, 1500)) - - intervals = [(0, 1), (10, 15)] - with self.assertRaises(PulseError): - _find_insertion_index(intervals, (7, 13)) - - def test_find_insertion_index_empty_list(self): - """Test that the insertion index is properly found for empty lists.""" - self.assertEqual(_find_insertion_index([], (0, 1)), 0) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/pulse/test_transforms.py b/test/python/pulse/test_transforms.py deleted file mode 100644 index acf3dd5006ba..000000000000 --- a/test/python/pulse/test_transforms.py +++ /dev/null @@ -1,714 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2019. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test cases for the pulse Schedule transforms.""" -import unittest -from typing import List, Set - -import numpy as np - -from qiskit import pulse -from qiskit.pulse import ( - Play, - Delay, - Schedule, - Waveform, - Drag, - Gaussian, - GaussianSquare, - Constant, -) -from qiskit.pulse import transforms, instructions -from qiskit.pulse.channels import ( - MemorySlot, - DriveChannel, - AcquireChannel, - RegisterSlot, - SnapshotChannel, -) -from qiskit.pulse.instructions import directives -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestAddImplicitAcquires(QiskitTestCase): - """Test the helper function which makes implicit acquires explicit.""" - - @ignore_pulse_deprecation_warnings - def test_multiple_acquires(self): - """Test for multiple acquires.""" - sched = pulse.Schedule() - acq_q0 = pulse.Acquire(1200, AcquireChannel(0), MemorySlot(0)) - sched += acq_q0 - sched += acq_q0 << sched.duration - sched = transforms.add_implicit_acquires(sched, meas_map=[[0]]) - self.assertEqual(sched.instructions, ((0, acq_q0), (2400, acq_q0))) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestPad(QiskitTestCase): - """Test padding of schedule with delays.""" - - def test_padding_empty_schedule(self): - """Test padding of empty schedule.""" - self.assertEqual(pulse.Schedule(), transforms.pad(pulse.Schedule())) - - def test_padding_schedule(self): - """Test padding schedule.""" - delay = 10 - sched = ( - Delay(delay, DriveChannel(0)).shift(10) - + Delay(delay, DriveChannel(0)).shift(10) - + Delay(delay, DriveChannel(1)).shift(10) - ) - - ref_sched = ( - sched # pylint: disable=unsupported-binary-operation - | Delay(delay, DriveChannel(0)) - | Delay(delay, DriveChannel(0)).shift(20) - | Delay(delay, DriveChannel(1)) - | Delay( # pylint: disable=unsupported-binary-operation - 2 * delay, DriveChannel(1) - ).shift(20) - ) - - self.assertEqual(transforms.pad(sched), ref_sched) - - def test_padding_schedule_inverse_order(self): - """Test padding schedule is insensitive to order in which commands were added. - - This test is the same as `test_adding_schedule` but the order by channel - in which commands were added to the schedule to be padded has been reversed. - """ - delay = 10 - sched = ( - Delay(delay, DriveChannel(1)).shift(10) - + Delay(delay, DriveChannel(0)).shift(10) - + Delay(delay, DriveChannel(0)).shift(10) - ) - - ref_sched = ( - sched # pylint: disable=unsupported-binary-operation - | Delay(delay, DriveChannel(0)) - | Delay(delay, DriveChannel(0)).shift(20) - | Delay(delay, DriveChannel(1)) - | Delay( # pylint: disable=unsupported-binary-operation - 2 * delay, DriveChannel(1) - ).shift(20) - ) - - self.assertEqual(transforms.pad(sched), ref_sched) - - def test_padding_until_less(self): - """Test padding until time that is less than schedule duration.""" - delay = 10 - - sched = Delay(delay, DriveChannel(0)).shift(10) + Delay(delay, DriveChannel(1)) - - ref_sched = sched | Delay(delay, DriveChannel(0)) | Delay(5, DriveChannel(1)).shift(10) - - self.assertEqual(transforms.pad(sched, until=15), ref_sched) - - def test_padding_until_greater(self): - """Test padding until time that is greater than schedule duration.""" - delay = 10 - - sched = Delay(delay, DriveChannel(0)).shift(10) + Delay(delay, DriveChannel(1)) - - ref_sched = ( - sched # pylint: disable=unsupported-binary-operation - | Delay(delay, DriveChannel(0)) - | Delay(30, DriveChannel(0)).shift(20) - | Delay(40, DriveChannel(1)).shift(10) # pylint: disable=unsupported-binary-operation - ) - - self.assertEqual(transforms.pad(sched, until=50), ref_sched) - - def test_padding_supplied_channels(self): - """Test padding of only specified channels.""" - delay = 10 - sched = Delay(delay, DriveChannel(0)).shift(10) + Delay(delay, DriveChannel(1)) - - ref_sched = sched | Delay(delay, DriveChannel(0)) | Delay(2 * delay, DriveChannel(2)) - - channels = [DriveChannel(0), DriveChannel(2)] - - self.assertEqual(transforms.pad(sched, channels=channels), ref_sched) - - def test_padding_less_than_sched_duration(self): - """Test that the until arg is respected even for less than the input schedule duration.""" - delay = 10 - sched = Delay(delay, DriveChannel(0)) + Delay(delay, DriveChannel(0)).shift(20) - ref_sched = sched | pulse.Delay(5, DriveChannel(0)).shift(10) - self.assertEqual(transforms.pad(sched, until=15), ref_sched) - - def test_padding_prepended_delay(self): - """Test that there is delay before the first instruction.""" - delay = 10 - sched = Delay(delay, DriveChannel(0)).shift(10) + Delay(delay, DriveChannel(0)) - - ref_sched = ( - Delay(delay, DriveChannel(0)) - + Delay(delay, DriveChannel(0)) - + Delay(delay, DriveChannel(0)) - ) - - self.assertEqual(transforms.pad(sched, until=30, inplace=True), ref_sched) - - def test_pad_no_delay_on_classical_io_channels(self): - """Test padding does not apply to classical IO channels.""" - delay = 10 - sched = ( - Delay(delay, MemorySlot(0)).shift(20) - + Delay(delay, RegisterSlot(0)).shift(10) - + Delay(delay, SnapshotChannel()) - ) - - ref_sched = ( - Delay(delay, MemorySlot(0)).shift(20) - + Delay(delay, RegisterSlot(0)).shift(10) - + Delay(delay, SnapshotChannel()) - ) - - self.assertEqual(transforms.pad(sched, until=15), ref_sched) - - -def get_pulse_ids(schedules: List[Schedule]) -> Set[int]: - """Returns ids of pulses used in Schedules.""" - ids = set() - for schedule in schedules: - for _, inst in schedule.instructions: - ids.add(inst.pulse.id) - return ids - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestCompressTransform(QiskitTestCase): - """Compress function test.""" - - def test_with_duplicates(self): - """Test compression of schedule.""" - schedule = Schedule() - drive_channel = DriveChannel(0) - schedule += Play(Waveform([0.0, 0.1]), drive_channel) - schedule += Play(Waveform([0.0, 0.1]), drive_channel) - - compressed_schedule = transforms.compress_pulses([schedule]) - original_pulse_ids = get_pulse_ids([schedule]) - compressed_pulse_ids = get_pulse_ids(compressed_schedule) - - self.assertEqual(len(compressed_pulse_ids), 1) - self.assertEqual(len(original_pulse_ids), 2) - self.assertTrue(next(iter(compressed_pulse_ids)) in original_pulse_ids) - - def test_sample_pulse_with_clipping(self): - """Test sample pulses with clipping.""" - schedule = Schedule() - drive_channel = DriveChannel(0) - schedule += Play(Waveform([0.0, 1.0]), drive_channel) - schedule += Play(Waveform([0.0, 1.001], epsilon=1e-3), drive_channel) - schedule += Play(Waveform([0.0, 1.0000000001]), drive_channel) - - compressed_schedule = transforms.compress_pulses([schedule]) - original_pulse_ids = get_pulse_ids([schedule]) - compressed_pulse_ids = get_pulse_ids(compressed_schedule) - - self.assertEqual(len(compressed_pulse_ids), 1) - self.assertEqual(len(original_pulse_ids), 3) - self.assertTrue(next(iter(compressed_pulse_ids)) in original_pulse_ids) - - def test_no_duplicates(self): - """Test with no pulse duplicates.""" - schedule = Schedule() - drive_channel = DriveChannel(0) - schedule += Play(Waveform([0.0, 1.0]), drive_channel) - schedule += Play(Waveform([0.0, 0.9]), drive_channel) - schedule += Play(Waveform([0.0, 0.3]), drive_channel) - - compressed_schedule = transforms.compress_pulses([schedule]) - original_pulse_ids = get_pulse_ids([schedule]) - compressed_pulse_ids = get_pulse_ids(compressed_schedule) - self.assertEqual(len(original_pulse_ids), len(compressed_pulse_ids)) - - def test_parametric_pulses_with_duplicates(self): - """Test with parametric pulses.""" - schedule = Schedule() - drive_channel = DriveChannel(0) - schedule += Play(Gaussian(duration=25, sigma=4, amp=0.5, angle=np.pi / 2), drive_channel) - schedule += Play(Gaussian(duration=25, sigma=4, amp=0.5, angle=np.pi / 2), drive_channel) - schedule += Play(GaussianSquare(duration=150, amp=0.2, sigma=8, width=140), drive_channel) - schedule += Play(GaussianSquare(duration=150, amp=0.2, sigma=8, width=140), drive_channel) - schedule += Play(Constant(duration=150, amp=0.5, angle=0.7), drive_channel) - schedule += Play(Constant(duration=150, amp=0.5, angle=0.7), drive_channel) - schedule += Play(Drag(duration=25, amp=0.4, angle=-0.3, sigma=7.8, beta=4), drive_channel) - schedule += Play(Drag(duration=25, amp=0.4, angle=-0.3, sigma=7.8, beta=4), drive_channel) - - compressed_schedule = transforms.compress_pulses([schedule]) - original_pulse_ids = get_pulse_ids([schedule]) - compressed_pulse_ids = get_pulse_ids(compressed_schedule) - self.assertEqual(len(original_pulse_ids), 8) - self.assertEqual(len(compressed_pulse_ids), 4) - - def test_parametric_pulses_with_no_duplicates(self): - """Test parametric pulses with no duplicates.""" - schedule = Schedule() - drive_channel = DriveChannel(0) - schedule += Play(Gaussian(duration=25, sigma=4, amp=0.5, angle=np.pi / 2), drive_channel) - schedule += Play(Gaussian(duration=25, sigma=4, amp=0.49, angle=np.pi / 2), drive_channel) - schedule += Play(GaussianSquare(duration=150, amp=0.2, sigma=8, width=140), drive_channel) - schedule += Play(GaussianSquare(duration=150, amp=0.19, sigma=8, width=140), drive_channel) - schedule += Play(Constant(duration=150, amp=0.5, angle=0.3), drive_channel) - schedule += Play(Constant(duration=150, amp=0.51, angle=0.3), drive_channel) - schedule += Play(Drag(duration=25, amp=0.5, angle=0.5, sigma=7.8, beta=4), drive_channel) - schedule += Play(Drag(duration=25, amp=0.5, angle=0.51, sigma=7.8, beta=4), drive_channel) - - compressed_schedule = transforms.compress_pulses([schedule]) - original_pulse_ids = get_pulse_ids([schedule]) - compressed_pulse_ids = get_pulse_ids(compressed_schedule) - self.assertEqual(len(original_pulse_ids), len(compressed_pulse_ids)) - - def test_with_different_channels(self): - """Test with different channels.""" - schedule = Schedule() - schedule += Play(Waveform([0.0, 0.1]), DriveChannel(0)) - schedule += Play(Waveform([0.0, 0.1]), DriveChannel(1)) - - compressed_schedule = transforms.compress_pulses([schedule]) - original_pulse_ids = get_pulse_ids([schedule]) - compressed_pulse_ids = get_pulse_ids(compressed_schedule) - self.assertEqual(len(original_pulse_ids), 2) - self.assertEqual(len(compressed_pulse_ids), 1) - - def test_sample_pulses_with_tolerance(self): - """Test sample pulses with tolerance.""" - schedule = Schedule() - schedule += Play(Waveform([0.0, 0.1001], epsilon=1e-3), DriveChannel(0)) - schedule += Play(Waveform([0.0, 0.1], epsilon=1e-3), DriveChannel(1)) - - compressed_schedule = transforms.compress_pulses([schedule]) - original_pulse_ids = get_pulse_ids([schedule]) - compressed_pulse_ids = get_pulse_ids(compressed_schedule) - self.assertEqual(len(original_pulse_ids), 2) - self.assertEqual(len(compressed_pulse_ids), 1) - - def test_multiple_schedules(self): - """Test multiple schedules.""" - schedules = [] - for _ in range(2): - schedule = Schedule() - drive_channel = DriveChannel(0) - schedule += Play(Waveform([0.0, 0.1]), drive_channel) - schedule += Play(Waveform([0.0, 0.1]), drive_channel) - schedule += Play(Waveform([0.0, 0.2]), drive_channel) - schedules.append(schedule) - - compressed_schedule = transforms.compress_pulses(schedules) - original_pulse_ids = get_pulse_ids(schedules) - compressed_pulse_ids = get_pulse_ids(compressed_schedule) - self.assertEqual(len(original_pulse_ids), 6) - self.assertEqual(len(compressed_pulse_ids), 2) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestAlignSequential(QiskitTestCase): - """Test sequential alignment transform.""" - - def test_align_sequential(self): - """Test sequential alignment without a barrier.""" - context = transforms.AlignSequential() - - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - - schedule = pulse.Schedule() - schedule.insert(1, instructions.Delay(3, d0), inplace=True) - schedule.insert(4, instructions.Delay(5, d1), inplace=True) - schedule.insert(12, instructions.Delay(7, d0), inplace=True) - schedule = context.align(schedule) - - reference = pulse.Schedule() - # d0 - reference.insert(0, instructions.Delay(3, d0), inplace=True) - reference.insert(8, instructions.Delay(7, d0), inplace=True) - # d1 - reference.insert(3, instructions.Delay(5, d1), inplace=True) - - self.assertEqual(schedule, reference) - - def test_align_sequential_with_barrier(self): - """Test sequential alignment with a barrier.""" - context = transforms.AlignSequential() - - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - - schedule = pulse.Schedule() - schedule.insert(1, instructions.Delay(3, d0), inplace=True) - schedule.append(directives.RelativeBarrier(d0, d1), inplace=True) - schedule.insert(4, instructions.Delay(5, d1), inplace=True) - schedule.insert(12, instructions.Delay(7, d0), inplace=True) - schedule = context.align(schedule) - - reference = pulse.Schedule() - reference.insert(0, instructions.Delay(3, d0), inplace=True) - reference.insert(3, directives.RelativeBarrier(d0, d1), inplace=True) - reference.insert(3, instructions.Delay(5, d1), inplace=True) - reference.insert(8, instructions.Delay(7, d0), inplace=True) - - self.assertEqual(schedule, reference) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestAlignLeft(QiskitTestCase): - """Test left alignment transform.""" - - def test_align_left(self): - """Test left alignment without a barrier.""" - context = transforms.AlignLeft() - - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - d2 = pulse.DriveChannel(2) - - schedule = pulse.Schedule() - schedule.insert(1, instructions.Delay(3, d0), inplace=True) - schedule.insert(17, instructions.Delay(11, d2), inplace=True) - - sched_grouped = pulse.Schedule() - sched_grouped += instructions.Delay(5, d1) - sched_grouped += instructions.Delay(7, d0) - schedule.append(sched_grouped, inplace=True) - schedule = context.align(schedule) - - reference = pulse.Schedule() - # d0 - reference.insert(0, instructions.Delay(3, d0), inplace=True) - reference.insert(3, instructions.Delay(7, d0), inplace=True) - # d1 - reference.insert(3, instructions.Delay(5, d1), inplace=True) - # d2 - reference.insert(0, instructions.Delay(11, d2), inplace=True) - - self.assertEqual(schedule, reference) - - def test_align_left_with_barrier(self): - """Test left alignment with a barrier.""" - context = transforms.AlignLeft() - - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - d2 = pulse.DriveChannel(2) - - schedule = pulse.Schedule() - schedule.insert(1, instructions.Delay(3, d0), inplace=True) - schedule.append(directives.RelativeBarrier(d0, d1, d2), inplace=True) - schedule.insert(17, instructions.Delay(11, d2), inplace=True) - - sched_grouped = pulse.Schedule() - sched_grouped += instructions.Delay(5, d1) - sched_grouped += instructions.Delay(7, d0) - schedule.append(sched_grouped, inplace=True) - schedule = transforms.remove_directives(context.align(schedule)) - - reference = pulse.Schedule() - # d0 - reference.insert(0, instructions.Delay(3, d0), inplace=True) - reference.insert(3, instructions.Delay(7, d0), inplace=True) - # d1 - reference = reference.insert(3, instructions.Delay(5, d1)) - # d2 - reference = reference.insert(3, instructions.Delay(11, d2)) - - self.assertEqual(schedule, reference) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestAlignRight(QiskitTestCase): - """Test right alignment transform.""" - - def test_align_right(self): - """Test right alignment without a barrier.""" - context = transforms.AlignRight() - - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - d2 = pulse.DriveChannel(2) - - schedule = pulse.Schedule() - schedule.insert(1, instructions.Delay(3, d0), inplace=True) - schedule.insert(17, instructions.Delay(11, d2), inplace=True) - - sched_grouped = pulse.Schedule() - sched_grouped.insert(2, instructions.Delay(5, d1), inplace=True) - sched_grouped += instructions.Delay(7, d0) - - schedule.append(sched_grouped, inplace=True) - schedule = context.align(schedule) - - reference = pulse.Schedule() - # d0 - reference.insert(1, instructions.Delay(3, d0), inplace=True) - reference.insert(4, instructions.Delay(7, d0), inplace=True) - # d1 - reference.insert(6, instructions.Delay(5, d1), inplace=True) - # d2 - reference.insert(0, instructions.Delay(11, d2), inplace=True) - self.assertEqual(schedule, reference) - - def test_align_right_with_barrier(self): - """Test right alignment with a barrier.""" - context = transforms.AlignRight() - - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - d2 = pulse.DriveChannel(2) - - schedule = pulse.Schedule() - schedule.insert(1, instructions.Delay(3, d0), inplace=True) - schedule.append(directives.RelativeBarrier(d0, d1, d2), inplace=True) - schedule.insert(17, instructions.Delay(11, d2), inplace=True) - - sched_grouped = pulse.Schedule() - sched_grouped.insert(2, instructions.Delay(5, d1), inplace=True) - sched_grouped += instructions.Delay(7, d0) - - schedule.append(sched_grouped, inplace=True) - schedule = transforms.remove_directives(context.align(schedule)) - - reference = pulse.Schedule() - # d0 - reference.insert(0, instructions.Delay(3, d0), inplace=True) - reference.insert(7, instructions.Delay(7, d0), inplace=True) - # d1 - reference.insert(9, instructions.Delay(5, d1), inplace=True) - # d2 - reference.insert(3, instructions.Delay(11, d2), inplace=True) - - self.assertEqual(schedule, reference) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestAlignEquispaced(QiskitTestCase): - """Test equispaced alignment transform.""" - - def test_equispaced_with_short_duration(self): - """Test equispaced context with duration shorter than the schedule duration.""" - context = transforms.AlignEquispaced(duration=20) - - d0 = pulse.DriveChannel(0) - - schedule = pulse.Schedule() - for _ in range(3): - schedule.append(Delay(10, d0), inplace=True) - schedule = context.align(schedule) - - 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) - - self.assertEqual(schedule, reference) - - def test_equispaced_with_longer_duration(self): - """Test equispaced context with duration longer than the schedule duration.""" - context = transforms.AlignEquispaced(duration=50) - - d0 = pulse.DriveChannel(0) - - schedule = pulse.Schedule() - for _ in range(3): - schedule.append(Delay(10, d0), inplace=True) - schedule = context.align(schedule) - - reference = pulse.Schedule() - reference.insert(0, Delay(10, d0), inplace=True) - reference.insert(20, Delay(10, d0), inplace=True) - reference.insert(40, Delay(10, d0), inplace=True) - - self.assertEqual(schedule, reference) - - def test_equispaced_with_multiple_channels_short_duration(self): - """Test equispaced context with multiple channels and duration shorter than the total - duration.""" - context = transforms.AlignEquispaced(duration=20) - - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - - schedule = pulse.Schedule() - schedule.append(Delay(10, d0), inplace=True) - schedule.append(Delay(20, d1), inplace=True) - schedule = context.align(schedule) - - reference = pulse.Schedule() - reference.insert(0, Delay(10, d0), inplace=True) - reference.insert(0, Delay(20, d1), inplace=True) - - self.assertEqual(schedule, reference) - - def test_equispaced_with_multiple_channels_longer_duration(self): - """Test equispaced context with multiple channels and duration longer than the total - duration.""" - context = transforms.AlignEquispaced(duration=30) - - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - - schedule = pulse.Schedule() - schedule.append(Delay(10, d0), inplace=True) - schedule.append(Delay(20, d1), inplace=True) - schedule = context.align(schedule) - - reference = pulse.Schedule() - reference.insert(0, Delay(10, d0), inplace=True) - reference.insert(10, Delay(20, d1), inplace=True) - - self.assertEqual(schedule, reference) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestAlignFunc(QiskitTestCase): - """Test callback alignment transform.""" - - @staticmethod - def _position(ind): - """Returns 0.25, 0.5, 0.75 for ind = 1, 2, 3.""" - return ind / (3 + 1) - - def test_numerical_with_short_duration(self): - """Test numerical alignment context with duration shorter than the schedule duration.""" - context = transforms.AlignFunc(duration=20, func=self._position) - - d0 = pulse.DriveChannel(0) - - schedule = pulse.Schedule() - for _ in range(3): - schedule.append(Delay(10, d0), inplace=True) - schedule = context.align(schedule) - - 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) - - self.assertEqual(schedule, reference) - - def test_numerical_with_longer_duration(self): - """Test numerical alignment context with duration longer than the schedule duration.""" - context = transforms.AlignFunc(duration=80, func=self._position) - - d0 = pulse.DriveChannel(0) - - schedule = pulse.Schedule() - for _ in range(3): - schedule.append(Delay(10, d0), inplace=True) - schedule = context.align(schedule) - - reference = pulse.Schedule() - reference.insert(15, Delay(10, d0), inplace=True) - reference.insert(35, Delay(10, d0), inplace=True) - reference.insert(55, Delay(10, d0), inplace=True) - - self.assertEqual(schedule, reference) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestFlatten(QiskitTestCase): - """Test flattening transform.""" - - def test_flatten(self): - """Test the flatten transform.""" - context_left = transforms.AlignLeft() - - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - - schedule = pulse.Schedule() - schedule += instructions.Delay(3, d0) - - grouped = pulse.Schedule() - grouped += instructions.Delay(5, d1) - grouped += instructions.Delay(7, d0) - # include a grouped schedule - grouped = schedule + grouped - - # flatten the schedule inline internal groups - flattened = transforms.flatten(grouped) - - # align all the instructions to the left after flattening - flattened = context_left.align(flattened) - grouped = context_left.align(grouped) - - reference = pulse.Schedule() - # d0 - reference.insert(0, instructions.Delay(3, d0), inplace=True) - reference.insert(3, instructions.Delay(7, d0), inplace=True) - # d1 - reference.insert(0, instructions.Delay(5, d1), inplace=True) - - self.assertEqual(flattened, reference) - self.assertNotEqual(grouped, reference) - - -class _TestDirective(directives.Directive): - """Pulse ``RelativeBarrier`` directive.""" - - def __init__(self, *channels): - """Test directive""" - super().__init__(operands=tuple(channels)) - - @property - def channels(self): - return self.operands - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestRemoveDirectives(QiskitTestCase): - """Test removing of directives.""" - - def test_remove_directives(self): - """Test that all directives are removed.""" - d0 = pulse.DriveChannel(0) - d1 = pulse.DriveChannel(1) - - schedule = pulse.Schedule() - schedule += _TestDirective(d0, d1) - schedule += instructions.Delay(3, d0) - schedule += _TestDirective(d0, d1) - schedule = transforms.remove_directives(schedule) - - reference = pulse.Schedule() - # d0 - reference += instructions.Delay(3, d0) - self.assertEqual(schedule, reference) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestRemoveTrivialBarriers(QiskitTestCase): - """Test scheduling transforms.""" - - def test_remove_trivial_barriers(self): - """Test that trivial barriers are properly removed.""" - schedule = pulse.Schedule() - schedule += directives.RelativeBarrier() - schedule += directives.RelativeBarrier(pulse.DriveChannel(0)) - schedule += directives.RelativeBarrier(pulse.DriveChannel(0), pulse.DriveChannel(1)) - schedule = transforms.remove_trivial_barriers(schedule) - - reference = pulse.Schedule() - reference += directives.RelativeBarrier(pulse.DriveChannel(0), pulse.DriveChannel(1)) - self.assertEqual(schedule, reference) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/transpiler/test_normalize_rx_angle.py b/test/python/transpiler/test_normalize_rx_angle.py deleted file mode 100644 index 6f1c7f22ea76..000000000000 --- a/test/python/transpiler/test_normalize_rx_angle.py +++ /dev/null @@ -1,138 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2023, 2024. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test the NormalizeRXAngle pass""" - -import unittest -import numpy as np -from ddt import ddt, named_data - -from qiskit import QuantumCircuit -from qiskit.transpiler.passes.optimization.normalize_rx_angle import ( - NormalizeRXAngle, -) -from qiskit.providers.fake_provider import GenericBackendV2 -from qiskit.transpiler import Target -from qiskit.circuit.library.standard_gates import SXGate -from test import QiskitTestCase # pylint: disable=wrong-import-order - - -@ddt -class TestNormalizeRXAngle(QiskitTestCase): - """Tests the NormalizeRXAngle pass.""" - - def test_not_convert_to_x_if_no_calib_in_target(self): - """Check that RX(pi) is NOT converted to X, - if X calibration is not present in the target""" - empty_target = Target() - tp = NormalizeRXAngle(target=empty_target) - - qc = QuantumCircuit(1) - qc.rx(90, 0) - - transpiled_circ = tp(qc) - self.assertEqual(transpiled_circ.count_ops().get("x", 0), 0) - - def test_sx_conversion_works(self): - """Check that RX(pi/2) is converted to SX, - if SX calibration is present in the target""" - target = Target() - target.add_instruction(SXGate(), properties={(0,): None}) - tp = NormalizeRXAngle(target=target) - - qc = QuantumCircuit(1) - qc.rx(np.pi / 2, 0) - - transpiled_circ = tp(qc) - self.assertEqual(transpiled_circ.count_ops().get("sx", 0), 1) - - def test_rz_added_for_negative_rotation_angles(self): - """Check that RZ is added before and after RX, - if RX rotation angle is negative""" - - backend = GenericBackendV2(num_qubits=5) - tp = NormalizeRXAngle(target=backend.target) - - # circuit to transpile and test - qc = QuantumCircuit(1) - qc.rx((-1 / 3) * np.pi, 0) - transpiled_circ = tp(qc) - - # circuit to show the correct answer - qc_ref = QuantumCircuit(1) - qc_ref.rz(np.pi, 0) - qc_ref.rx(np.pi / 3, 0) - qc_ref.rz(-np.pi, 0) - - self.assertQuantumCircuitEqual(transpiled_circ, qc_ref) - - @named_data( - {"name": "-0.3pi", "raw_theta": -0.3 * np.pi, "correct_wrapped_theta": 0.3 * np.pi}, - {"name": "1.7pi", "raw_theta": 1.7 * np.pi, "correct_wrapped_theta": 0.3 * np.pi}, - {"name": "2.2pi", "raw_theta": 2.2 * np.pi, "correct_wrapped_theta": 0.2 * np.pi}, - ) - def test_angle_wrapping_works(self, raw_theta, correct_wrapped_theta): - """Check that RX rotation angles are correctly wrapped to [0, pi]""" - backend = GenericBackendV2(num_qubits=5) - tp = NormalizeRXAngle(target=backend.target) - - # circuit to transpile and test - qc = QuantumCircuit(1) - qc.rx(raw_theta, 0) - - transpiled_circuit = tp(qc) - wrapped_theta = transpiled_circuit.get_instructions("rx")[0].operation.params[0] - self.assertAlmostEqual(wrapped_theta, correct_wrapped_theta) - - @named_data( - { - "name": "angles are within resolution", - "resolution": 0.1, - "rx_angles": [0.3, 0.303], - "correct_num_of_cals": 1, - }, - { - "name": "angles are not within resolution", - "resolution": 0.1, - "rx_angles": [0.2, 0.4], - "correct_num_of_cals": 2, - }, - { - "name": "same angle three times", - "resolution": 0.1, - "rx_angles": [0.2, 0.2, 0.2], - "correct_num_of_cals": 1, - }, - ) - def test_quantize_angles(self, resolution, rx_angles, correct_num_of_cals): - """Test that quantize_angles() adds a new calibration only if - the requested angle is not in the vicinity of the already generated angles. - """ - backend = GenericBackendV2(num_qubits=5) - tp = NormalizeRXAngle(backend.target, resolution_in_radian=resolution) - - qc = QuantumCircuit(1) - for rx_angle in rx_angles: - qc.rx(rx_angle, 0) - transpiled_circuit = tp(qc) - - angles = [ - inst.operation.params[0] - for inst in transpiled_circuit.data - if inst.operation.name == "rx" - ] - angles_without_duplicate = list(dict.fromkeys(angles)) - self.assertEqual(len(angles_without_duplicate), correct_num_of_cals) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/python/transpiler/test_template_matching.py b/test/python/transpiler/test_template_matching.py index d7c4baa18fd9..56c6c07f9f4a 100644 --- a/test/python/transpiler/test_template_matching.py +++ b/test/python/transpiler/test_template_matching.py @@ -34,7 +34,7 @@ from qiskit.converters.circuit_to_dagdependency import circuit_to_dagdependency from qiskit.transpiler import PassManager from qiskit.transpiler.passes import TemplateOptimization -from qiskit.transpiler.passes.calibration.rzx_templates import rzx_templates +from qiskit.circuit.library.templates import rzx from qiskit.transpiler.exceptions import TranspilerError from test.python.quantum_info.operators.symplectic.test_clifford import ( # pylint: disable=wrong-import-order random_clifford_circuit, @@ -428,7 +428,11 @@ def test_unbound_parameters_in_rzx_template(self): circuit_in.p(2 * theta, 1) circuit_in.cx(0, 1) - pass_ = TemplateOptimization(**rzx_templates(["zz2"])) + pass_ = TemplateOptimization( + template_list=[rzx.rzx_zz2()], + user_cost_dict={"rzx": 0, "cx": 6, "rz": 0, "sx": 1, "p": 0, "h": 1, "rx": 1, "ry": 1}, + ) + circuit_out = PassManager(pass_).run(circuit_in) # these are NOT equal if template optimization works @@ -444,7 +448,7 @@ def test_unbound_parameters_in_rzx_template(self): def test_two_parameter_template(self): """ - Test a two-Parameter template based on rzx_templates(["zz3"]), + Test a two-Parameter template based on rzx.rzx_zz3() ┌───┐┌───────┐┌───┐┌────────────┐» q_0: ──■─────────────■──┤ X ├┤ Rz(φ) ├┤ X ├┤ Rz(-1.0*φ) ├» diff --git a/test/python/utils/test_parallel.py b/test/python/utils/test_parallel.py index c160d7d62298..81dd3340e622 100644 --- a/test/python/utils/test_parallel.py +++ b/test/python/utils/test_parallel.py @@ -16,12 +16,10 @@ import sys import tempfile import time -import warnings from unittest import mock from qiskit.utils import local_hardware_info, should_run_in_parallel, parallel_map from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit -from qiskit.pulse import Schedule from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -38,13 +36,6 @@ def _build_simple_circuit(_): return qc -def _build_simple_schedule(_): - with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=DeprecationWarning) - # `Schedule` is deprecated in Qiskit 1.3 - return Schedule() - - class TestParallel(QiskitTestCase): """A class for testing parallel_map functionality.""" @@ -59,12 +50,6 @@ def test_parallel_circuit_names(self): names = [circ.name for circ in out_circs] self.assertEqual(len(names), len(set(names))) - def test_parallel_schedule_names(self): - """Verify unique schedule names in parallel""" - out_schedules = parallel_map(_build_simple_schedule, list(range(10))) - names = [schedule.name for schedule in out_schedules] - self.assertEqual(len(names), len(set(names))) - class TestUtilities(QiskitTestCase): """Tests for parallel utilities.""" diff --git a/test/python/visualization/pulse_v2/__init__.py b/test/python/visualization/pulse_v2/__init__.py deleted file mode 100644 index ab9be8d4d857..000000000000 --- a/test/python/visualization/pulse_v2/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Test for pulse visualization modules.""" diff --git a/test/python/visualization/pulse_v2/test_core.py b/test/python/visualization/pulse_v2/test_core.py deleted file mode 100644 index 8c677e554dd9..000000000000 --- a/test/python/visualization/pulse_v2/test_core.py +++ /dev/null @@ -1,387 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -# pylint: disable=missing-function-docstring, unused-argument - -"""Tests for core modules of pulse drawer.""" - -import numpy as np -from qiskit import pulse -from qiskit.visualization.exceptions import VisualizationError -from qiskit.visualization.pulse_v2 import core, stylesheet, device_info, drawings, types, layouts -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestChart(QiskitTestCase): - """Tests for chart.""" - - @ignore_pulse_deprecation_warnings - def setUp(self) -> None: - super().setUp() - - self.style = stylesheet.QiskitPulseStyle() - self.device = device_info.OpenPulseBackendInfo( - name="test", - dt=1, - channel_frequency_map={ - pulse.DriveChannel(0): 5.0, - pulse.MeasureChannel(0): 7.0, - pulse.ControlChannel(0): 5.0, - }, - qubit_channel_map={ - 0: [ - pulse.DriveChannel(0), - pulse.MeasureChannel(0), - pulse.AcquireChannel(0), - pulse.ControlChannel(0), - ] - }, - ) - - # objects - self.short_pulse = drawings.LineData( - data_type=types.WaveformType.REAL, - xvals=[0, 0, 1, 4, 5, 5], - yvals=[0, 0.5, 0.5, 0.5, 0.5, 0], - channels=[pulse.DriveChannel(0)], - ) - self.long_pulse = drawings.LineData( - data_type=types.WaveformType.REAL, - xvals=[8, 8, 9, 19, 20, 20], - yvals=[0, 0.3, 0.3, 0.3, 0.3, 0], - channels=[pulse.DriveChannel(1)], - ) - self.abstract_hline = drawings.LineData( - data_type=types.LineType.BASELINE, - xvals=[types.AbstractCoordinate.LEFT, types.AbstractCoordinate.RIGHT], - yvals=[0, 0], - channels=[pulse.DriveChannel(0)], - ) - - def test_add_data(self): - """Test add data to chart.""" - fake_canvas = core.DrawerCanvas(stylesheet=self.style, device=self.device) - chart = core.Chart(parent=fake_canvas) - - chart.add_data(self.short_pulse) - self.assertEqual(len(chart._collections), 1) - - # the same pulse will be overwritten - chart.add_data(self.short_pulse) - self.assertEqual(len(chart._collections), 1) - - chart.add_data(self.long_pulse) - self.assertEqual(len(chart._collections), 2) - - def test_bind_coordinate(self): - """Test bind coordinate.""" - fake_canvas = core.DrawerCanvas(stylesheet=self.style, device=self.device) - fake_canvas.formatter = {"margin.left_percent": 0.1, "margin.right_percent": 0.1} - fake_canvas.time_range = (500, 2000) - - chart = core.Chart(parent=fake_canvas) - chart.vmin = -0.1 - chart.vmax = 0.5 - - # vertical - vline = [types.AbstractCoordinate.BOTTOM, types.AbstractCoordinate.TOP] - vals = chart._bind_coordinate(vline) - np.testing.assert_array_equal(vals, np.array([-0.1, 0.5])) - - # horizontal, margin is is considered - hline = [types.AbstractCoordinate.LEFT, types.AbstractCoordinate.RIGHT] - vals = chart._bind_coordinate(hline) - np.testing.assert_array_equal(vals, np.array([350.0, 2150.0])) - - def test_truncate(self): - """Test pulse truncation.""" - fake_canvas = core.DrawerCanvas(stylesheet=self.style, device=self.device) - fake_canvas.formatter = { - "margin.left_percent": 0, - "margin.right_percent": 0, - "axis_break.length": 20, - "axis_break.max_length": 10, - } - fake_canvas.time_range = (0, 20) - fake_canvas.time_breaks = [(5, 10)] - - chart = core.Chart(parent=fake_canvas) - - xvals = np.array([4, 5, 6, 7, 8, 9, 10, 11]) - yvals = np.array([1, 2, 3, 4, 5, 6, 7, 8]) - - new_xvals, new_yvals = chart._truncate_vectors(xvals, yvals) - - ref_xvals = np.array([4.0, 5.0, 5.0, 6.0]) - ref_yvals = np.array([1.0, 2.0, 7.0, 8.0]) - - np.testing.assert_array_almost_equal(new_xvals, ref_xvals) - np.testing.assert_array_almost_equal(new_yvals, ref_yvals) - - def test_truncate_multiple(self): - """Test pulse truncation.""" - fake_canvas = core.DrawerCanvas(stylesheet=self.style, device=self.device) - fake_canvas.formatter = { - "margin.left_percent": 0, - "margin.right_percent": 0, - "axis_break.length": 20, - "axis_break.max_length": 10, - } - fake_canvas.time_range = (2, 12) - fake_canvas.time_breaks = [(4, 7), (9, 11)] - - chart = core.Chart(parent=fake_canvas) - - xvals = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]) - yvals = np.array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) - - new_xvals, new_yvals = chart._truncate_vectors(xvals, yvals) - - ref_xvals = np.array([2.0, 3.0, 4.0, 4.0, 5.0, 6.0, 6.0, 7.0]) - ref_yvals = np.array([1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]) - - np.testing.assert_array_almost_equal(new_xvals, ref_xvals) - np.testing.assert_array_almost_equal(new_yvals, ref_yvals) - - def test_visible(self): - """Test pulse truncation.""" - fake_canvas = core.DrawerCanvas(stylesheet=self.style, device=self.device) - fake_canvas.disable_chans = {pulse.DriveChannel(0)} - fake_canvas.disable_types = {types.WaveformType.REAL} - - chart = core.Chart(parent=fake_canvas) - - test_data = drawings.ElementaryData( - data_type=types.WaveformType.REAL, - xvals=np.array([0]), - yvals=np.array([0]), - channels=[pulse.DriveChannel(0)], - ) - self.assertFalse(chart._check_visible(test_data)) - - test_data = drawings.ElementaryData( - data_type=types.WaveformType.IMAG, - xvals=np.array([0]), - yvals=np.array([0]), - channels=[pulse.DriveChannel(0)], - ) - self.assertFalse(chart._check_visible(test_data)) - - test_data = drawings.ElementaryData( - data_type=types.WaveformType.IMAG, - xvals=np.array([0]), - yvals=np.array([0]), - channels=[pulse.DriveChannel(1)], - ) - self.assertTrue(chart._check_visible(test_data)) - - def test_update(self): - fake_canvas = core.DrawerCanvas(stylesheet=self.style, device=self.device) - fake_canvas.formatter = { - "margin.left_percent": 0, - "margin.right_percent": 0, - "axis_break.length": 20, - "axis_break.max_length": 10, - "control.auto_chart_scaling": True, - "general.vertical_resolution": 1e-6, - "general.max_scale": 10, - "channel_scaling.pos_spacing": 0.1, - "channel_scaling.neg_spacing": -0.1, - } - fake_canvas.time_range = (0, 20) - fake_canvas.time_breaks = [(10, 15)] - - chart = core.Chart(fake_canvas) - chart.add_data(self.short_pulse) - chart.add_data(self.long_pulse) - chart.add_data(self.abstract_hline) - chart.update() - - short_pulse = chart._output_dataset[self.short_pulse.data_key] - xref = np.array([0.0, 0.0, 1.0, 4.0, 5.0, 5.0]) - yref = np.array([0.0, 0.5, 0.5, 0.5, 0.5, 0.0]) - np.testing.assert_array_almost_equal(xref, short_pulse.xvals) - np.testing.assert_array_almost_equal(yref, short_pulse.yvals) - - long_pulse = chart._output_dataset[self.long_pulse.data_key] - xref = np.array([8.0, 8.0, 9.0, 10.0, 10.0, 14.0, 15.0, 15.0]) - yref = np.array([0.0, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.0]) - np.testing.assert_array_almost_equal(xref, long_pulse.xvals) - np.testing.assert_array_almost_equal(yref, long_pulse.yvals) - - abstract_hline = chart._output_dataset[self.abstract_hline.data_key] - xref = np.array([0.0, 10.0, 10.0, 15.0]) - yref = np.array([0.0, 0.0, 0.0, 0.0]) - np.testing.assert_array_almost_equal(xref, abstract_hline.xvals) - np.testing.assert_array_almost_equal(yref, abstract_hline.yvals) - - self.assertEqual(chart.vmax, 1.0) - self.assertEqual(chart.vmin, -0.1) - self.assertEqual(chart.scale, 2.0) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestDrawCanvas(QiskitTestCase): - """Tests for draw canvas.""" - - @ignore_pulse_deprecation_warnings - def setUp(self) -> None: - super().setUp() - self.style = stylesheet.QiskitPulseStyle() - self.device = device_info.OpenPulseBackendInfo( - name="test", - dt=1, - channel_frequency_map={ - pulse.DriveChannel(0): 5.0, - pulse.MeasureChannel(0): 7.0, - pulse.ControlChannel(0): 5.0, - }, - qubit_channel_map={ - 0: [ - pulse.DriveChannel(0), - pulse.MeasureChannel(0), - pulse.AcquireChannel(0), - pulse.ControlChannel(0), - ] - }, - ) - - self.sched = pulse.Schedule() - self.sched.insert( - 0, - pulse.Play(pulse.Waveform([0.0, 0.1, 0.2, 0.3, 0.4, 0.5]), pulse.DriveChannel(0)), - inplace=True, - ) - self.sched.insert( - 10, - pulse.Play(pulse.Waveform([0.5, 0.4, 0.3, 0.2, 0.1, 0.0]), pulse.DriveChannel(0)), - inplace=True, - ) - self.sched.insert( - 0, - pulse.Play(pulse.Waveform([0.3, 0.3, 0.3, 0.3, 0.3, 0.3]), pulse.DriveChannel(1)), - inplace=True, - ) - - def test_time_breaks(self): - """Test calculating time breaks.""" - canvas = core.DrawerCanvas(stylesheet=self.style, device=self.device) - canvas.formatter = { - "margin.left_percent": 0, - "margin.right_percent": 0, - "axis_break.length": 20, - "axis_break.max_length": 10, - } - canvas.layout = {"figure_title": layouts.empty_title} - canvas.time_breaks = [(10, 40), (60, 80)] - - canvas.time_range = (0, 100) - ref_breaks = [(10, 40), (60, 80)] - self.assertListEqual(canvas.time_breaks, ref_breaks) - - # break too large - canvas.time_range = (20, 30) - with self.assertRaises(VisualizationError): - _ = canvas.time_breaks - - # time range overlap - canvas.time_range = (15, 100) - ref_breaks = [(20, 40), (60, 80)] - self.assertListEqual(canvas.time_breaks, ref_breaks) - - # time range overlap - canvas.time_range = (30, 100) - ref_breaks = [(60, 80)] - self.assertListEqual(canvas.time_breaks, ref_breaks) - - # time range overlap - canvas.time_range = (0, 70) - ref_breaks = [(10, 40)] - self.assertListEqual(canvas.time_breaks, ref_breaks) - - # time range no overlap - canvas.time_range = (40, 60) - ref_breaks = [] - self.assertListEqual(canvas.time_breaks, ref_breaks) - - def test_time_range(self): - """Test calculating time range.""" - canvas = core.DrawerCanvas(stylesheet=self.style, device=self.device) - canvas.formatter = { - "margin.left_percent": 0.1, - "margin.right_percent": 0.1, - "axis_break.length": 20, - "axis_break.max_length": 10, - } - canvas.layout = {"figure_title": layouts.empty_title} - canvas.time_range = (0, 100) - - # no breaks - canvas.time_breaks = [] - ref_range = [-10.0, 110.0] - self.assertListEqual(list(canvas.time_range), ref_range) - - # with break - canvas.time_breaks = [(20, 40)] - ref_range = [-8.0, 108.0] - self.assertListEqual(list(canvas.time_range), ref_range) - - def chart_channel_map(self, **kwargs): - """Mock of chart channel mapper.""" - names = ["D0", "D1"] - chans = [[pulse.DriveChannel(0)], [pulse.DriveChannel(1)]] - - yield from zip(names, chans) - - def generate_dummy_obj(self, data: types.PulseInstruction, **kwargs): - dummy_obj = drawings.ElementaryData( - data_type="test", - xvals=np.arange(data.inst.pulse.duration), - yvals=data.inst.pulse.samples, - channels=[data.inst.channel], - ) - return [dummy_obj] - - def test_load_program(self): - """Test loading program.""" - canvas = core.DrawerCanvas(stylesheet=self.style, device=self.device) - canvas.formatter = { - "axis_break.length": 20, - "axis_break.max_length": 10, - "channel_scaling.drive": 5, - } - canvas.generator = { - "waveform": [self.generate_dummy_obj], - "frame": [], - "chart": [], - "snapshot": [], - "barrier": [], - } - canvas.layout = { - "chart_channel_map": self.chart_channel_map, - "figure_title": layouts.empty_title, - } - - canvas.load_program(self.sched) - - self.assertEqual(len(canvas.charts), 2) - - self.assertListEqual(canvas.charts[0].channels, [pulse.DriveChannel(0)]) - self.assertListEqual(canvas.charts[1].channels, [pulse.DriveChannel(1)]) - - self.assertEqual(len(canvas.charts[0]._collections), 2) - self.assertEqual(len(canvas.charts[1]._collections), 1) - - ref_scale = {pulse.DriveChannel(0): 5, pulse.DriveChannel(1): 5} - self.assertDictEqual(canvas.chan_scales, ref_scale) diff --git a/test/python/visualization/pulse_v2/test_drawings.py b/test/python/visualization/pulse_v2/test_drawings.py deleted file mode 100644 index d0766fc4f9bb..000000000000 --- a/test/python/visualization/pulse_v2/test_drawings.py +++ /dev/null @@ -1,210 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Tests for core modules of pulse drawer.""" - -from qiskit import pulse -from qiskit.visualization.pulse_v2 import drawings, types -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestDrawingObjects(QiskitTestCase): - """Tests for DrawingObjects.""" - - @ignore_pulse_deprecation_warnings - def setUp(self) -> None: - """Setup.""" - super().setUp() - - # bits - self.ch_d0 = [pulse.DriveChannel(0)] - self.ch_d1 = [pulse.DriveChannel(1)] - - # metadata - self.meta1 = {"val1": 0, "val2": 1} - self.meta2 = {"val1": 2, "val2": 3} - - # style data - self.style1 = {"property1": 0, "property2": 1} - self.style2 = {"property1": 2, "property2": 3} - - def test_line_data_equivalent(self): - """Test for LineData.""" - xs = [0, 1, 2] - ys = [3, 4, 5] - - data1 = drawings.LineData( - data_type="test", - xvals=xs, - yvals=ys, - channels=self.ch_d0, - meta=self.meta1, - ignore_scaling=True, - styles=self.style1, - ) - - data2 = drawings.LineData( - data_type="test", - xvals=xs, - yvals=ys, - channels=self.ch_d1, - meta=self.meta2, - ignore_scaling=True, - styles=self.style2, - ) - - self.assertEqual(data1, data2) - - def test_line_data_equivalent_with_abstract_coordinate(self): - """Test for LineData with abstract coordinate.""" - xs = [types.AbstractCoordinate.LEFT, types.AbstractCoordinate.RIGHT] - ys = [types.AbstractCoordinate.TOP, types.AbstractCoordinate.BOTTOM] - - data1 = drawings.LineData( - data_type="test", - xvals=xs, - yvals=ys, - channels=self.ch_d0, - meta=self.meta1, - ignore_scaling=True, - styles=self.style1, - ) - - data2 = drawings.LineData( - data_type="test", - xvals=xs, - yvals=ys, - channels=self.ch_d1, - meta=self.meta2, - ignore_scaling=True, - styles=self.style2, - ) - - self.assertEqual(data1, data2) - - def test_text_data(self): - """Test for TextData.""" - xs = [0] - ys = [1] - - data1 = drawings.TextData( - data_type="test", - xvals=xs, - yvals=ys, - text="test1", - latex=r"test_1", - channels=self.ch_d0, - meta=self.meta1, - ignore_scaling=True, - styles=self.style1, - ) - - data2 = drawings.TextData( - data_type="test", - xvals=xs, - yvals=ys, - text="test2", - latex=r"test_2", - channels=self.ch_d1, - meta=self.meta2, - ignore_scaling=True, - styles=self.style2, - ) - - self.assertEqual(data1, data2) - - def test_text_data_with_abstract_coordinate(self): - """Test for TextData with abstract coordinates.""" - xs = [types.AbstractCoordinate.RIGHT] - ys = [types.AbstractCoordinate.TOP] - - data1 = drawings.TextData( - data_type="test", - xvals=xs, - yvals=ys, - text="test1", - latex=r"test_1", - channels=self.ch_d0, - meta=self.meta1, - ignore_scaling=True, - styles=self.style1, - ) - - data2 = drawings.TextData( - data_type="test", - xvals=xs, - yvals=ys, - text="test2", - latex=r"test_2", - channels=self.ch_d1, - meta=self.meta2, - ignore_scaling=True, - styles=self.style2, - ) - - self.assertEqual(data1, data2) - - def test_box_data(self): - """Test for BoxData.""" - xs = [0, 1] - ys = [-1, 1] - - data1 = drawings.BoxData( - data_type="test", - xvals=xs, - yvals=ys, - channels=self.ch_d0, - meta=self.meta1, - ignore_scaling=True, - styles=self.style1, - ) - - data2 = drawings.BoxData( - data_type="test", - xvals=xs, - yvals=ys, - channels=self.ch_d1, - meta=self.meta2, - ignore_scaling=True, - styles=self.style2, - ) - - self.assertEqual(data1, data2) - - def test_box_data_with_abstract_coordinate(self): - """Test for BoxData with abstract coordinate.""" - xs = [types.AbstractCoordinate.LEFT, types.AbstractCoordinate.RIGHT] - ys = [types.AbstractCoordinate.BOTTOM, types.AbstractCoordinate.TOP] - - data1 = drawings.BoxData( - data_type="test", - xvals=xs, - yvals=ys, - channels=self.ch_d0, - meta=self.meta1, - ignore_scaling=True, - styles=self.style1, - ) - - data2 = drawings.BoxData( - data_type="test", - xvals=xs, - yvals=ys, - channels=self.ch_d1, - meta=self.meta2, - ignore_scaling=True, - styles=self.style2, - ) - - self.assertEqual(data1, data2) diff --git a/test/python/visualization/pulse_v2/test_events.py b/test/python/visualization/pulse_v2/test_events.py deleted file mode 100644 index 74fbf00f325c..000000000000 --- a/test/python/visualization/pulse_v2/test_events.py +++ /dev/null @@ -1,165 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Tests for core modules of pulse drawer.""" - -from qiskit import pulse, circuit -from qiskit.visualization.pulse_v2 import events -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestChannelEvents(QiskitTestCase): - """Tests for ChannelEvents.""" - - def test_parse_program(self): - """Test typical pulse program.""" - test_pulse = pulse.Gaussian(10, 0.1, 3) - - sched = pulse.Schedule() - sched = sched.insert(0, pulse.SetPhase(3.14, pulse.DriveChannel(0))) - sched = sched.insert(0, pulse.Play(test_pulse, pulse.DriveChannel(0))) - sched = sched.insert(10, pulse.ShiftPhase(-1.57, pulse.DriveChannel(0))) - sched = sched.insert(10, pulse.Play(test_pulse, pulse.DriveChannel(0))) - - ch_events = events.ChannelEvents.load_program(sched, pulse.DriveChannel(0)) - - # check waveform data - waveforms = list(ch_events.get_waveforms()) - inst_data0 = waveforms[0] - self.assertEqual(inst_data0.t0, 0) - self.assertEqual(inst_data0.frame.phase, 3.14) - self.assertEqual(inst_data0.frame.freq, 0) - self.assertEqual(inst_data0.inst, pulse.Play(test_pulse, pulse.DriveChannel(0))) - - inst_data1 = waveforms[1] - self.assertEqual(inst_data1.t0, 10) - self.assertEqual(inst_data1.frame.phase, 1.57) - self.assertEqual(inst_data1.frame.freq, 0) - self.assertEqual(inst_data1.inst, pulse.Play(test_pulse, pulse.DriveChannel(0))) - - # check frame data - frames = list(ch_events.get_frame_changes()) - inst_data0 = frames[0] - self.assertEqual(inst_data0.t0, 0) - self.assertEqual(inst_data0.frame.phase, 3.14) - self.assertEqual(inst_data0.frame.freq, 0) - self.assertListEqual(inst_data0.inst, [pulse.SetPhase(3.14, pulse.DriveChannel(0))]) - - inst_data1 = frames[1] - self.assertEqual(inst_data1.t0, 10) - self.assertEqual(inst_data1.frame.phase, -1.57) - self.assertEqual(inst_data1.frame.freq, 0) - self.assertListEqual(inst_data1.inst, [pulse.ShiftPhase(-1.57, pulse.DriveChannel(0))]) - - def test_multiple_frames_at_the_same_time(self): - """Test multiple frame instruction at the same time.""" - # shift phase followed by set phase - sched = pulse.Schedule() - sched = sched.insert(0, pulse.ShiftPhase(-1.57, pulse.DriveChannel(0))) - sched = sched.insert(0, pulse.SetPhase(3.14, pulse.DriveChannel(0))) - - ch_events = events.ChannelEvents.load_program(sched, pulse.DriveChannel(0)) - frames = list(ch_events.get_frame_changes()) - inst_data0 = frames[0] - self.assertAlmostEqual(inst_data0.frame.phase, 3.14) - - # set phase followed by shift phase - sched = pulse.Schedule() - sched = sched.insert(0, pulse.SetPhase(3.14, pulse.DriveChannel(0))) - sched = sched.insert(0, pulse.ShiftPhase(-1.57, pulse.DriveChannel(0))) - - ch_events = events.ChannelEvents.load_program(sched, pulse.DriveChannel(0)) - frames = list(ch_events.get_frame_changes()) - inst_data0 = frames[0] - self.assertAlmostEqual(inst_data0.frame.phase, 1.57) - - def test_frequency(self): - """Test parse frequency.""" - sched = pulse.Schedule() - sched = sched.insert(0, pulse.ShiftFrequency(1.0, pulse.DriveChannel(0))) - sched = sched.insert(5, pulse.SetFrequency(5.0, pulse.DriveChannel(0))) - - ch_events = events.ChannelEvents.load_program(sched, pulse.DriveChannel(0)) - ch_events.set_config(dt=0.1, init_frequency=3.0, init_phase=0) - frames = list(ch_events.get_frame_changes()) - - inst_data0 = frames[0] - self.assertAlmostEqual(inst_data0.frame.freq, 1.0) - - inst_data1 = frames[1] - self.assertAlmostEqual(inst_data1.frame.freq, 1.0) - - def test_parameterized_parametric_pulse(self): - """Test generating waveforms that are parameterized.""" - param = circuit.Parameter("amp") - - test_waveform = pulse.Play(pulse.Constant(10, param), pulse.DriveChannel(0)) - - ch_events = events.ChannelEvents( - waveforms={0: test_waveform}, frames={}, channel=pulse.DriveChannel(0) - ) - - pulse_inst = list(ch_events.get_waveforms())[0] - - self.assertTrue(pulse_inst.is_opaque) - self.assertEqual(pulse_inst.inst, test_waveform) - - def test_parameterized_frame_change(self): - """Test generating waveforms that are parameterized. - - Parameterized phase should be ignored when calculating waveform frame. - This is due to phase modulated representation of waveforms, - i.e. we cannot calculate the phase factor of waveform if the phase is unbound. - """ - param = circuit.Parameter("phase") - - test_fc1 = pulse.ShiftPhase(param, pulse.DriveChannel(0)) - test_fc2 = pulse.ShiftPhase(1.57, pulse.DriveChannel(0)) - test_waveform = pulse.Play(pulse.Constant(10, 0.1), pulse.DriveChannel(0)) - - ch_events = events.ChannelEvents( - waveforms={0: test_waveform}, - frames={0: [test_fc1, test_fc2]}, - channel=pulse.DriveChannel(0), - ) - - # waveform frame - pulse_inst = list(ch_events.get_waveforms())[0] - - self.assertFalse(pulse_inst.is_opaque) - self.assertEqual(pulse_inst.frame.phase, 1.57) - - # framechange - pulse_inst = list(ch_events.get_frame_changes())[0] - - self.assertTrue(pulse_inst.is_opaque) - self.assertEqual(pulse_inst.frame.phase, param + 1.57) - - def test_zero_duration_delay(self): - """Test generating waveforms that contains zero duration delay. - - Zero duration delay should be ignored. - """ - ch = pulse.DriveChannel(0) - - 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) - - self.assertEqual(len(list(ch_events.get_waveforms())), 4) diff --git a/test/python/visualization/pulse_v2/test_generators.py b/test/python/visualization/pulse_v2/test_generators.py deleted file mode 100644 index 749ec4ab3488..000000000000 --- a/test/python/visualization/pulse_v2/test_generators.py +++ /dev/null @@ -1,905 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -# pylint: disable=invalid-name - -"""Tests for drawing object generation.""" - -import numpy as np - -from qiskit import pulse, circuit -from qiskit.visualization.pulse_v2 import drawings, types, stylesheet, device_info -from qiskit.visualization.pulse_v2.generators import barrier, chart, frame, snapshot, waveform -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings - - -def create_instruction(inst, phase, freq, t0, dt, is_opaque=False): - """A helper function to create InstructionTuple.""" - frame_info = types.PhaseFreqTuple(phase=phase, freq=freq) - return types.PulseInstruction(t0=t0, dt=dt, frame=frame_info, inst=inst, is_opaque=is_opaque) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestWaveformGenerators(QiskitTestCase): - """Tests for waveform generators.""" - - @ignore_pulse_deprecation_warnings - def setUp(self) -> None: - super().setUp() - style = stylesheet.QiskitPulseStyle() - self.formatter = style.formatter - self.device = device_info.OpenPulseBackendInfo( - name="test", - dt=1, - channel_frequency_map={ - pulse.DriveChannel(0): 5.0e9, - pulse.DriveChannel(1): 5.1e9, - pulse.MeasureChannel(0): 7.0e9, - pulse.MeasureChannel(1): 7.1e9, - pulse.ControlChannel(0): 5.0e9, - pulse.ControlChannel(1): 5.1e9, - }, - qubit_channel_map={ - 0: [ - pulse.DriveChannel(0), - pulse.MeasureChannel(0), - pulse.AcquireChannel(0), - pulse.ControlChannel(0), - ], - 1: [ - pulse.DriveChannel(1), - pulse.MeasureChannel(1), - pulse.AcquireChannel(1), - pulse.ControlChannel(1), - ], - }, - ) - - def test_consecutive_index_all_equal(self): - """Test for helper function to find consecutive index with identical numbers.""" - vec = np.array([1, 1, 1, 1, 1, 1]) - ref_inds = np.array([True, False, False, False, False, True], dtype=bool) - - inds = waveform._find_consecutive_index(vec, resolution=1e-6) - - np.testing.assert_array_equal(inds, ref_inds) - - def test_consecutive_index_tiny_diff(self): - """Test for helper function to find consecutive index with vector with tiny change.""" - eps = 1e-10 - vec = np.array([0.5, 0.5 + eps, 0.5 - eps, 0.5 + eps, 0.5 - eps, 0.5]) - ref_inds = np.array([True, False, False, False, False, True], dtype=bool) - - inds = waveform._find_consecutive_index(vec, resolution=1e-6) - - np.testing.assert_array_equal(inds, ref_inds) - - def test_parse_waveform(self): - """Test helper function that parse waveform with Waveform instance.""" - test_pulse = pulse.library.Gaussian(10, 0.1, 3).get_waveform() - - inst = pulse.Play(test_pulse, pulse.DriveChannel(0)) - inst_data = create_instruction(inst, 0, 0, 10, 0.1) - - x, y, _ = waveform._parse_waveform(inst_data) - - x_ref = np.arange(10, 20) - y_ref = test_pulse.samples - - np.testing.assert_array_equal(x, x_ref) - np.testing.assert_array_equal(y, y_ref) - - def test_parse_waveform_parametric(self): - """Test helper function that parse waveform with ParametricPulse instance.""" - test_pulse = pulse.library.Gaussian(10, 0.1, 3) - - inst = pulse.Play(test_pulse, pulse.DriveChannel(0)) - inst_data = create_instruction(inst, 0, 0, 10, 0.1) - - x, y, _ = waveform._parse_waveform(inst_data) - - x_ref = np.arange(10, 20) - y_ref = test_pulse.get_waveform().samples - - np.testing.assert_array_equal(x, x_ref) - np.testing.assert_array_equal(y, y_ref) - - def test_gen_filled_waveform_stepwise_play(self): - """Test gen_filled_waveform_stepwise with play instruction.""" - my_pulse = pulse.Waveform(samples=[0, 0.5 + 0.5j, 0.5 + 0.5j, 0], name="my_pulse") - play = pulse.Play(my_pulse, pulse.DriveChannel(0)) - inst_data = create_instruction(play, np.pi / 2, 5e9, 5, 0.1) - - objs = waveform.gen_filled_waveform_stepwise( - inst_data, formatter=self.formatter, device=self.device - ) - - self.assertEqual(len(objs), 2) - - # type check - self.assertEqual(type(objs[0]), drawings.LineData) - self.assertEqual(type(objs[1]), drawings.LineData) - - y_ref = np.array([0, 0, -0.5, -0.5, 0, 0]) - - # data check - self.assertListEqual(objs[0].channels, [pulse.DriveChannel(0)]) - self.assertListEqual(list(objs[0].xvals), [5, 6, 6, 8, 8, 9]) - np.testing.assert_array_almost_equal(objs[0].yvals, y_ref) - - # meta data check - ref_meta = { - "duration (cycle time)": 4, - "duration (sec)": 0.4, - "t0 (cycle time)": 5, - "t0 (sec)": 0.5, - "phase": np.pi / 2, - "frequency": 5e9, - "qubit": 0, - "name": "my_pulse", - "data": "real", - } - self.assertDictEqual(objs[0].meta, ref_meta) - - # style check - ref_style = { - "alpha": self.formatter["alpha.fill_waveform"], - "zorder": self.formatter["layer.fill_waveform"], - "linewidth": self.formatter["line_width.fill_waveform"], - "linestyle": self.formatter["line_style.fill_waveform"], - "color": self.formatter["color.waveforms"]["D"][0], - } - self.assertDictEqual(objs[0].styles, ref_style) - - def test_gen_filled_waveform_stepwise_acquire(self): - """Test gen_filled_waveform_stepwise with acquire instruction.""" - acquire = pulse.Acquire( - duration=4, - channel=pulse.AcquireChannel(0), - mem_slot=pulse.MemorySlot(0), - discriminator=pulse.Discriminator(name="test_discr"), - name="acquire", - ) - inst_data = create_instruction(acquire, 0, 7e9, 5, 0.1) - - objs = waveform.gen_filled_waveform_stepwise( - inst_data, formatter=self.formatter, device=self.device - ) - - # imaginary part is empty and not returned - self.assertEqual(len(objs), 1) - - # type check - self.assertEqual(type(objs[0]), drawings.LineData) - - y_ref = np.array([1, 1]) - - # data check - data is compressed - self.assertListEqual(objs[0].channels, [pulse.AcquireChannel(0)]) - self.assertListEqual(list(objs[0].xvals), [5, 9]) - np.testing.assert_array_almost_equal(objs[0].yvals, y_ref) - - # meta data check - ref_meta = { - "memory slot": "m0", - "register slot": "N/A", - "discriminator": "test_discr", - "kernel": "N/A", - "duration (cycle time)": 4, - "duration (sec)": 0.4, - "t0 (cycle time)": 5, - "t0 (sec)": 0.5, - "phase": 0, - "frequency": 7e9, - "qubit": 0, - "name": "acquire", - "data": "real", - } - - self.assertDictEqual(objs[0].meta, ref_meta) - - # style check - ref_style = { - "alpha": self.formatter["alpha.fill_waveform"], - "zorder": self.formatter["layer.fill_waveform"], - "linewidth": self.formatter["line_width.fill_waveform"], - "linestyle": self.formatter["line_style.fill_waveform"], - "color": self.formatter["color.waveforms"]["A"][0], - } - self.assertDictEqual(objs[0].styles, ref_style) - - def test_gen_iqx_latex_waveform_name_x90(self): - """Test gen_iqx_latex_waveform_name with x90 waveform.""" - iqx_pulse = pulse.Waveform(samples=[0, 0, 0, 0], name="X90p_d0_1234567") - play = pulse.Play(iqx_pulse, pulse.DriveChannel(0)) - inst_data = create_instruction(play, 0, 0, 0, 0.1) - - obj = waveform.gen_ibmq_latex_waveform_name( - inst_data, formatter=self.formatter, device=self.device - )[0] - - # type check - self.assertEqual(type(obj), drawings.TextData) - - # data check - self.assertListEqual(obj.channels, [pulse.DriveChannel(0)]) - self.assertEqual(obj.text, "X90p_d0_1234567") - self.assertEqual(obj.latex, r"{\rm X}(\pi/2)") - - # style check - ref_style = { - "zorder": self.formatter["layer.annotate"], - "color": self.formatter["color.annotate"], - "size": self.formatter["text_size.annotate"], - "va": "center", - "ha": "center", - } - self.assertDictEqual(obj.styles, ref_style) - - def test_gen_iqx_latex_waveform_name_x180(self): - """Test gen_iqx_latex_waveform_name with x180 waveform.""" - iqx_pulse = pulse.Waveform(samples=[0, 0, 0, 0], name="Xp_d0_1234567") - play = pulse.Play(iqx_pulse, pulse.DriveChannel(0)) - inst_data = create_instruction(play, 0, 0, 0, 0.1) - - obj = waveform.gen_ibmq_latex_waveform_name( - inst_data, formatter=self.formatter, device=self.device - )[0] - - # type check - self.assertEqual(type(obj), drawings.TextData) - - # data check - self.assertListEqual(obj.channels, [pulse.DriveChannel(0)]) - self.assertEqual(obj.text, "Xp_d0_1234567") - self.assertEqual(obj.latex, r"{\rm X}(\pi)") - - def test_gen_iqx_latex_waveform_name_cr(self): - """Test gen_iqx_latex_waveform_name with CR waveform.""" - iqx_pulse = pulse.Waveform(samples=[0, 0, 0, 0], name="CR90p_u0_1234567") - play = pulse.Play(iqx_pulse, pulse.ControlChannel(0)) - inst_data = create_instruction(play, 0, 0, 0, 0.1) - - obj = waveform.gen_ibmq_latex_waveform_name( - inst_data, formatter=self.formatter, device=self.device - )[0] - - # type check - self.assertEqual(type(obj), drawings.TextData) - - # data check - self.assertListEqual(obj.channels, [pulse.ControlChannel(0)]) - self.assertEqual(obj.text, "CR90p_u0_1234567") - self.assertEqual(obj.latex, r"{\rm CR}(\pi/4)") - - def test_gen_iqx_latex_waveform_name_compensation_tone(self): - """Test gen_iqx_latex_waveform_name with CR compensation waveform.""" - iqx_pulse = pulse.Waveform(samples=[0, 0, 0, 0], name="CR90p_d0_u0_1234567") - play = pulse.Play(iqx_pulse, pulse.DriveChannel(0)) - inst_data = create_instruction(play, 0, 0, 0, 0.1) - - obj = waveform.gen_ibmq_latex_waveform_name( - inst_data, formatter=self.formatter, device=self.device - )[0] - - # type check - self.assertEqual(type(obj), drawings.TextData) - - # data check - self.assertListEqual(obj.channels, [pulse.DriveChannel(0)]) - self.assertEqual(obj.text, "CR90p_d0_u0_1234567") - self.assertEqual(obj.latex, r"\overline{\rm CR}(\pi/4)") - - def test_gen_waveform_max_value(self): - """Test gen_waveform_max_value.""" - iqx_pulse = pulse.Waveform(samples=[0, 0.1, 0.3, -0.2j], name="test") - play = pulse.Play(iqx_pulse, pulse.DriveChannel(0)) - inst_data = create_instruction(play, 0, 0, 0, 0.1) - - objs = waveform.gen_waveform_max_value( - inst_data, formatter=self.formatter, device=self.device - ) - - # type check - self.assertEqual(type(objs[0]), drawings.TextData) - self.assertEqual(type(objs[1]), drawings.TextData) - - # data check, real part, positive max - self.assertListEqual(objs[0].channels, [pulse.DriveChannel(0)]) - self.assertEqual(objs[0].text, "0.30\n\u25BE") - - # style check - ref_style = { - "zorder": self.formatter["layer.annotate"], - "color": self.formatter["color.annotate"], - "size": self.formatter["text_size.annotate"], - "va": "bottom", - "ha": "center", - } - self.assertDictEqual(objs[0].styles, ref_style) - - # data check, imaginary part, negative max - self.assertListEqual(objs[1].channels, [pulse.DriveChannel(0)]) - self.assertEqual(objs[1].text, "\u25B4\n-0.20") - - # style check - ref_style = { - "zorder": self.formatter["layer.annotate"], - "color": self.formatter["color.annotate"], - "size": self.formatter["text_size.annotate"], - "va": "top", - "ha": "center", - } - self.assertDictEqual(objs[1].styles, ref_style) - - def test_gen_filled_waveform_stepwise_opaque(self): - """Test generating waveform with unbound parameter.""" - amp = circuit.Parameter("amp") - my_pulse = pulse.Gaussian(10, amp, 3, name="my_pulse") - play = pulse.Play(my_pulse, pulse.DriveChannel(0)) - inst_data = create_instruction(play, np.pi / 2, 5e9, 5, 0.1, True) - - objs = waveform.gen_filled_waveform_stepwise( - inst_data, formatter=self.formatter, device=self.device - ) - - self.assertEqual(len(objs), 2) - - # type check - self.assertEqual(type(objs[0]), drawings.BoxData) - self.assertEqual(type(objs[1]), drawings.TextData) - - x_ref = np.array([5, 15]) - y_ref = np.array( - [ - -0.5 * self.formatter["box_height.opaque_shape"], - 0.5 * self.formatter["box_height.opaque_shape"], - ] - ) - - # data check - np.testing.assert_array_equal(objs[0].xvals, x_ref) - np.testing.assert_array_equal(objs[0].yvals, y_ref) - - # meta data check - ref_meta = { - "duration (cycle time)": 10, - "duration (sec)": 1.0, - "t0 (cycle time)": 5, - "t0 (sec)": 0.5, - "waveform shape": "Gaussian", - "amp": "amp", - "angle": 0, - "sigma": 3, - "phase": np.pi / 2, - "frequency": 5e9, - "qubit": 0, - "name": "my_pulse", - } - self.assertDictEqual(objs[0].meta, ref_meta) - - # style check - ref_style = { - "alpha": self.formatter["alpha.opaque_shape"], - "zorder": self.formatter["layer.fill_waveform"], - "linewidth": self.formatter["line_width.opaque_shape"], - "linestyle": self.formatter["line_style.opaque_shape"], - "facecolor": self.formatter["color.opaque_shape"][0], - "edgecolor": self.formatter["color.opaque_shape"][1], - } - self.assertDictEqual(objs[0].styles, ref_style) - - # test label - self.assertEqual(objs[1].text, "Gaussian(amp)") - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestChartGenerators(QiskitTestCase): - """Tests for chart info generators.""" - - @ignore_pulse_deprecation_warnings - def setUp(self) -> None: - super().setUp() - style = stylesheet.QiskitPulseStyle() - self.formatter = style.formatter - self.device = device_info.OpenPulseBackendInfo( - name="test", - dt=1, - channel_frequency_map={ - pulse.DriveChannel(0): 5.0e9, - pulse.DriveChannel(1): 5.1e9, - pulse.MeasureChannel(0): 7.0e9, - pulse.MeasureChannel(1): 7.1e9, - pulse.ControlChannel(0): 5.0e9, - pulse.ControlChannel(1): 5.1e9, - }, - qubit_channel_map={ - 0: [ - pulse.DriveChannel(0), - pulse.MeasureChannel(0), - pulse.AcquireChannel(0), - pulse.ControlChannel(0), - ], - 1: [ - pulse.DriveChannel(1), - pulse.MeasureChannel(1), - pulse.AcquireChannel(1), - pulse.ControlChannel(1), - ], - }, - ) - - def test_gen_baseline(self): - """Test gen_baseline.""" - channel_info = types.ChartAxis(name="D0", channels=[pulse.DriveChannel(0)]) - - obj = chart.gen_baseline(channel_info, formatter=self.formatter, device=self.device)[0] - - # type check - self.assertEqual(type(obj), drawings.LineData) - - # data check - self.assertListEqual(obj.channels, [pulse.DriveChannel(0)]) - - ref_x = [types.AbstractCoordinate.LEFT, types.AbstractCoordinate.RIGHT] - ref_y = [0, 0] - - self.assertListEqual(list(obj.xvals), ref_x) - self.assertListEqual(list(obj.yvals), ref_y) - - # style check - ref_style = { - "alpha": self.formatter["alpha.baseline"], - "zorder": self.formatter["layer.baseline"], - "linewidth": self.formatter["line_width.baseline"], - "linestyle": self.formatter["line_style.baseline"], - "color": self.formatter["color.baseline"], - } - self.assertDictEqual(obj.styles, ref_style) - - def test_gen_chart_name(self): - """Test gen_chart_name.""" - channel_info = types.ChartAxis(name="D0", channels=[pulse.DriveChannel(0)]) - - obj = chart.gen_chart_name(channel_info, formatter=self.formatter, device=self.device)[0] - - # type check - self.assertEqual(type(obj), drawings.TextData) - - # data check - self.assertListEqual(obj.channels, [pulse.DriveChannel(0)]) - self.assertEqual(obj.text, "D0") - - # style check - ref_style = { - "zorder": self.formatter["layer.axis_label"], - "color": self.formatter["color.axis_label"], - "size": self.formatter["text_size.axis_label"], - "va": "center", - "ha": "right", - } - self.assertDictEqual(obj.styles, ref_style) - - def test_gen_scaling_info(self): - """Test gen_scaling_info.""" - channel_info = types.ChartAxis(name="D0", channels=[pulse.DriveChannel(0)]) - - obj = chart.gen_chart_scale(channel_info, formatter=self.formatter, device=self.device)[0] - - # type check - self.assertEqual(type(obj), drawings.TextData) - - # data check - self.assertListEqual(obj.channels, [pulse.DriveChannel(0)]) - self.assertEqual(obj.text, f"x{types.DynamicString.SCALE}") - - # style check - ref_style = { - "zorder": self.formatter["layer.axis_label"], - "color": self.formatter["color.axis_label"], - "size": self.formatter["text_size.annotate"], - "va": "center", - "ha": "right", - } - self.assertDictEqual(obj.styles, ref_style) - - def test_gen_frequency_info(self): - """Test gen_scaling_info.""" - channel_info = types.ChartAxis(name="D0", channels=[pulse.DriveChannel(0)]) - - obj = chart.gen_channel_freqs(channel_info, formatter=self.formatter, device=self.device)[0] - - # type check - self.assertEqual(type(obj), drawings.TextData) - - # data check - self.assertListEqual(obj.channels, [pulse.DriveChannel(0)]) - self.assertEqual(obj.text, "5.00 GHz") - - # style check - ref_style = { - "zorder": self.formatter["layer.axis_label"], - "color": self.formatter["color.axis_label"], - "size": self.formatter["text_size.annotate"], - "va": "center", - "ha": "right", - } - self.assertDictEqual(obj.styles, ref_style) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestFrameGenerators(QiskitTestCase): - """Tests for frame info generators.""" - - @ignore_pulse_deprecation_warnings - def setUp(self) -> None: - super().setUp() - style = stylesheet.QiskitPulseStyle() - self.formatter = style.formatter - self.device = device_info.OpenPulseBackendInfo( - name="test", - dt=1, - channel_frequency_map={ - pulse.DriveChannel(0): 5.0e9, - pulse.DriveChannel(1): 5.1e9, - pulse.MeasureChannel(0): 7.0e9, - pulse.MeasureChannel(1): 7.1e9, - pulse.ControlChannel(0): 5.0e9, - pulse.ControlChannel(1): 5.1e9, - }, - qubit_channel_map={ - 0: [ - pulse.DriveChannel(0), - pulse.MeasureChannel(0), - pulse.AcquireChannel(0), - pulse.ControlChannel(0), - ], - 1: [ - pulse.DriveChannel(1), - pulse.MeasureChannel(1), - pulse.AcquireChannel(1), - pulse.ControlChannel(1), - ], - }, - ) - - def test_phase_to_text(self): - """Test helper function to convert phase to text.""" - plain, latex = frame._phase_to_text(self.formatter, np.pi, max_denom=10, flip=True) - self.assertEqual(plain, "-pi") - self.assertEqual(latex, r"-\pi") - - plain, latex = frame._phase_to_text(self.formatter, np.pi / 2, max_denom=10, flip=True) - self.assertEqual(plain, "-pi/2") - self.assertEqual(latex, r"-\pi/2") - - plain, latex = frame._phase_to_text(self.formatter, np.pi * 3 / 4, max_denom=10, flip=True) - self.assertEqual(plain, "-3/4 pi") - self.assertEqual(latex, r"-3/4 \pi") - - def test_frequency_to_text(self): - """Test helper function to convert frequency to text.""" - plain, latex = frame._freq_to_text(self.formatter, 1e6, unit="MHz") - self.assertEqual(plain, "1.00 MHz") - self.assertEqual(latex, r"1.00~{\rm MHz}") - - def test_gen_formatted_phase(self): - """Test gen_formatted_phase.""" - fcs = [ - pulse.ShiftPhase(np.pi / 2, pulse.DriveChannel(0)), - pulse.ShiftFrequency(1e6, pulse.DriveChannel(0)), - ] - inst_data = create_instruction(fcs, np.pi / 2, 1e6, 5, 0.1) - - obj = frame.gen_formatted_phase(inst_data, formatter=self.formatter, device=self.device)[0] - - # type check - self.assertEqual(type(obj), drawings.TextData) - - # data check - self.assertListEqual(obj.channels, [pulse.DriveChannel(0)]) - self.assertEqual(obj.latex, r"{\rm VZ}(-\pi/2)") - self.assertEqual(obj.text, "VZ(-pi/2)") - - # style check - ref_style = { - "zorder": self.formatter["layer.frame_change"], - "color": self.formatter["color.frame_change"], - "size": self.formatter["text_size.annotate"], - "va": "center", - "ha": "center", - } - self.assertDictEqual(obj.styles, ref_style) - - def test_gen_formatted_freq_mhz(self): - """Test gen_formatted_freq_mhz.""" - fcs = [ - pulse.ShiftPhase(np.pi / 2, pulse.DriveChannel(0)), - pulse.ShiftFrequency(1e6, pulse.DriveChannel(0)), - ] - inst_data = create_instruction(fcs, np.pi / 2, 1e6, 5, 0.1) - - obj = frame.gen_formatted_freq_mhz(inst_data, formatter=self.formatter, device=self.device)[ - 0 - ] - - # type check - self.assertEqual(type(obj), drawings.TextData) - - # data check - self.assertListEqual(obj.channels, [pulse.DriveChannel(0)]) - self.assertEqual(obj.latex, r"\Delta f = 1.00~{\rm MHz}") - self.assertEqual(obj.text, "\u0394f = 1.00 MHz") - - # style check - ref_style = { - "zorder": self.formatter["layer.frame_change"], - "color": self.formatter["color.frame_change"], - "size": self.formatter["text_size.annotate"], - "va": "center", - "ha": "center", - } - self.assertDictEqual(obj.styles, ref_style) - - def test_gen_formatted_frame_values(self): - """Test gen_formatted_frame_values.""" - fcs = [ - pulse.ShiftPhase(np.pi / 2, pulse.DriveChannel(0)), - pulse.ShiftFrequency(1e6, pulse.DriveChannel(0)), - ] - inst_data = create_instruction(fcs, np.pi / 2, 1e6, 5, 0.1) - - objs = frame.gen_formatted_frame_values( - inst_data, formatter=self.formatter, device=self.device - ) - - # type check - self.assertEqual(type(objs[0]), drawings.TextData) - self.assertEqual(type(objs[1]), drawings.TextData) - - def test_gen_raw_operand_values_compact(self): - """Test gen_raw_operand_values_compact.""" - fcs = [ - pulse.ShiftPhase(np.pi / 2, pulse.DriveChannel(0)), - pulse.ShiftFrequency(1e6, pulse.DriveChannel(0)), - ] - inst_data = create_instruction(fcs, np.pi / 2, 1e6, 5, 0.1) - - obj = frame.gen_raw_operand_values_compact( - inst_data, formatter=self.formatter, device=self.device - )[0] - - # type check - self.assertEqual(type(obj), drawings.TextData) - - # data check - self.assertListEqual(obj.channels, [pulse.DriveChannel(0)]) - self.assertEqual(obj.text, "1.57\n1.0e6") - - def gen_frame_symbol(self): - """Test gen_frame_symbol.""" - fcs = [ - pulse.ShiftPhase(np.pi / 2, pulse.DriveChannel(0)), - pulse.ShiftFrequency(1e6, pulse.DriveChannel(0)), - ] - inst_data = create_instruction(fcs, np.pi / 2, 1e6, 5, 0.1) - - obj = frame.gen_frame_symbol(inst_data, formatter=self.formatter, device=self.device)[0] - - # type check - self.assertEqual(type(obj), drawings.TextData) - - # data check - self.assertListEqual(obj.channels, [pulse.DriveChannel(0)]) - self.assertEqual(obj.latex, self.formatter["latex_symbol.frame_change"]) - self.assertEqual(obj.text, self.formatter["unicode_symbol.frame_change"]) - - # metadata check - ref_meta = { - "total phase change": np.pi / 2, - "total frequency change": 1e6, - "program": ["ShiftPhase(1.57 rad.)", "ShiftFrequency(1.00e+06 Hz)"], - "t0 (cycle time)": 5, - "t0 (sec)": 0.5, - } - self.assertDictEqual(obj.meta, ref_meta) - - # style check - ref_style = { - "zorder": self.formatter["layer.frame_change"], - "color": self.formatter["color.frame_change"], - "size": self.formatter["text_size.frame_change"], - "va": "center", - "ha": "center", - } - self.assertDictEqual(obj.styles, ref_style) - - def gen_frame_symbol_with_parameters(self): - """Test gen_frame_symbol with parameterized frame.""" - theta = -1.0 * circuit.Parameter("P0") - fcs = [pulse.ShiftPhase(theta, pulse.DriveChannel(0))] - inst_data = create_instruction(fcs, np.pi / 2, 1e6, 5, 0.1) - - obj = frame.gen_frame_symbol(inst_data, formatter=self.formatter, device=self.device)[0] - - # metadata check - ref_meta = { - "total phase change": np.pi / 2, - "total frequency change": 1e6, - "program": ["ShiftPhase(-1.0*P0)"], - "t0 (cycle time)": 5, - "t0 (sec)": 0.5, - } - self.assertDictEqual(obj.meta, ref_meta) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestSnapshotGenerators(QiskitTestCase): - """Tests for snapshot generators.""" - - @ignore_pulse_deprecation_warnings - def setUp(self) -> None: - super().setUp() - style = stylesheet.QiskitPulseStyle() - self.formatter = style.formatter - self.device = device_info.OpenPulseBackendInfo( - name="test", - dt=1, - channel_frequency_map={ - pulse.DriveChannel(0): 5.0e9, - pulse.DriveChannel(1): 5.1e9, - pulse.MeasureChannel(0): 7.0e9, - pulse.MeasureChannel(1): 7.1e9, - pulse.ControlChannel(0): 5.0e9, - pulse.ControlChannel(1): 5.1e9, - }, - qubit_channel_map={ - 0: [ - pulse.DriveChannel(0), - pulse.MeasureChannel(0), - pulse.AcquireChannel(0), - pulse.ControlChannel(0), - ], - 1: [ - pulse.DriveChannel(1), - pulse.MeasureChannel(1), - pulse.AcquireChannel(1), - pulse.ControlChannel(1), - ], - }, - ) - - def test_gen_snapshot_name(self): - """Test gen_snapshot_name.""" - snap_inst = pulse.instructions.Snapshot(label="test_snapshot", snapshot_type="statevector") - inst_data = types.SnapshotInstruction(5, 0.1, snap_inst) - - obj = snapshot.gen_snapshot_name(inst_data, formatter=self.formatter, device=self.device)[0] - - # type check - self.assertEqual(type(obj), drawings.TextData) - - # data check - self.assertListEqual(obj.channels, [pulse.channels.SnapshotChannel()]) - self.assertEqual(obj.text, "test_snapshot") - - # style check - ref_style = { - "zorder": self.formatter["layer.snapshot"], - "color": self.formatter["color.snapshot"], - "size": self.formatter["text_size.annotate"], - "va": "center", - "ha": "center", - } - self.assertDictEqual(obj.styles, ref_style) - - def gen_snapshot_symbol(self): - """Test gen_snapshot_symbol.""" - snap_inst = pulse.instructions.Snapshot(label="test_snapshot", snapshot_type="statevector") - inst_data = types.SnapshotInstruction(5, 0.1, snap_inst) - - obj = snapshot.gen_snapshot_name(inst_data, formatter=self.formatter, device=self.device)[0] - - # type check - self.assertEqual(type(obj), drawings.TextData) - - # data check - self.assertListEqual(obj.channels, [pulse.channels.SnapshotChannel()]) - self.assertEqual(obj.text, self.formatter["unicode_symbol.snapshot"]) - self.assertEqual(obj.latex, self.formatter["latex_symbol.snapshot"]) - - # metadata check - ref_meta = { - "snapshot type": "statevector", - "t0 (cycle time)": 5, - "t0 (sec)": 0.5, - "name": "test_snapshot", - "label": "test_snapshot", - } - self.assertDictEqual(obj.meta, ref_meta) - - # style check - ref_style = { - "zorder": self.formatter["layer.snapshot"], - "color": self.formatter["color.snapshot"], - "size": self.formatter["text_size.snapshot"], - "va": "bottom", - "ha": "center", - } - self.assertDictEqual(obj.styles, ref_style) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestBarrierGenerators(QiskitTestCase): - """Tests for barrier generators.""" - - @ignore_pulse_deprecation_warnings - def setUp(self) -> None: - super().setUp() - style = stylesheet.QiskitPulseStyle() - self.formatter = style.formatter - self.device = device_info.OpenPulseBackendInfo( - name="test", - dt=1, - channel_frequency_map={ - pulse.DriveChannel(0): 5.0e9, - pulse.DriveChannel(1): 5.1e9, - pulse.MeasureChannel(0): 7.0e9, - pulse.MeasureChannel(1): 7.1e9, - pulse.ControlChannel(0): 5.0e9, - pulse.ControlChannel(1): 5.1e9, - }, - qubit_channel_map={ - 0: [ - pulse.DriveChannel(0), - pulse.MeasureChannel(0), - pulse.AcquireChannel(0), - pulse.ControlChannel(0), - ], - 1: [ - pulse.DriveChannel(1), - pulse.MeasureChannel(1), - pulse.AcquireChannel(1), - pulse.ControlChannel(1), - ], - }, - ) - - def test_gen_barrier(self): - """Test gen_barrier.""" - inst_data = types.BarrierInstruction( - 5, 0.1, [pulse.DriveChannel(0), pulse.ControlChannel(0)] - ) - obj = barrier.gen_barrier(inst_data, formatter=self.formatter, device=self.device)[0] - - # type check - self.assertEqual(type(obj), drawings.LineData) - - # data check - self.assertListEqual(obj.channels, [pulse.DriveChannel(0), pulse.ControlChannel(0)]) - - ref_x = [5, 5] - ref_y = [types.AbstractCoordinate.BOTTOM, types.AbstractCoordinate.TOP] - - self.assertListEqual(list(obj.xvals), ref_x) - self.assertListEqual(list(obj.yvals), ref_y) - - # style check - ref_style = { - "alpha": self.formatter["alpha.barrier"], - "zorder": self.formatter["layer.barrier"], - "linewidth": self.formatter["line_width.barrier"], - "linestyle": self.formatter["line_style.barrier"], - "color": self.formatter["color.barrier"], - } - self.assertDictEqual(obj.styles, ref_style) diff --git a/test/python/visualization/pulse_v2/test_layouts.py b/test/python/visualization/pulse_v2/test_layouts.py deleted file mode 100644 index 3985f1dd9318..000000000000 --- a/test/python/visualization/pulse_v2/test_layouts.py +++ /dev/null @@ -1,258 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Tests for core modules of pulse drawer.""" - -from qiskit import pulse -from qiskit.visualization.pulse_v2 import layouts, device_info -from test import QiskitTestCase # pylint: disable=wrong-import-order -from qiskit.utils.deprecate_pulse import decorate_test_methods, ignore_pulse_deprecation_warnings - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestChannelArrangement(QiskitTestCase): - """Tests for channel mapping functions.""" - - @ignore_pulse_deprecation_warnings - def setUp(self) -> None: - super().setUp() - self.channels = [ - pulse.DriveChannel(0), - pulse.DriveChannel(1), - pulse.DriveChannel(2), - pulse.MeasureChannel(1), - pulse.MeasureChannel(2), - pulse.AcquireChannel(1), - pulse.AcquireChannel(2), - pulse.ControlChannel(0), - pulse.ControlChannel(2), - pulse.ControlChannel(5), - ] - self.formatter = {"control.show_acquire_channel": True} - self.device = device_info.OpenPulseBackendInfo( - name="test", - dt=1, - channel_frequency_map={ - pulse.DriveChannel(0): 5.0e9, - pulse.DriveChannel(1): 5.1e9, - pulse.DriveChannel(2): 5.2e9, - pulse.MeasureChannel(1): 7.0e9, - pulse.MeasureChannel(1): 7.1e9, - pulse.MeasureChannel(2): 7.2e9, - pulse.ControlChannel(0): 5.0e9, - pulse.ControlChannel(1): 5.1e9, - pulse.ControlChannel(2): 5.2e9, - pulse.ControlChannel(3): 5.3e9, - pulse.ControlChannel(4): 5.4e9, - pulse.ControlChannel(5): 5.5e9, - }, - qubit_channel_map={ - 0: [ - pulse.DriveChannel(0), - pulse.MeasureChannel(0), - pulse.AcquireChannel(0), - pulse.ControlChannel(0), - ], - 1: [ - pulse.DriveChannel(1), - pulse.MeasureChannel(1), - pulse.AcquireChannel(1), - pulse.ControlChannel(1), - ], - 2: [ - pulse.DriveChannel(2), - pulse.MeasureChannel(2), - pulse.AcquireChannel(2), - pulse.ControlChannel(2), - pulse.ControlChannel(3), - pulse.ControlChannel(4), - ], - 3: [ - pulse.DriveChannel(3), - pulse.MeasureChannel(3), - pulse.AcquireChannel(3), - pulse.ControlChannel(5), - ], - }, - ) - - def test_channel_type_grouped_sort(self): - """Test channel_type_grouped_sort.""" - out_layout = layouts.channel_type_grouped_sort( - self.channels, formatter=self.formatter, device=self.device - ) - - ref_channels = [ - [pulse.DriveChannel(0)], - [pulse.DriveChannel(1)], - [pulse.DriveChannel(2)], - [pulse.ControlChannel(0)], - [pulse.ControlChannel(2)], - [pulse.ControlChannel(5)], - [pulse.MeasureChannel(1)], - [pulse.MeasureChannel(2)], - [pulse.AcquireChannel(1)], - [pulse.AcquireChannel(2)], - ] - ref_names = ["D0", "D1", "D2", "U0", "U2", "U5", "M1", "M2", "A1", "A2"] - - ref = list(zip(ref_names, ref_channels)) - - self.assertListEqual(list(out_layout), ref) - - def test_channel_index_sort(self): - """Test channel_index_grouped_sort.""" - # Add an unusual channel number to stress test the channel ordering - self.channels.append(pulse.DriveChannel(100)) - self.channels.reverse() - out_layout = layouts.channel_index_grouped_sort( - self.channels, formatter=self.formatter, device=self.device - ) - - ref_channels = [ - [pulse.DriveChannel(0)], - [pulse.ControlChannel(0)], - [pulse.DriveChannel(1)], - [pulse.MeasureChannel(1)], - [pulse.AcquireChannel(1)], - [pulse.DriveChannel(2)], - [pulse.ControlChannel(2)], - [pulse.MeasureChannel(2)], - [pulse.AcquireChannel(2)], - [pulse.ControlChannel(5)], - [pulse.DriveChannel(100)], - ] - - ref_names = ["D0", "U0", "D1", "M1", "A1", "D2", "U2", "M2", "A2", "U5", "D100"] - - ref = list(zip(ref_names, ref_channels)) - - self.assertListEqual(list(out_layout), ref) - - def test_channel_index_sort_grouped_control(self): - """Test channel_index_grouped_sort_u.""" - out_layout = layouts.channel_index_grouped_sort_u( - self.channels, formatter=self.formatter, device=self.device - ) - - ref_channels = [ - [pulse.DriveChannel(0)], - [pulse.DriveChannel(1)], - [pulse.MeasureChannel(1)], - [pulse.AcquireChannel(1)], - [pulse.DriveChannel(2)], - [pulse.MeasureChannel(2)], - [pulse.AcquireChannel(2)], - [pulse.ControlChannel(0)], - [pulse.ControlChannel(2)], - [pulse.ControlChannel(5)], - ] - - ref_names = ["D0", "D1", "M1", "A1", "D2", "M2", "A2", "U0", "U2", "U5"] - - ref = list(zip(ref_names, ref_channels)) - - self.assertListEqual(list(out_layout), ref) - - def test_channel_qubit_index_sort(self): - """Test qubit_index_sort.""" - out_layout = layouts.qubit_index_sort( - self.channels, formatter=self.formatter, device=self.device - ) - - ref_channels = [ - [pulse.DriveChannel(0), pulse.ControlChannel(0)], - [pulse.DriveChannel(1), pulse.MeasureChannel(1)], - [pulse.DriveChannel(2), pulse.MeasureChannel(2), pulse.ControlChannel(2)], - [pulse.ControlChannel(5)], - ] - - ref_names = ["Q0", "Q1", "Q2", "Q3"] - - ref = list(zip(ref_names, ref_channels)) - - self.assertListEqual(list(out_layout), ref) - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestHorizontalAxis(QiskitTestCase): - """Tests for horizontal axis mapping functions.""" - - def test_time_map_in_ns(self): - """Test for time_map_in_ns.""" - time_window = (0, 1000) - breaks = [(100, 200)] - dt = 1e-9 - - haxis = layouts.time_map_in_ns(time_window=time_window, axis_breaks=breaks, dt=dt) - - self.assertListEqual(list(haxis.window), [0, 900]) - self.assertListEqual(list(haxis.axis_break_pos), [100]) - ref_axis_map = { - 0.0: "0", - 180.0: "280", - 360.0: "460", - 540.0: "640", - 720.0: "820", - 900.0: "1000", - } - self.assertDictEqual(haxis.axis_map, ref_axis_map) - self.assertEqual(haxis.label, "Time (ns)") - - def test_time_map_in_without_dt(self): - """Test for time_map_in_ns when dt is not provided.""" - time_window = (0, 1000) - breaks = [(100, 200)] - dt = None - - haxis = layouts.time_map_in_ns(time_window=time_window, axis_breaks=breaks, dt=dt) - - self.assertListEqual(list(haxis.window), [0, 900]) - self.assertListEqual(list(haxis.axis_break_pos), [100]) - ref_axis_map = { - 0.0: "0", - 180.0: "280", - 360.0: "460", - 540.0: "640", - 720.0: "820", - 900.0: "1000", - } - self.assertDictEqual(haxis.axis_map, ref_axis_map) - self.assertEqual(haxis.label, "System cycle time (dt)") - - -@decorate_test_methods(ignore_pulse_deprecation_warnings) -class TestFigureTitle(QiskitTestCase): - """Tests for figure title generation.""" - - @ignore_pulse_deprecation_warnings - def setUp(self) -> None: - super().setUp() - self.device = device_info.OpenPulseBackendInfo(name="test_backend", dt=1e-9) - self.prog = pulse.Schedule(name="test_sched") - self.prog.insert( - 0, pulse.Play(pulse.Constant(100, 0.1), pulse.DriveChannel(0)), inplace=True - ) - - def detail_title(self): - """Test detail_title layout function.""" - ref_title = "Name: test_sched, Duration: 100.0 ns, Backend: test_backend" - out = layouts.detail_title(self.prog, self.device) - - self.assertEqual(out, ref_title) - - def empty_title(self): - """Test empty_title layout function.""" - ref_title = "" - out = layouts.detail_title(self.prog, self.device) - - self.assertEqual(out, ref_title) diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index 7a551fbcf8eb..8e00e0c60a06 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -1077,10 +1077,11 @@ def load_qpy(qpy_files, version_parts): from qiskit.qpy.exceptions import QpyError - while pulse_files: - path, version = pulse_files.popitem() + for path, min_version in pulse_files.items(): - if version_parts < version or version_parts >= (2, 0): + # version_parts is the version of Qiskit used to generate the payloads being loaded in this test. + # min_version is the minimal version of Qiskit this pulse payload was generated with. + if version_parts < min_version or version_parts >= (2, 0): continue if path == "pulse_gates.qpy": @@ -1093,7 +1094,7 @@ def load_qpy(qpy_files, version_parts): sys.exit(1) else: try: - # A ScheduleBlock payload, should raise QpyError + # A ScheduleBlock payload, should raise QpyError. with open(path, "rb") as fd: load(fd) except QpyError: