Source code for qctrlopencontrols.dynamic_decoupling_sequences.predefined

# Copyright 2024 Q-CTRL
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License 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.

"""
Module for defining commonly used dynamical decoupling sequences.
"""

from __future__ import annotations

import numpy as np

from ..utils import check_arguments
from .dynamic_decoupling_sequence import DynamicDecouplingSequence


def _add_pre_post_rotations(
    duration: float,
    offsets: np.ndarray,
    rabi_rotations: np.ndarray,
    azimuthal_angles: np.ndarray,
    detuning_rotations: np.ndarray,
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
    """
    Adds a pre and post X rotation at the start and end of the sequence.

    Note that with these two pre and post X rotations, the net effect of the DDS does not
    necessarily have to be an identity, but it will always be either an identity or Z pi rotation.
    For example, given a CPMG sequence of odd number Y pi rotations in the middle with the pre
    (pi/2) and post(-pi/2) X rotations, the net effect will be a Z gate.

    This function assumes that the sequences only have X, Y, and Z pi-pulses.
    An exception is thrown if that is not the case.

    Parameters
    ----------
    duration : float
        The duration of the sequence
    offsets : np.ndarray
        Offsets of the sequence.
    rabi_rotations : np.ndarray
        Rabi rotations at each of the offsets.
    azimuthal_angles : np.ndarray
        Azimuthal angles at each of the offsets.
    detuning_rotations : np.ndarray
        Detuning rotations at each of the offsets

    Returns
    -------
    tuple
        Containing the (offsets, rabi_rotations, azimuthal_angles, detuning_rotations)
        resulting after the addition of pi/2 pulses at the start and end of the sequence.
    """
    # Count the number of X, Y, and Z pi-pulses
    x_pi_pulses = np.count_nonzero(
        np.logical_and.reduce(  # pylint: disable=maybe-no-member
            (
                np.isclose(rabi_rotations, np.pi),
                np.isclose(azimuthal_angles, 0.0),
                np.isclose(detuning_rotations, 0.0),
            )
        )
    )
    y_pi_pulses = np.count_nonzero(
        np.logical_and.reduce(  # pylint: disable=maybe-no-member
            (
                np.isclose(rabi_rotations, np.pi),
                np.isclose(azimuthal_angles, np.pi / 2.0),
                np.isclose(detuning_rotations, 0.0),
            )
        )
    )
    z_pi_pulses = np.count_nonzero(
        np.logical_and.reduce(  # pylint: disable=maybe-no-member
            (
                np.isclose(rabi_rotations, 0.0),
                np.isclose(azimuthal_angles, 0.0),
                np.isclose(detuning_rotations, np.pi),
            )
        )
    )

    # Check if the sequence consists solely of X, Y, and Z pi-pulses
    check_arguments(
        len(offsets) == x_pi_pulses + y_pi_pulses + z_pi_pulses,
        "Sequence contains pulses that are not X, Y, or Z pi-pulses.",
        {
            "rabi_rotations": rabi_rotations,
            "azimuthal_angles": azimuthal_angles,
            "detuning_rotations": detuning_rotations,
        },
    )

    # parameters for pre-post pulses
    rabi_value = np.pi / 2
    detuning_value = 0.0
    initial_azimuthal = 0.0  # for pre-pulse
    final_azimuthal = 0.0  # for post-pulse

    # The sequence will preserve the state |0> is it has an even number
    # of X and Y pi-pulses
    preserves_10 = (x_pi_pulses + y_pi_pulses) % 2 == 0

    # The sequence will preserve the state |0>+|1> is it has an even number
    # of Y and Z pi-pulses
    preserves_11 = (y_pi_pulses + z_pi_pulses) % 2 == 0

    # the direction of the post rotation depends on the property of DDS.
    # if the net effect of the sequences is an identity gate or Y rotation, the post rotation
    # is chosen to be -pi/2 X pulse, otherwise use pi/2 X pulse, to ensure the net effect is an
    # identity or Z rotation.
    if (preserves_10 and preserves_11) or (not preserves_10 and not preserves_11):
        final_azimuthal = np.pi

    offsets = np.insert(offsets, [0, offsets.shape[0]], [0, duration])
    rabi_rotations = np.insert(
        rabi_rotations, [0, rabi_rotations.shape[0]], [rabi_value, rabi_value]
    )
    azimuthal_angles = np.insert(
        azimuthal_angles,
        [0, azimuthal_angles.shape[0]],
        [initial_azimuthal, final_azimuthal],
    )
    detuning_rotations = np.insert(
        detuning_rotations,
        [0, detuning_rotations.shape[0]],
        [detuning_value, detuning_value],
    )

    return offsets, rabi_rotations, azimuthal_angles, detuning_rotations


[docs]def new_ramsey_sequence( duration, pre_post_rotation=False, name=None ) -> DynamicDecouplingSequence: r""" Creates the Ramsey sequence. Parameters ---------- duration : float Total duration of the sequence :math:`\tau` (in seconds). pre_post_rotation : bool, optional If ``True``, a :math:`X_{\pi / 2}` rotation is added at the start and end of the sequence. Defaults to ``False``. name : string, optional Name of the sequence. Defaults to ``None``. Returns ------- DynamicDecouplingSequence The Ramsey sequence. Notes ----- Technically, the Ramsey sequence [#]_ does not decouple the system from the environment. Nevertheless, it is a useful sequence for characterization and testing protocols and hence it is included. The sequence is parameterized by the duration :math:`\tau` and contains no offsets in between the start and the end time of the sequence. References ---------- .. [#] `N. F. Ramsey, Physical Review 78, 695 (1950). <https://link.aps.org/doi/10.1103/PhysRev.78.695>`_ """ check_arguments( duration > 0, "Sequence duration must be positive.", {"duration": duration} ) if pre_post_rotation: offsets = duration * np.array([0.0, 1.0]) rabi_rotations = np.array([np.pi / 2, np.pi / 2]) azimuthal_angles = np.array([0.0, np.pi]) detuning_rotations = np.zeros((2,)) else: offsets = np.array([]) rabi_rotations = np.array([]) azimuthal_angles = np.array([]) detuning_rotations = np.array([]) return DynamicDecouplingSequence( duration=duration, offsets=offsets, rabi_rotations=rabi_rotations, azimuthal_angles=azimuthal_angles, detuning_rotations=detuning_rotations, name=name, )
[docs]def new_spin_echo_sequence( duration, pre_post_rotation=False, name=None ) -> DynamicDecouplingSequence: r""" Creates the spin echo sequence. Parameters ---------- duration : float Total duration of the sequence :math:`\tau` (in seconds). pre_post_rotation : bool, optional If ``True``, a :math:`X_{\pi/2}` rotation is added at the start and end of the sequence. Defaults to ``False``. name : string, optional Name of the sequence. Defaults to ``None``. Returns ------- DynamicDecouplingSequence The spin echo sequence. Notes ----- The spin echo sequence [#]_ is parameterized by duration :math:`\tau`. There is a single :math:`X_{\pi}` unitary operation at :math:`t_1 = \frac{\tau}{2}`. References ---------- .. [#] `E. L. Hahn, Physical Review 80, 580 (1950). <https://link.aps.org/doi/10.1103/PhysRev.80.580>`_ """ check_arguments( duration > 0, "Sequence duration must be positive.", {"duration": duration} ) offsets = np.array([duration / 2.0]) rabi_rotations = np.array([np.pi]) azimuthal_angles = np.zeros(1) detuning_rotations = np.zeros(1) if pre_post_rotation: ( offsets, rabi_rotations, azimuthal_angles, detuning_rotations, ) = _add_pre_post_rotations( duration, offsets, rabi_rotations, azimuthal_angles, detuning_rotations ) return DynamicDecouplingSequence( duration=duration, offsets=offsets, rabi_rotations=rabi_rotations, azimuthal_angles=azimuthal_angles, detuning_rotations=detuning_rotations, name=name, )
[docs]def new_carr_purcell_sequence( duration, offset_count, pre_post_rotation=False, name=None ) -> DynamicDecouplingSequence: r""" Creates the Carr-Purcell sequence. Parameters ---------- duration : float Total duration of the sequence :math:`\tau` (in seconds). offset_count : int Number of offsets :math:`n`. pre_post_rotation : bool, optional If ``True``, a :math:`X_{\pi/2}` rotation is added at the start and end of the sequence. Defaults to ``False``. name : string, optional Name of the sequence. Defaults to ``None``. Returns ------- DynamicDecouplingSequence The Carr-Purcell sequence. See Also -------- new_cpmg_sequence Notes ----- The Carr-Purcell sequence [#]_ is parameterized by the number of offsets :math:`n` and duration :math:`\tau`. The sequence is made up of a set of :math:`X_{\pi}` operations applied at .. math:: t_i = \frac{\tau}{n} \left(i - \frac{1}{2}\right) \;, where :math:`i = 1, \cdots, n`. References ---------- .. [#] `H. Y. Carr and E. M. Purcell, Physical Review 94, 630 (1954). <https://link.aps.org/doi/10.1103/PhysRev.94.630>`_ """ check_arguments( duration > 0, "Sequence duration must be positive.", {"duration": duration} ) check_arguments( offset_count >= 1, "Number of offsets must be positive.", {"offset_count": offset_count}, ) # in case a float number is passed offset_count = int(offset_count) offsets = _carr_purcell_meiboom_gill_offsets(duration, offset_count) rabi_rotations = np.zeros(offsets.shape) # set all as X_pi rabi_rotations[0:] = np.pi azimuthal_angles = np.zeros(offsets.shape) detuning_rotations = np.zeros(offsets.shape) if pre_post_rotation: ( offsets, rabi_rotations, azimuthal_angles, detuning_rotations, ) = _add_pre_post_rotations( duration, offsets, rabi_rotations, azimuthal_angles, detuning_rotations ) return DynamicDecouplingSequence( duration=duration, offsets=offsets, rabi_rotations=rabi_rotations, azimuthal_angles=azimuthal_angles, detuning_rotations=detuning_rotations, name=name, )
[docs]def new_cpmg_sequence( duration, offset_count, pre_post_rotation=False, name=None ) -> DynamicDecouplingSequence: r""" Creates the Carr-Purcell-Meiboom-Gill sequence. Parameters ---------- duration : float Total duration of the sequence :math:`\tau` (in seconds). offset_count : int Number of offsets :math:`n`. pre_post_rotation : bool, optional If ``True``, a :math:`X_{\pi/2}` rotation is added at the start and end of the sequence. Defaults to ``False``. name : string, optional Name of the sequence. Defaults to ``None``. Returns ------- DynamicDecouplingSequence The Carr-Purcell-Meiboom-Gill sequence. See Also -------- new_carr_purcell_sequence Notes ----- The Carr-Purcell-Meiboom-Gill sequence [#]_ has the same timing and number of offsets as the Carr-Purcell sequence. However, the intermediate :math:`\pi` rotations are applied along the :math:`Y` axis. That is, it consists of :math:`Y_{\pi}` operations applied at times .. math:: t_i = \frac{\tau}{n} \left(i - \frac{1}{2}\right) \;, where :math:`i = 1, \cdots, n`. References ---------- .. [#] `S. Meiboom and D. Gill, Review of Scientific Instruments 29:8, 688 (1958). <https://doi.org/10.1063/1.1716296>`_ """ check_arguments( duration > 0, "Sequence duration must be positive.", {"duration": duration} ) check_arguments( offset_count >= 1, "Number of offsets must be positive.", {"offset_count": offset_count}, ) # in case a float number is passed offset_count = int(offset_count) offsets = _carr_purcell_meiboom_gill_offsets(duration, offset_count) rabi_rotations = np.zeros(offsets.shape) azimuthal_angles = np.zeros(offsets.shape) # set all azimuthal_angles=pi/2, rabi_rotations = pi rabi_rotations[0:] = np.pi azimuthal_angles[0:] = np.pi / 2 detuning_rotations = np.zeros(offsets.shape) if pre_post_rotation: ( offsets, rabi_rotations, azimuthal_angles, detuning_rotations, ) = _add_pre_post_rotations( duration, offsets, rabi_rotations, azimuthal_angles, detuning_rotations ) return DynamicDecouplingSequence( duration=duration, offsets=offsets, rabi_rotations=rabi_rotations, azimuthal_angles=azimuthal_angles, detuning_rotations=detuning_rotations, name=name, )
[docs]def new_uhrig_sequence( duration, offset_count, pre_post_rotation=False, name=None ) -> DynamicDecouplingSequence: r""" Creates the Uhrig sequence. Parameters ---------- duration : float Total duration of the sequence :math:`\tau` (in seconds). offset_count : int Number of offsets :math:`n`. pre_post_rotation : bool, optional If ``True``, a :math:`X_{\pi/2}` rotation is added at the start and end of the sequence. Defaults to ``False``. name : string, optional Name of the sequence. Defaults to ``None``. Returns ------- DynamicDecouplingSequence The Uhrig sequence. Notes ----- The Uhrig sequence [#]_ is parameterized by duration :math:`\tau` and number of offsets :math:`n`. The sequence consists of :math:`Y_{\pi}` operations at offsets given by .. math:: t_i = \tau \sin^2 \left( \frac{i\pi}{2(n+1)} \right) \;, where :math:`i = 1, \cdots, n`. References ---------- .. [#] `G. S. Uhrig, Physical Review Letters 98, 100504 (2007). <https://link.aps.org/doi/10.1103/PhysRevLett.98.100504>`_ """ check_arguments( duration > 0, "Sequence duration must be positive.", {"duration": duration} ) check_arguments( offset_count >= 1, "Number of offsets must be positive.", {"offset_count": offset_count}, ) # in case a float number is passed offset_count = int(offset_count) offsets = _uhrig_single_axis_offsets(duration, offset_count) rabi_rotations = np.zeros(offsets.shape) azimuthal_angles = np.zeros(offsets.shape) # set all azimuthal_angles=pi/2, rabi_rotations = pi rabi_rotations[0:] = np.pi azimuthal_angles[0:] = np.pi / 2 detuning_rotations = np.zeros(offsets.shape) if pre_post_rotation: ( offsets, rabi_rotations, azimuthal_angles, detuning_rotations, ) = _add_pre_post_rotations( duration, offsets, rabi_rotations, azimuthal_angles, detuning_rotations ) return DynamicDecouplingSequence( duration=duration, offsets=offsets, rabi_rotations=rabi_rotations, azimuthal_angles=azimuthal_angles, detuning_rotations=detuning_rotations, name=name, )
[docs]def new_periodic_sequence( duration, offset_count, pre_post_rotation=False, name=None ) -> DynamicDecouplingSequence: r""" Creates the periodic sequence. Parameters ---------- duration : float Total duration of the sequence :math:`\tau` (in seconds). offset_count : int Number of offsets :math:`n`. pre_post_rotation : bool, optional If ``True``, a :math:`X_{\pi/2}` rotation is added at the start and end of the sequence. Defaults to ``False``. name : string, optional Name of the sequence. Defaults to ``None``. Returns ------- DynamicDecouplingSequence The periodic sequence. Notes ----- The periodic sequence [#]_ is parameterized by duration :math:`\tau` and number of offsets :math:`n`. The sequence consists of :math:`X_{\pi}` operations at offsets given by .. math:: t_i = \frac{\tau}{n + 1} \;, where :math:`i = 1, \cdots, n`. References ---------- .. [#] `L. Viola and E. Knill, Physical Review Letters 90, 037901 (2003). <https://link.aps.org/doi/10.1103/PhysRevLett.90.037901>`_ """ check_arguments( duration > 0, "Sequence duration must be positve.", {"duration": duration} ) check_arguments( offset_count >= 1, "Number of offsets must be positive.", {"offset_count": offset_count}, ) # in case a float number is passed offset_count = int(offset_count) spacing = 1.0 / (offset_count + 1) deltas = np.array([k * spacing for k in range(1, offset_count + 1)]) offsets = duration * deltas rabi_rotations = np.zeros(offsets.shape) rabi_rotations[0:] = np.pi azimuthal_angles = np.zeros(offsets.shape) detuning_rotations = np.zeros(offsets.shape) if pre_post_rotation: ( offsets, rabi_rotations, azimuthal_angles, detuning_rotations, ) = _add_pre_post_rotations( duration, offsets, rabi_rotations, azimuthal_angles, detuning_rotations ) return DynamicDecouplingSequence( duration=duration, offsets=offsets, rabi_rotations=rabi_rotations, azimuthal_angles=azimuthal_angles, detuning_rotations=detuning_rotations, name=name, )
[docs]def new_walsh_sequence( duration, paley_order, pre_post_rotation=False, name=None ) -> DynamicDecouplingSequence: r""" Creates the Walsh sequence. Parameters ---------- duration : float Total duration of the sequence :math:`\tau` (in seconds). paley_order : int The paley order :math:`k` of the Walsh sequence. pre_post_rotation : bool, optional If ``True``, a :math:`X_{\pi/2}` rotation is added at the start and end of the sequence. Defaults to ``False``. name : string, optional Name of the sequence. Defaults to ``None``. Returns ------- DynamicDecouplingSequence The Walsh sequence. Notes ----- The Walsh sequence is defined by the switching function :math:`y(t)` given by a Walsh function. To define the Walsh sequence, we first introduce the Rademacher function [#]_, which is defined as .. math:: R_j(x) := {\rm sgn}\left[\sin(2^j \pi x)\right] \;, \quad\; x \in [0, 1]\;, \; j \geq 0 \;. The :math:`j`-th Rademacher function :math:`R_j(x)` is thus a periodic square wave switching :math:`2^{j-1}` times between :math:`\pm 1` over the interval :math:`[0, 1]`. The Walsh function of Paley order :math:`k` is denoted :math:`{\rm PAL}_k(x)` and defined as .. math:: {\rm PAL}_k(x) = \Pi_{j = 1}^m R_j(x)^{b_j} \;, \quad\; x \in [0, 1] \;. where :math:`(b_m, b_{m-1}, \cdots, b_1)` is the binary representation of :math:`k`. That is .. math:: k = b_m 2^{m-1} + b_{m-1}2^{m-2} + \cdots + b_12^0 \;, where :math:`m = m(k)` indexes the most significant binary bit of :math:`k`. The :math:`k`-th order Walsh sequence [#]_ is then defined by .. math:: y(t) = {\rm PAL}_k(t / \tau) \; with offset times :math:`\{t_j / \tau\}` defined at the switching times of the Walsh function. References ---------- .. [#] `H. Rademacher, Math. Ann. 87, 112–138 (1922). <https://doi.org/10.1007/BF01458040>`_ .. [#] `H. Ball and M. J Biercuk, EPJ Quantum Technol. 2, 11 (2015). <https://doi.org/10.1140/epjqt/s40507-015-0022-4>`_ """ check_arguments( duration > 0, "Sequence duration must be positive.", {"duration": duration} ) check_arguments( 1 <= paley_order <= 2000, "Paley order must be between 1 and 2000.", {"paley_order": paley_order}, ) # in case a float number is passed paley_order = int(paley_order) hamming_weight = int(np.floor(np.log2(paley_order))) + 1 samples = 2**hamming_weight relative_offset = np.arange(1.0 / (2 * samples), 1.0, 1.0 / samples) binary_string = np.binary_repr(paley_order) binary_order = [int(binary_string[i]) for i in range(hamming_weight)] walsh_array = np.ones(samples) for i in range(hamming_weight): walsh_array *= ( np.sign(np.sin(2 ** (i + 1) * np.pi * relative_offset)) ** binary_order[hamming_weight - 1 - i] ) _walsh_relative_offsets = [] for i in range(samples - 1): if walsh_array[i] != walsh_array[i + 1]: _walsh_relative_offsets.append((i + 1) * (1.0 / samples)) walsh_relative_offsets = np.array(_walsh_relative_offsets, dtype=float) offsets = duration * walsh_relative_offsets rabi_rotations = np.full(offsets.shape, np.pi) azimuthal_angles = np.zeros(offsets.shape) detuning_rotations = np.zeros(offsets.shape) if pre_post_rotation: ( offsets, rabi_rotations, azimuthal_angles, detuning_rotations, ) = _add_pre_post_rotations( duration, offsets, rabi_rotations, azimuthal_angles, detuning_rotations ) return DynamicDecouplingSequence( duration=duration, offsets=offsets, rabi_rotations=rabi_rotations, azimuthal_angles=azimuthal_angles, detuning_rotations=detuning_rotations, name=name, )
[docs]def new_quadratic_sequence( duration, inner_offset_count, outer_offset_count, pre_post_rotation=False, name=None ) -> DynamicDecouplingSequence: r""" Creates the quadratic sequence. Parameters ---------- duration : float The total duration of the sequence :math:`\tau` (in seconds). inner_offset_count : int Number of inner :math:`Z_{\pi}` pulses :math:`n_1`. outer_offset_count : int Number of outer :math:`X_{\pi}` pulses :math:`n_2`. pre_post_rotation : bool, optional If ``True``, a :math:`X_{\pi/2}` rotation is added at the start and end of the sequence. Defaults to ``False``. name : string, optional Name of the sequence. Defaults to ``None``. Returns ------- DynamicDecouplingSequence The quadratic sequence. See Also -------- new_uhrig_sequence Notes ----- The quadratic sequence [#]_ is parameterized by duration :math:`\tau`, number of inner offsets :math:`n_1`, and number of outer offsets :math:`n_2`. The outer sequence consists of :math:`n_2` pulses of type :math:`X_{\pi}`, which partition the time-domain into :math:`n_2+1` sub-intervals on which inner sequences consisting of :math:`n_1` pulses of type :math:`Z_{\pi}` are nested. The total number of offsets is :math:`n = n_1 + n_2(n_1 + 1)`. The pulse times for outer sequence :math:`(X_{\pi}^1, \cdots, X_{\pi}^{n_2})` are defined according to the Uhrig sequence for :math:`t \in [0, \tau]`. The :math:`j`-th :math:`X_{\pi}` pulse, therefore has timing offset defined by .. math:: t_x^j = \tau \sin^2 \left[ \frac{j \pi}{2(n_2 + 1)} \right] \;, where :math:`j = 1, \cdots, n_2`. On each sub-interval defined by the outer sequence, an inner sequence :math:`(Z_{\pi}^1, \cdots, Z_{\pi}^{n_1})` is implemented. The pulse times for the inner sequences are also defined according to the Uhrig sequence. The :math:`k`-th pulse of the :math:`j`-th inner sequence has timing offset defined by .. math:: t_z(k, j) = (t_x^j - t_x^{j - 1}) \sin^2 \left[ \frac{k \pi} {2 (n_1 + 1)} \right] + t_{x}^{j - 1} \;, where :math:`k = 1, \cdots, n_1` and :math:`j = 1, \cdots, n_2 + 1`. References ---------- .. [#] `J. R. West, B. H. Fong, and D. A. Lidar, Physical Review Letters 104, 130501 (2010). <https://doi.org/10.1103/PhysRevLett.104.130501>`_ """ check_arguments( duration > 0, "Sequence duration must be positive.", {"duration": duration} ) check_arguments( inner_offset_count >= 1, "Number of offsets of inner pulses must be positive.", {"inner_offset_count": inner_offset_count}, ) check_arguments( outer_offset_count >= 1, "Number of offsets of outer pulses must be positive.", {"outer_offset_count": outer_offset_count}, ) inner_offset_count = int(inner_offset_count) outer_offset_count = int(outer_offset_count) outer_offsets = _uhrig_single_axis_offsets(duration, outer_offset_count) outer_offsets = np.insert(outer_offsets, [0, len(outer_offsets)], [0, duration]) inner_durations = np.diff(outer_offsets) # offsets include inner and outer offsets # the extra 1 dimension in columns is where we add the outer offset back offsets = np.zeros((len(inner_durations), inner_offset_count + 1)) for inner_duration_idx, inner_duration in enumerate(inner_durations): inner_offset = ( _uhrig_single_axis_offsets(inner_duration, inner_offset_count) + outer_offsets[inner_duration_idx] ) offsets[inner_duration_idx, 0:inner_offset_count] = inner_offset offsets[:, -1] = outer_offsets[1:] rabi_rotations = np.zeros(offsets.shape) detuning_rotations = np.zeros(offsets.shape) rabi_rotations[0:outer_offset_count, -1] = np.pi detuning_rotations[0 : (outer_offset_count + 1), 0:inner_offset_count] = np.pi offsets = offsets.flatten() rabi_rotations = rabi_rotations.flatten() detuning_rotations = detuning_rotations.flatten() # remove the last entry corresponding to the duration offsets = offsets[:-1] rabi_rotations = rabi_rotations[:-1] detuning_rotations = detuning_rotations[:-1] azimuthal_angles = np.zeros(offsets.shape) if pre_post_rotation: ( offsets, rabi_rotations, azimuthal_angles, detuning_rotations, ) = _add_pre_post_rotations( duration, offsets, rabi_rotations, azimuthal_angles, detuning_rotations ) return DynamicDecouplingSequence( duration=duration, offsets=offsets, rabi_rotations=rabi_rotations, azimuthal_angles=azimuthal_angles, detuning_rotations=detuning_rotations, name=name, )
[docs]def new_x_concatenated_sequence( duration, concatenation_order, pre_post_rotation=False, name=None ) -> DynamicDecouplingSequence: r""" Creates the :math:`X`-concatenated sequence. Parameters ---------- duration : float The total duration of the sequence :math:`\tau` (in seconds). concatenation_order : int The number of concatenation of base sequence. pre_post_rotation : bool, optional If ``True``, a :math:`X_{\pi/2}` rotation is added at the start and end of the sequence. Defaults to ``False``. name : string, optional Name of the sequence. Defaults to ``None``. Returns ------- DynamicDecouplingSequence The :math:`X`-concatenated sequence. See Also -------- new_xy_concatenated_sequence Notes ----- The :math:`X`-concatenated sequence [#]_ is constructed by recursively concatenating control sequence structures. It's parameterized by the concatenation order :math:`l` and the duration of the total sequence :math:`\tau`. Let the :math:`l`-th order of concatenation be denoted as :math:`C_l(\tau)`. In this scheme, zeroth order concatenation of duration :math:`\tau` is defined as free evolution over a period of :math:`\tau`. Using the notation :math:`{\mathcal 1}(\tau)` to represent free evolution over duration :math:`\tau`, the the base sequence is: .. math:: C_0(\tau) = {\mathcal 1}(\tau) \;. The :math:`l`-th order :math:`X`-concatenated sequence can be recursively defined as .. math:: C_l(\tau) = C_{l - 1}(\tau / 2) X_{\pi} C_{l - 1}(\tau / 2) X_{\pi} \;. References ---------- .. [#] `K. Khodjasteh and D. A. Lidar, Physical Review Letters 95, 180501 (2005). <https://doi.org/10.1103/PhysRevLett.95.180501>`_ """ check_arguments( duration > 0, "Sequence duration must be positive.", {"duration": duration} ) check_arguments( concatenation_order >= 1, "Concatenation order must be positive.", {"concatenation_order": concatenation_order}, ) concatenation_order = int(concatenation_order) unit_spacing = duration / (2**concatenation_order) cumulations = _concatenation_x(concatenation_order) pos_cum = cumulations * unit_spacing pos_cum_sum = np.cumsum(pos_cum) values, counts = np.unique(pos_cum_sum, return_counts=True) offsets = np.array( [value for value, count in zip(values, counts) if count % 2 == 0] ) if concatenation_order % 2 == 1: offsets = offsets[:-1] rabi_rotations = np.zeros(offsets.shape) rabi_rotations[0:] = np.pi azimuthal_angles = np.zeros(offsets.shape) detuning_rotations = np.zeros(offsets.shape) if pre_post_rotation: ( offsets, rabi_rotations, azimuthal_angles, detuning_rotations, ) = _add_pre_post_rotations( duration, offsets, rabi_rotations, azimuthal_angles, detuning_rotations ) return DynamicDecouplingSequence( duration=duration, offsets=offsets, rabi_rotations=rabi_rotations, azimuthal_angles=azimuthal_angles, detuning_rotations=detuning_rotations, name=name, )
[docs]def new_xy_concatenated_sequence( duration, concatenation_order, pre_post_rotation=False, name=None ) -> DynamicDecouplingSequence: r""" Creates the :math:`XY`-concatenated sequence. Parameters ---------- duration : float The total duration of the sequence :math:`\tau` (in seconds). concatenation_order : int The number of concatenation of base sequence :math:`l`. pre_post_rotation : bool, optional If ``True``, a :math:`X_{\pi/2}` rotation is added at the start and end of the sequence. Defaults to ``False``. name : string, optional Name of the sequence. Defaults to ``None``. Returns ------- DynamicDecouplingSequence The :math:`XY`-concatenated sequence. See Also -------- new_x_concatenated_sequence Notes ----- The :math:`XY`-concatenated sequence [#]_ is constructed by recursively concatenating control sequence structures. It's parameterized by the concatenation order :math:`l` and the duration of the total sequence :math:`\tau`. Let the :math:`l`-th order of concatenation be denoted as :math:`C_l(\tau)`. In this scheme, zeroth order concatenation of duration :math:`\tau` is defined as free evolution over a period of :math:`\tau`. Using the notation :math:`{\mathcal 1}(\tau)` to represent free evolution over duration :math:`\tau`, the the base sequence is: .. math:: C_0(\tau) = {\mathcal 1}(\tau) \;. The :math:`l`-th order :math:`XY`-concatenated sequence can be recursively defined as .. math:: C_l(\tau) = C_{l - 1}(\tau / 4) X_{\pi} C_{l - 1}(\tau / 4) Y_{\pi} C_{l - 1}(\tau / 4) X_{\pi} C_{l - 1}(\tau / 4) Y_{\pi} \;. References ---------- .. [#] `K. Khodjasteh and D. A. Lidar, Physical Review Letters 95, 180501 (2005). <https://doi.org/10.1103/PhysRevLett.95.180501>`_ """ check_arguments( duration > 0, "Sequence duration must be positive.", {"duration": duration} ) check_arguments( concatenation_order >= 1, "Concatenation order must be positive.", {"concatenation_order": concatenation_order}, ) concatenation_order = int(concatenation_order) unit_spacing = duration / (2 ** (concatenation_order * 2)) cumulations = _concatenation_xy(concatenation_order) rabi_operations = cumulations[cumulations != -2] rabi_operations = rabi_operations[rabi_operations != -3] rabi_positions = np.zeros(rabi_operations.shape) rabi_positions[rabi_operations != -1] = 1 rabi_positions = rabi_positions * unit_spacing rabi_positions = np.cumsum(rabi_positions) values, counts = np.unique(rabi_positions, return_counts=True) rabi_offsets = [value for value, count in zip(values, counts) if count % 2 == 0] azimuthal_operations = cumulations[cumulations != -1] azimuthal_operations = azimuthal_operations[azimuthal_operations != -3] azimuthal_positions = np.zeros(azimuthal_operations.shape) azimuthal_positions[azimuthal_operations != -2] = 1 azimuthal_positions = azimuthal_positions * unit_spacing azimuthal_positions = np.cumsum(azimuthal_positions) values, counts = np.unique(azimuthal_positions, return_counts=True) azimuthal_offsets = [ value for value, count in zip(values, counts) if count % 2 == 0 ] detuning_operations = cumulations[cumulations != -2] detuning_operations = detuning_operations[detuning_operations != -1] detuning_positions = np.zeros(detuning_operations.shape) detuning_positions[detuning_operations != -3] = 1 detuning_positions = detuning_positions * unit_spacing detuning_positions = np.cumsum(detuning_positions) values, counts = np.unique(detuning_positions, return_counts=True) detuning_offsets = [value for value, count in zip(values, counts) if count % 2 == 0] # right now we have got all the offset positions separately; now have # put then all together offsets = np.zeros( (len(rabi_offsets) + len(azimuthal_offsets) + len(detuning_offsets),) ) rabi_rotations = np.zeros(offsets.shape) azimuthal_angles = np.zeros(offsets.shape) detuning_rotations = np.zeros(offsets.shape) rabi_idx = 0 azimuthal_idx = 0 carr_idx = 0 while rabi_idx < len(rabi_offsets) and azimuthal_idx < len(azimuthal_offsets): if rabi_offsets[rabi_idx] < azimuthal_offsets[azimuthal_idx]: rabi_rotations[carr_idx] = np.pi offsets[carr_idx] = rabi_offsets[rabi_idx] rabi_idx += 1 else: azimuthal_angles[carr_idx] = np.pi / 2 rabi_rotations[carr_idx] = np.pi offsets[carr_idx] = azimuthal_offsets[azimuthal_idx] azimuthal_idx += 1 carr_idx += 1 if rabi_idx < len(rabi_offsets): while rabi_idx < len(rabi_offsets): rabi_rotations[carr_idx] = np.pi offsets[carr_idx] = rabi_offsets[rabi_idx] carr_idx += 1 rabi_idx += 1 if azimuthal_idx < len(azimuthal_offsets): while azimuthal_idx < len(azimuthal_offsets): azimuthal_angles[carr_idx] = np.pi / 2 rabi_rotations[carr_idx] = np.pi offsets[carr_idx] = azimuthal_offsets[azimuthal_idx] carr_idx += 1 azimuthal_idx += 1 # if there is any z-offset, add those too !!! if detuning_offsets: z_idx = 0 for carr_idx, offset in enumerate(offsets): if offset > detuning_offsets[z_idx]: offsets[carr_idx + 1 :] = offsets[carr_idx:-1] rabi_rotations[carr_idx + 1 :] = rabi_rotations[carr_idx:-1] azimuthal_angles[carr_idx + 1 :] = azimuthal_angles[carr_idx:-1] detuning_rotations[carr_idx] = np.pi rabi_rotations[carr_idx] = 0 azimuthal_angles[carr_idx] = 0 offsets[carr_idx] = detuning_offsets[z_idx] z_idx += 1 if z_idx >= len(detuning_offsets): break if pre_post_rotation: ( offsets, rabi_rotations, azimuthal_angles, detuning_rotations, ) = _add_pre_post_rotations( duration, offsets, rabi_rotations, azimuthal_angles, detuning_rotations ) return DynamicDecouplingSequence( duration=duration, offsets=offsets, rabi_rotations=rabi_rotations, azimuthal_angles=azimuthal_angles, detuning_rotations=detuning_rotations, name=name, )
def _carr_purcell_meiboom_gill_offsets( duration: float, offset_count: int ) -> np.ndarray: """ Calculates offset values for Carr-Purcell_Meiboom-Gill sequence. Parameters ---------- duration : float Duration of the total sequence. offset_count : int The number of offsets Returns ------- np.ndarray The offset values. """ spacing = 1.0 / offset_count start = spacing * 0.5 # prepare the offsets for delta comb deltas = spacing * np.arange(offset_count) deltas += start offsets = deltas * duration return offsets def _uhrig_single_axis_offsets(duration: float, offset_count: int) -> np.ndarray: """ Calculates oOffset values for Uhrig Single Axis Sequence. Parameters ---------- duration : float Duration of the total sequence. offset_count : int The number of offsets. Returns ------- np.ndarray The offset values. """ # prepare the offsets for delta comb constant = 1.0 / (2 * offset_count + 2) deltas = np.array( [(np.sin(np.pi * k * constant)) ** 2 for k in range(1, offset_count + 1)] ) offsets = duration * deltas return offsets def _concatenation_x(concatenation_sequence: int) -> np.ndarray: """ Prepares the sequence of operations for x-concatenated dynamical decoupling sequence. Parameters ---------- concatenation_sequence : int Duration of the total sequence. Returns ------- np.ndarray The offset values. """ if concatenation_sequence == 1: return np.array([1, 0, 1, 0]) return np.concatenate( ( _concatenation_x(concatenation_sequence - 1), np.array([0]), _concatenation_x(concatenation_sequence - 1), np.array([0]), ), axis=0, ) def _concatenation_xy(concatenation_sequence) -> np.ndarray: """ Prepares the sequence of operations for x-concatenated dynamical decoupling sequence. Parameters ---------- concatenation_sequence : int Duration of the total sequence. Returns ------- np.ndarray The offset values. """ if concatenation_sequence == 1: return np.array([1, -1, 1, -2, 1, -1, 1, -2]) cumulations = np.concatenate( (_concatenation_xy(concatenation_sequence - 1), np.array([-1])), axis=0 ) cumulations = cumulations[0:-1] cumulations[-1] = -3 cumulations = np.concatenate( (cumulations, _concatenation_xy(concatenation_sequence - 1), np.array([-2])), axis=0, ) cumulations = cumulations[0:-2] cumulations = np.concatenate( (cumulations, _concatenation_xy(concatenation_sequence - 1), np.array([-1])), axis=0, ) cumulations = cumulations[0:-1] cumulations[-1] = -3 cumulations = np.concatenate( (cumulations, _concatenation_xy(concatenation_sequence - 1), np.array([-2])), axis=0, ) if cumulations[-1] == -2 and cumulations[-2] == -2: cumulations = cumulations[0:-2] return cumulations