Source code for pyquil.quiltwaveforms
from copy import copy
from dataclasses import dataclass
from numbers import Complex, Real
from typing import Callable, Dict, Union, List, Optional, no_type_check
import numpy as np
from scipy.special import erf
from pyquil.quilatom import TemplateWaveform, _update_envelope, _complex_str, Expression, substitute
_waveform_classes: Dict[str, type] = {}
"""A mapping from Quil-T waveform names to their corresponding classes.
This should not be mutated directly, but rather filled by the @waveform
decorator.
"""
[docs]def waveform(name: str) -> Callable[[type], type]:
"""Define a Quil-T wavefom with the given name."""
def wrap(cls: type) -> type:
cls: type = dataclass(cls)
_waveform_classes[name] = cls
return cls
return wrap
@no_type_check
def _wf_from_dict(name: str, params: Dict[str, Union[Expression, Real, Complex]]) -> TemplateWaveform:
"""Construct a TemplateWaveform from a name and a dictionary of properties.
:param name: The Quil-T name of the template.
:param params: A mapping from parameter names to their corresponding values.
:returns: A template waveform.
"""
params = copy(params)
if name not in _waveform_classes:
raise ValueError(f"Unknown template waveform {name}.")
cls = _waveform_classes[name]
fields = getattr(cls, "__dataclass_fields__", {})
for param, value in params.items():
if param not in fields:
raise ValueError(f"Unexpected parameter '{param}' in {name}.")
if isinstance(value, Expression):
value = substitute(value, {})
if isinstance(value, Real):
# normalize to float
params[param] = float(value)
elif isinstance(value, Complex):
# no normalization needed
pass
else:
raise ValueError(f"Unable to resolve parameter '{param}' in template {name} to a constant value.")
for field, spec in fields.items():
if field not in params and spec.default is not None:
raise ValueError(f"Missing parameter '{field}' in {name}.")
return cls(**params)
def _optional_field_strs(wf: TemplateWaveform) -> List[str]:
"""Get the printed representations of optional template parameters."""
result = []
for field, spec in getattr(wf, "__dataclass_fields__", {}).items():
if spec.default is None:
value = getattr(wf, field, None)
if value is not None:
result.append(f"{field}: {value}")
return result
[docs]@waveform("flat")
class FlatWaveform(TemplateWaveform):
"""
A flat (constant) waveform.
"""
iq: Complex
""" A raw IQ value. """
scale: Optional[float] = None
""" An optional global scaling factor. """
phase: Optional[float] = None
""" An optional phase shift factor. """
detuning: Optional[float] = None
""" An optional frequency detuning factor. """
[docs] def out(self) -> str:
output = "flat("
output += ", ".join([f"duration: {self.duration}", f"iq: {_complex_str(self.iq)}"] + _optional_field_strs(self))
output += ")"
return output
def __str__(self) -> str:
return self.out()
[docs] def samples(self, rate: float) -> np.ndarray:
iqs = np.full(self.num_samples(rate), self.iq, dtype=np.complex128)
return _update_envelope(iqs, rate, scale=self.scale, phase=self.phase, detuning=self.detuning)
[docs]@waveform("gaussian")
class GaussianWaveform(TemplateWaveform):
"""A Gaussian pulse."""
fwhm: float
""" The Full-Width-Half-Max of the Gaussian (seconds). """
t0: float
""" The center time coordinate of the Gaussian (seconds). """
scale: Optional[float] = None
""" An optional global scaling factor. """
phase: Optional[float] = None
""" An optional phase shift factor. """
detuning: Optional[float] = None
""" An optional frequency detuning factor. """
[docs] def out(self) -> str:
output = "gaussian("
output += ", ".join(
[f"duration: {self.duration}", f"fwhm: {self.fwhm}", f"t0: {self.t0}"] + _optional_field_strs(self)
)
output += ")"
return output
def __str__(self) -> str:
return self.out()
[docs] def samples(self, rate: float) -> np.ndarray:
ts = np.arange(self.num_samples(rate), dtype=np.complex128) / rate
sigma = 0.5 * self.fwhm / np.sqrt(2.0 * np.log(2.0))
iqs = np.exp(-0.5 * (ts - self.t0) ** 2 / sigma**2)
return _update_envelope(iqs, rate, scale=self.scale, phase=self.phase, detuning=self.detuning)
[docs]@waveform("drag_gaussian")
class DragGaussianWaveform(TemplateWaveform):
"""A DRAG Gaussian pulse."""
fwhm: float
""" The Full-Width-Half-Max of the gaussian (seconds). """
t0: float
""" The center time coordinate of the Gaussian (seconds). """
anh: float
""" The anharmonicity of the qubit, f01-f12 (Hertz). """
alpha: float
""" Dimensionles DRAG parameter. """
scale: Optional[float] = None
""" An optional global scaling factor. """
phase: Optional[float] = None
""" An optional phase shift factor. """
detuning: Optional[float] = None
""" An optional frequency detuning factor. """
[docs] def out(self) -> str:
output = "drag_gaussian("
output += ", ".join(
[
f"duration: {self.duration}",
f"fwhm: {self.fwhm}",
f"t0: {self.t0}",
f"anh: {self.anh}",
f"alpha: {self.alpha}",
]
+ _optional_field_strs(self)
)
output += ")"
return output
def __str__(self) -> str:
return self.out()
[docs] def samples(self, rate: float) -> np.ndarray:
ts = np.arange(self.num_samples(rate), dtype=np.complex128) / rate
sigma = 0.5 * self.fwhm / np.sqrt(2.0 * np.log(2.0))
env = np.exp(-0.5 * (ts - self.t0) ** 2 / sigma**2)
env_der = (self.alpha * (1.0 / (2 * np.pi * self.anh * sigma**2))) * (ts - self.t0) * env
iqs = env + 1.0j * env_der
return _update_envelope(iqs, rate, scale=self.scale, phase=self.phase, detuning=self.detuning)
[docs]@waveform("hrm_gaussian")
class HrmGaussianWaveform(TemplateWaveform):
"""A Hermite Gaussian waveform.
REFERENCE: Effects of arbitrary laser or NMR pulse shapes on population
inversion and coherence Warren S. Warren. 81, (1984); doi:
10.1063/1.447644
"""
fwhm: float
""" The Full-Width-Half-Max of the Gaussian (seconds). """
t0: float
""" The center time coordinate of the Gaussian (seconds). """
anh: float
""" The anharmonicity of the qubit, f01-f12 (Hertz). """
alpha: float
""" Dimensionles DRAG parameter. """
second_order_hrm_coeff: float
""" Second order coefficient (see Warren 1984). """
scale: Optional[float] = None
""" An optional global scaling factor. """
phase: Optional[float] = None
""" An optional phase shift factor. """
detuning: Optional[float] = None
""" An optional frequency detuning factor. """
[docs] def out(self) -> str:
output = "hrm_gaussian("
output += ", ".join(
[
f"duration: {self.duration}",
f"fwhm: {self.fwhm}",
f"t0: {self.t0}",
f"anh: {self.anh}",
f"alpha: {self.alpha}",
f"second_order_hrm_coeff: {self.second_order_hrm_coeff}",
]
+ _optional_field_strs(self)
)
output += ")"
return output
def __str__(self) -> str:
return self.out()
[docs] def samples(self, rate: float) -> np.ndarray:
ts = np.arange(self.num_samples(rate), dtype=np.complex128) / rate
sigma = 0.5 * self.fwhm / np.sqrt(2.0 * np.log(2.0))
exponent_of_t = 0.5 * (ts - self.t0) ** 2 / sigma**2
gauss = np.exp(-exponent_of_t)
env = (1 - self.second_order_hrm_coeff * exponent_of_t) * gauss
deriv_prefactor = -self.alpha / (2 * np.pi * self.anh)
env_der = (
deriv_prefactor
* (ts - self.t0)
/ (sigma**2)
* gauss
* (self.second_order_hrm_coeff * (exponent_of_t - 1) - 1)
)
iqs = env + 1.0j * env_der
return _update_envelope(iqs, rate, scale=self.scale, phase=self.phase, detuning=self.detuning)
[docs]@waveform("erf_square")
class ErfSquareWaveform(TemplateWaveform):
"""A pulse with a flat top and edges that are error functions (erf)."""
risetime: float
""" The width of each of the rise and fall sections of the pulse (seconds). """
pad_left: float
""" Amount of zero-padding to add to the left of the pulse (seconds)."""
pad_right: float
""" Amount of zero-padding to add to the right of the pulse (seconds). """
scale: Optional[float] = None
""" An optional global scaling factor. """
phase: Optional[float] = None
""" An optional phase shift factor. """
detuning: Optional[float] = None
""" An optional frequency detuning factor. """
[docs] def out(self) -> str:
output = "erf_square("
output += ", ".join(
[
f"duration: {self.duration}",
f"risetime: {self.risetime}",
f"pad_left: {self.pad_left}",
f"pad_right: {self.pad_right}",
]
+ _optional_field_strs(self)
)
output += ")"
return output
def __str__(self) -> str:
return self.out()
[docs] def samples(self, rate: float) -> np.ndarray:
ts = np.arange(self.num_samples(rate), dtype=np.complex128) / rate
fwhm = 0.5 * self.risetime
t1 = fwhm
t2 = self.duration - fwhm
sigma = 0.5 * fwhm / np.sqrt(2.0 * np.log(2.0))
vals = 0.5 * (erf((ts - t1) / sigma) - erf((ts - t2) / sigma))
zeros_left = np.zeros(int(np.ceil(self.pad_left * rate)), dtype=np.complex128)
zeros_right = np.zeros(int(np.ceil(self.pad_right * rate)), dtype=np.complex128)
iqs = np.concatenate((zeros_left, vals, zeros_right)) # type: ignore
return _update_envelope(iqs, rate, scale=self.scale, phase=self.phase, detuning=self.detuning)
[docs]@waveform("boxcar_kernel")
class BoxcarAveragerKernel(TemplateWaveform):
scale: Optional[float] = None
""" An optional global scaling factor. """
phase: Optional[float] = None
""" An optional phase shift factor. """
detuning: Optional[float] = None
""" An optional frequency detuning factor. """
[docs] def out(self) -> str:
output = "boxcar_kernel("
output += ", ".join([f"duration: {self.duration}"] + _optional_field_strs(self))
output += ")"
return output
def __str__(self) -> str:
return self.out()
[docs] def samples(self, rate: float) -> np.ndarray:
n = self.num_samples(rate)
iqs = np.full(n, 1.0 / n, dtype=np.complex128)
return _update_envelope(iqs, rate, scale=self.scale, phase=self.phase, detuning=self.detuning)