Source code for braket.pulse.waveforms

# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
#     http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

from __future__ import annotations

import random
import string
from abc import ABC, abstractmethod

import numpy as np
import scipy as sp
from oqpy import WaveformVar, bool_, complex128, declare_waveform_generator, duration, float64
from oqpy.base import OQPyExpression

from braket.parametric.free_parameter import FreeParameter
from braket.parametric.free_parameter_expression import (
    FreeParameterExpression,
    subs_if_free_parameter,
)
from braket.parametric.parameterizable import Parameterizable


[docs] class Waveform(ABC): """A waveform is a time-dependent envelope that can be used to emit signals on an output port or receive signals from an input port. As such, when transmitting signals to the qubit, a frame determines time at which the waveform envelope is emitted, its carrier frequency, and it's phase offset. When capturing signals from a qubit, at minimum a frame determines the time at which the signal is captured. See https://openqasm.com/language/openpulse.html#waveforms for more details. """ @abstractmethod def _to_oqpy_expression(self) -> OQPyExpression: """Returns an OQPyExpression defining this waveform."""
[docs] @abstractmethod def sample(self, dt: float) -> np.ndarray: """Generates a sample of amplitudes for this Waveform based on the given time resolution. Args: dt (float): The time resolution. Returns: np.ndarray: The sample amplitudes for this waveform. """
@staticmethod @abstractmethod def _from_calibration_schema(waveform_json: dict) -> Waveform: """Parses a JSON input and returns the BDK waveform. See https://github.com/aws/amazon-braket-schemas-python/blob/main/src/braket/device_schema/pulse/native_gate_calibrations_v1.py#L104 Args: waveform_json (dict): A JSON object with the needed parameters for making the Waveform. Returns: Waveform: A Waveform object parsed from the supplied JSON. """
[docs] class ArbitraryWaveform(Waveform): """An arbitrary waveform with amplitudes at each timestep explicitly specified using an array. """ def __init__(self, amplitudes: list[complex], id: str | None = None): """Initializes an `ArbitraryWaveform`. Args: amplitudes (list[complex]): Array of complex values specifying the waveform amplitude at each timestep. The timestep is determined by the sampling rate of the frame to which waveform is applied to. id (str | None): The identifier used for declaring this waveform. A random string of ascii characters is assigned by default. """ self.amplitudes = list(amplitudes) self.id = id or _make_identifier_name() def __repr__(self) -> str: return f"ArbitraryWaveform('id': {self.id}, 'amplitudes': {self.amplitudes})" def __eq__(self, other: ArbitraryWaveform): return isinstance(other, ArbitraryWaveform) and (self.amplitudes, self.id) == ( other.amplitudes, other.id, ) def _to_oqpy_expression(self) -> OQPyExpression: """Returns an OQPyExpression defining this waveform. Returns: OQPyExpression: The OQPyExpression. """ return WaveformVar(init_expression=self.amplitudes, name=self.id)
[docs] def sample(self, dt: float) -> np.ndarray: """Generates a sample of amplitudes for this Waveform based on the given time resolution. Args: dt (float): The time resolution. Raises: NotImplementedError: This class does not implement sample. Returns: np.ndarray: The sample amplitudes for this waveform. """ raise NotImplementedError
@staticmethod def _from_calibration_schema(waveform_json: dict) -> ArbitraryWaveform: wave_id = waveform_json["waveformId"] complex_amplitudes = [complex(i[0], i[1]) for i in waveform_json["amplitudes"]] return ArbitraryWaveform(complex_amplitudes, wave_id)
[docs] class ConstantWaveform(Waveform, Parameterizable): """A constant waveform which holds the supplied `iq` value as its amplitude for the specified length. """ def __init__(self, length: float | FreeParameterExpression, iq: complex, id: str | None = None): """Initializes a `ConstantWaveform`. Args: length (float | FreeParameterExpression): Value (in seconds) specifying the duration of the waveform. iq (complex): complex value specifying the amplitude of the waveform. id (str | None): The identifier used for declaring this waveform. A random string of ascii characters is assigned by default. """ self.length = length self.iq = iq self.id = id or _make_identifier_name() def __repr__(self) -> str: return f"ConstantWaveform('id': {self.id}, 'length': {self.length}, 'iq': {self.iq})" @property def parameters(self) -> list[float | FreeParameterExpression]: """Returns the parameters associated with the object, either unbound free parameter expressions or bound values. Returns: list[float | FreeParameterExpression]: a list of parameters. """ return [self.length]
[docs] def bind_values(self, **kwargs: FreeParameter | str) -> ConstantWaveform: """Takes in parameters and returns an object with specified parameters replaced with their values. Args: **kwargs (FreeParameter | str): Arbitrary keyword arguments. Returns: ConstantWaveform: A copy of this waveform with the requested parameters bound. """ constructor_kwargs = { "length": subs_if_free_parameter(self.length, **kwargs), "iq": self.iq, "id": self.id, } return ConstantWaveform(**constructor_kwargs)
def __eq__(self, other: ConstantWaveform): return isinstance(other, ConstantWaveform) and (self.length, self.iq, self.id) == ( other.length, other.iq, other.id, ) def _to_oqpy_expression(self) -> OQPyExpression: """Returns an OQPyExpression defining this waveform. Returns: OQPyExpression: The OQPyExpression. """ constant_generator = declare_waveform_generator( "constant", [("length", duration), ("iq", complex128)] ) return WaveformVar( init_expression=constant_generator(self.length, self.iq), name=self.id, )
[docs] def sample(self, dt: float) -> np.ndarray: """Generates a sample of amplitudes for this Waveform based on the given time resolution. Args: dt (float): The time resolution. Returns: np.ndarray: The sample amplitudes for this waveform. """ # Amplitudes should be gated by [0:self.length] sample_range = np.arange(0, self.length, dt) return self.iq * np.ones_like(sample_range)
@staticmethod def _from_calibration_schema(waveform_json: dict) -> ConstantWaveform: wave_id = waveform_json["waveformId"] length = iq = None for val in waveform_json["arguments"]: if val["name"] == "length": length = ( float(val["value"]) if val["type"] == "float" else FreeParameterExpression(val["value"]) ) if val["name"] == "iq": iq = ( complex(val["value"]) if val["type"] == "complex" else FreeParameterExpression(val["value"]) ) return ConstantWaveform(length=length, iq=iq, id=wave_id)
[docs] class DragGaussianWaveform(Waveform, Parameterizable): """A gaussian waveform with an additional gaussian derivative component and lifting applied.""" def __init__( self, length: float | FreeParameterExpression, sigma: float | FreeParameterExpression, beta: float | FreeParameterExpression, amplitude: float | FreeParameterExpression = 1, zero_at_edges: bool = False, id: str | None = None, ): """Initializes a `DragGaussianWaveform`. Args: length (float | FreeParameterExpression): Value (in seconds) specifying the duration of the waveform. sigma (float | FreeParameterExpression): A measure (in seconds) of how wide or narrow the Gaussian peak is. beta (float | FreeParameterExpression): The correction amplitude. amplitude (float | FreeParameterExpression): The amplitude of the waveform envelope. Defaults to 1. zero_at_edges (bool): bool specifying whether the waveform amplitude is clipped to zero at the edges. Defaults to False. id (str | None): The identifier used for declaring this waveform. A random string of ascii characters is assigned by default. """ self.length = length self.sigma = sigma self.beta = beta self.amplitude = amplitude self.zero_at_edges = zero_at_edges self.id = id or _make_identifier_name() def __repr__(self) -> str: return ( f"DragGaussianWaveform('id': {self.id}, 'length': {self.length}, " f"'sigma': {self.sigma}, 'beta': {self.beta}, 'amplitude': {self.amplitude}, " f"'zero_at_edges': {self.zero_at_edges})" ) @property def parameters(self) -> list[FreeParameterExpression | FreeParameter | float]: """Returns the parameters associated with the object, either unbound free parameter expressions or bound values. """ return [self.length, self.sigma, self.beta, self.amplitude]
[docs] def bind_values(self, **kwargs: FreeParameter | str) -> DragGaussianWaveform: """Takes in parameters and returns an object with specified parameters replaced with their values. Args: **kwargs (FreeParameter | str): Arbitrary keyword arguments. Returns: DragGaussianWaveform: A copy of this waveform with the requested parameters bound. """ constructor_kwargs = { "length": subs_if_free_parameter(self.length, **kwargs), "sigma": subs_if_free_parameter(self.sigma, **kwargs), "beta": subs_if_free_parameter(self.beta, **kwargs), "amplitude": subs_if_free_parameter(self.amplitude, **kwargs), "zero_at_edges": self.zero_at_edges, "id": self.id, } return DragGaussianWaveform(**constructor_kwargs)
def __eq__(self, other: DragGaussianWaveform): return isinstance(other, DragGaussianWaveform) and ( self.length, self.sigma, self.beta, self.amplitude, self.zero_at_edges, self.id, ) == (other.length, other.sigma, other.beta, other.amplitude, other.zero_at_edges, other.id) def _to_oqpy_expression(self) -> OQPyExpression: """Returns an OQPyExpression defining this waveform. Returns: OQPyExpression: The OQPyExpression. """ drag_gaussian_generator = declare_waveform_generator( "drag_gaussian", [ ("length", duration), ("sigma", duration), ("beta", float64), ("amplitude", float64), ("zero_at_edges", bool_), ], ) return WaveformVar( init_expression=drag_gaussian_generator( self.length, self.sigma, self.beta, self.amplitude, self.zero_at_edges, ), name=self.id, )
[docs] def sample(self, dt: float) -> np.ndarray: """Generates a sample of amplitudes for this Waveform based on the given time resolution. Args: dt (float): The time resolution. Returns: np.ndarray: The sample amplitudes for this waveform. """ sample_range = np.arange(0, self.length, dt) t0 = self.length / 2 zero_at_edges_int = int(self.zero_at_edges) return ( (1 - (1.0j * self.beta * ((sample_range - t0) / self.sigma**2))) * ( self.amplitude / (1 - zero_at_edges_int * np.exp(-0.5 * ((self.length / (2 * self.sigma)) ** 2))) ) * ( np.exp(-0.5 * (((sample_range - t0) / self.sigma) ** 2)) - zero_at_edges_int * np.exp(-0.5 * ((self.length / (2 * self.sigma)) ** 2)) ) )
@staticmethod def _from_calibration_schema(waveform_json: dict) -> DragGaussianWaveform: waveform_parameters = {"id": waveform_json["waveformId"]} for val in waveform_json["arguments"]: waveform_parameters[val["name"]] = ( float(val["value"]) if val["type"] == "float" else FreeParameterExpression(val["value"]) ) return DragGaussianWaveform(**waveform_parameters)
[docs] class GaussianWaveform(Waveform, Parameterizable): """A waveform with amplitudes following a gaussian distribution for the specified parameters.""" def __init__( self, length: float | FreeParameterExpression, sigma: float | FreeParameterExpression, amplitude: float | FreeParameterExpression = 1, zero_at_edges: bool = False, id: str | None = None, ): """Initializes a `GaussianWaveform`. Args: length (float | FreeParameterExpression): Value (in seconds) specifying the duration of the waveform. sigma (float | FreeParameterExpression): A measure (in seconds) of how wide or narrow the Gaussian peak is. amplitude (float | FreeParameterExpression): The amplitude of the waveform envelope. Defaults to 1. zero_at_edges (bool): bool specifying whether the waveform amplitude is clipped to zero at the edges. Defaults to False. id (str | None): The identifier used for declaring this waveform. A random string of ascii characters is assigned by default. """ self.length = length self.sigma = sigma self.amplitude = amplitude self.zero_at_edges = zero_at_edges self.id = id or _make_identifier_name() def __repr__(self) -> str: return ( f"GaussianWaveform('id': {self.id}, 'length': {self.length}, 'sigma': {self.sigma}, " f"'amplitude': {self.amplitude}, 'zero_at_edges': {self.zero_at_edges})" ) @property def parameters(self) -> list[FreeParameterExpression | FreeParameter | float]: """Returns the parameters associated with the object, either unbound free parameter expressions or bound values. """ return [self.length, self.sigma, self.amplitude]
[docs] def bind_values(self, **kwargs: FreeParameter | str) -> GaussianWaveform: """Takes in parameters and returns an object with specified parameters replaced with their values. Args: **kwargs (FreeParameter | str): Arbitrary keyword arguments. Returns: GaussianWaveform: A copy of this waveform with the requested parameters bound. """ constructor_kwargs = { "length": subs_if_free_parameter(self.length, **kwargs), "sigma": subs_if_free_parameter(self.sigma, **kwargs), "amplitude": subs_if_free_parameter(self.amplitude, **kwargs), "zero_at_edges": self.zero_at_edges, "id": self.id, } return GaussianWaveform(**constructor_kwargs)
def __eq__(self, other: GaussianWaveform): return isinstance(other, GaussianWaveform) and ( self.length, self.sigma, self.amplitude, self.zero_at_edges, self.id, ) == (other.length, other.sigma, other.amplitude, other.zero_at_edges, other.id) def _to_oqpy_expression(self) -> OQPyExpression: """Returns an OQPyExpression defining this waveform. Returns: OQPyExpression: The OQPyExpression. """ gaussian_generator = declare_waveform_generator( "gaussian", [ ("length", duration), ("sigma", duration), ("amplitude", float64), ("zero_at_edges", bool_), ], ) return WaveformVar( init_expression=gaussian_generator( self.length, self.sigma, self.amplitude, self.zero_at_edges, ), name=self.id, )
[docs] def sample(self, dt: float) -> np.ndarray: """Generates a sample of amplitudes for this Waveform based on the given time resolution. Args: dt (float): The time resolution. Returns: np.ndarray: The sample amplitudes for this waveform. """ sample_range = np.arange(0, self.length, dt) t0 = self.length / 2 zero_at_edges_int = int(self.zero_at_edges) return ( self.amplitude / (1 - zero_at_edges_int * np.exp(-0.5 * ((self.length / (2 * self.sigma)) ** 2))) ) * ( np.exp(-0.5 * (((sample_range - t0) / self.sigma) ** 2)) - zero_at_edges_int * np.exp(-0.5 * ((self.length / (2 * self.sigma)) ** 2)) )
@staticmethod def _from_calibration_schema(waveform_json: dict) -> GaussianWaveform: waveform_parameters = {"id": waveform_json["waveformId"]} for val in waveform_json["arguments"]: waveform_parameters[val["name"]] = ( float(val["value"]) if val["type"] == "float" else FreeParameterExpression(val["value"]) ) return GaussianWaveform(**waveform_parameters)
[docs] class ErfSquareWaveform(Waveform, Parameterizable): """A square waveform with smoothed edges.""" def __init__( self, length: float | FreeParameterExpression, width: float | FreeParameterExpression, sigma: float | FreeParameterExpression, off_center: float | FreeParameterExpression = 0, amplitude: float | FreeParameterExpression = 1, zero_at_edges: bool = False, id: str | None = None, ): r"""Initializes a `ErfSquareWaveform`. .. math:: (\text{step}((t-t_1)/sigma) + \text{step}(-(t-t_2)/sigma) - 1) where :math:`\text{step}(t)` is the rounded step function defined as :math:`(erf(t)+1)/2` and :math:`t_1` and :math:`t_2` are the timestamps at the half height. The waveform is scaled such that its maximum is equal to `amplitude`. Args: length (float | FreeParameterExpression): Duration (in seconds) from the start to the end of the waveform. width (float | FreeParameterExpression): Duration (in seconds) between the half height of the two edges. sigma (float | FreeParameterExpression): A characteristic time of how quickly the edges rise and fall. off_center (float | FreeParameterExpression): Shift the smoothed square waveform earlier or later in time. When positive, the smoothed square is shifted later (to the right), otherwise earlier (to the left). Defaults to 0. amplitude (float | FreeParameterExpression): The amplitude of the waveform envelope. Defaults to 1. zero_at_edges (bool): Whether the waveform is scaled such that it has zero value at the edges. Defaults to False. id (str | None): The identifier used for declaring this waveform. A random string of ascii characters is assigned by default. """ self.length = length self.width = width self.sigma = sigma self.off_center = off_center self.amplitude = amplitude self.zero_at_edges = zero_at_edges self.id = id or _make_identifier_name() def __repr__(self) -> str: return ( f"ErfSquareWaveform('id': {self.id}, 'length': {self.length}, " f"'width': {self.width}, 'sigma': {self.sigma}, 'off_center': {self.off_center}, " f"'amplitude': {self.amplitude}, 'zero_at_edges': {self.zero_at_edges})" ) @property def parameters(self) -> list[FreeParameterExpression | FreeParameter | float]: """Returns the parameters associated with the object, either unbound free parameter expressions or bound values. """ return [self.length, self.width, self.sigma, self.off_center, self.amplitude]
[docs] def bind_values(self, **kwargs: FreeParameter | str) -> ErfSquareWaveform: """Takes in parameters and returns an object with specified parameters replaced with their values. Args: **kwargs (FreeParameter | str): Arbitrary keyword arguments. Returns: ErfSquareWaveform: A copy of this waveform with the requested parameters bound. """ constructor_kwargs = { "length": subs_if_free_parameter(self.length, **kwargs), "width": subs_if_free_parameter(self.width, **kwargs), "sigma": subs_if_free_parameter(self.sigma, **kwargs), "off_center": subs_if_free_parameter(self.off_center, **kwargs), "amplitude": subs_if_free_parameter(self.amplitude, **kwargs), "zero_at_edges": self.zero_at_edges, "id": self.id, } return ErfSquareWaveform(**constructor_kwargs)
def __eq__(self, other: ErfSquareWaveform): return isinstance(other, ErfSquareWaveform) and ( self.length, self.width, self.sigma, self.off_center, self.amplitude, self.zero_at_edges, self.id, ) == ( other.length, other.width, other.sigma, other.off_center, other.amplitude, other.zero_at_edges, other.id, ) def _to_oqpy_expression(self) -> OQPyExpression: """Returns an OQPyExpression defining this waveform. Returns: OQPyExpression: The OQPyExpression. """ erf_square_generator = declare_waveform_generator( "erf_square", [ ("length", duration), ("width", duration), ("sigma", duration), ("off_center", duration), ("amplitude", float64), ("zero_at_edges", bool_), ], ) return WaveformVar( init_expression=erf_square_generator( self.length, self.width, self.sigma, self.off_center, self.amplitude, self.zero_at_edges, ), name=self.id, )
[docs] def sample(self, dt: float) -> np.ndarray: """Generates a sample of amplitudes for this Waveform based on the given time resolution. Args: dt (float): The time resolution. Returns: np.ndarray: The sample amplitudes for this waveform. """ sample_range = np.arange(0, self.length, dt) t1 = (self.length - self.width) / 2 + self.off_center t2 = (self.length + self.width) / 2 + self.off_center samples = ( sp.special.erf((sample_range - t1) / self.sigma) + sp.special.erf(-(sample_range - t2) / self.sigma) ) / 2 mid_waveform_height = sp.special.erf((self.width / 2) / self.sigma) waveform_bottom = (sp.special.erf(-t1 / self.sigma) + sp.special.erf(t2 / self.sigma)) / 2 if self.zero_at_edges: return ( (samples - waveform_bottom) / (mid_waveform_height - waveform_bottom) * self.amplitude ) return samples * self.amplitude / mid_waveform_height
@staticmethod def _from_calibration_schema(waveform_json: dict) -> ErfSquareWaveform: waveform_parameters = {"id": waveform_json["waveformId"]} for val in waveform_json["arguments"]: waveform_parameters[val["name"]] = ( float(val["value"]) if val["type"] == "float" else FreeParameterExpression(val["value"]) ) return ErfSquareWaveform(**waveform_parameters)
def _make_identifier_name() -> str: return "".join([random.choice(string.ascii_letters) for _ in range(10)]) # noqa: S311 def _parse_waveform_from_calibration_schema(waveform: dict) -> Waveform: waveform_names = { "arbitrary": ArbitraryWaveform._from_calibration_schema, "drag_gaussian": DragGaussianWaveform._from_calibration_schema, "gaussian": GaussianWaveform._from_calibration_schema, "constant": ConstantWaveform._from_calibration_schema, "erf_square": ErfSquareWaveform._from_calibration_schema, } if "amplitudes" in waveform: waveform["name"] = "arbitrary" if waveform["name"] in waveform_names: return waveform_names[waveform["name"]](waveform) waveform_id = waveform["waveformId"] raise ValueError(f"The waveform {waveform_id} of cannot be constructed")