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
from typing import Optional, Union

import numpy as np
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 # noqa: E501 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: Optional[str] = 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 (Optional[str]): 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: Union[float, FreeParameterExpression], iq: complex, id: Optional[str] = None ): """Initializes a `ConstantWaveform`. Args: length (Union[float, FreeParameterExpression]): Value (in seconds) specifying the duration of the waveform. iq (complex): complex value specifying the amplitude of the waveform. id (Optional[str]): 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[Union[FreeParameterExpression, FreeParameter, float]]: """Returns the parameters associated with the object, either unbound free parameter expressions or bound values. Returns: list[Union[FreeParameterExpression, FreeParameter, float]]: a list of parameters. """ return [self.length]
[docs] def bind_values(self, **kwargs: Union[FreeParameter, str]) -> ConstantWaveform: """Takes in parameters and returns an object with specified parameters replaced with their values. Args: **kwargs (Union[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) samples = self.iq * np.ones_like(sample_range) return samples
@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: Union[float, FreeParameterExpression], sigma: Union[float, FreeParameterExpression], beta: Union[float, FreeParameterExpression], amplitude: Union[float, FreeParameterExpression] = 1, zero_at_edges: bool = False, id: Optional[str] = None, ): """Initializes a `DragGaussianWaveform`. Args: length (Union[float, FreeParameterExpression]): Value (in seconds) specifying the duration of the waveform. sigma (Union[float, FreeParameterExpression]): A measure (in seconds) of how wide or narrow the Gaussian peak is. beta (Union[float, FreeParameterExpression]): The correction amplitude. amplitude (Union[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 (Optional[str]): 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[Union[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: Union[FreeParameter, str]) -> DragGaussianWaveform: """Takes in parameters and returns an object with specified parameters replaced with their values. Args: **kwargs (Union[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) samples = ( (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)) ) ) return samples
@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: Union[float, FreeParameterExpression], sigma: Union[float, FreeParameterExpression], amplitude: Union[float, FreeParameterExpression] = 1, zero_at_edges: bool = False, id: Optional[str] = None, ): """Initializes a `GaussianWaveform`. Args: length (Union[float, FreeParameterExpression]): Value (in seconds) specifying the duration of the waveform. sigma (Union[float, FreeParameterExpression]): A measure (in seconds) of how wide or narrow the Gaussian peak is. amplitude (Union[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 (Optional[str]): 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[Union[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: Union[FreeParameter, str]) -> GaussianWaveform: """Takes in parameters and returns an object with specified parameters replaced with their values. Args: **kwargs (Union[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) samples = ( 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)) ) return samples
@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)
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, } 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")