Source code for qiskit_dynamics.signals.transfer_functions

# -*- coding: utf-8 -*-

# 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

"""
Module for transformations between signals.
"""

from abc import ABC, abstractmethod
from typing import Callable, Union, List
import numpy as np

from qiskit import QiskitError
from qiskit_dynamics.arraylias import DYNAMICS_NUMPY as unp
from qiskit_dynamics.arraylias.alias import _numpy_multi_dispatch

from .signals import Signal, DiscreteSignal


class BaseTransferFunction(ABC):
    """Base class for transforming signals."""

    @property
    @abstractmethod
    def n_inputs(self):
        """Number of input signals to the transfer function."""
        pass

    def __call__(self, *args, **kwargs) -> Union[Signal, List[Signal]]:
        """Apply the transfer function to the input signals.

        Args:
            *args: The signals to which the transfer function will be applied.
            **kwargs: Key word arguments to control the transfer functions.

        Returns:
            Signal: The transformed signal.

        Raises:
            QiskitError: if the number of args is not correct.
        """

        if len(args) != self.n_inputs:
            raise QiskitError(
                f"""{self.__class__.__name__} expected {len(args)}
                input signals but {self.n_inputs} were given."""
            )

        return self._apply(*args, **kwargs)

    @abstractmethod
    def _apply(self, *args, **kwargs) -> Union[Signal, List[Signal]]:
        """Applies a transformation on a signal, such as a convolution, low pass filter, etc.

        Args:
            *args: The signals to which the transfer function will be applied.
            **kwargs: Key word arguments to control the transfer functions.

        Returns:
            Signal: The transformed signal.
        """
        pass


[docs] class Convolution(BaseTransferFunction): """Applies a convolution as a sum (f*g)(n) = sum_k f(k)g(n-k) The implementation is quadratic in the number of samples in the signal. """ def __init__(self, func: Callable): """ Args: func: The convolution function specified in time. This function will be normalized to one before doing the convolution. To scale signals multiply them by a float. """ self._func = func @property def n_inputs(self): return 1 # pylint: disable=arguments-differ def _apply(self, signal: Signal) -> Signal: """Applies a transformation on a signal, such as a convolution, low pass filter, etc. Once a convolution is applied the signal can longer have a carrier as the carrier is part of the signal value and gets convolved. Args: signal: A signal or list of signals to which the transfer function will be applied. Returns: signal: The transformed signal or list of signals. Raises: QiskitError: if the signal is not pwc. """ if isinstance(signal, DiscreteSignal): # Perform a discrete time convolution. dt = signal.dt func_samples = unp.asarray([self._func(dt * i) for i in range(signal.duration)]) func_samples = func_samples / unp.sum(func_samples) sig_samples = signal(dt * unp.arange(signal.duration)) convoluted_samples = _numpy_multi_dispatch(func_samples, sig_samples, path="convolve") return DiscreteSignal(dt, convoluted_samples, carrier_freq=0.0, phase=0.0) else: raise QiskitError("Transfer function not defined on input.")
class FFTConvolution(BaseTransferFunction): """Applies a convolution by moving into the fourier domain.""" def __init__(self, func: Callable): self._func = func @property def n_inputs(self): return 1 # pylint: disable=arguments-differ def _apply(self, signal: Signal) -> Signal: raise NotImplementedError class Sampler(BaseTransferFunction): """Resample a signal by wrapping DiscreteSignal.from_Signal.""" def __init__(self, dt: float, n_samples: int, start_time: float = 0): """ Args: dt: The new sample period. n_samples: number of samples to resample with. start_time: start time from which to resample. """ self._dt = dt self._n_samples = n_samples self._start_time = start_time @property def n_inputs(self): """Number of input signals to the transfer function.""" return 1 # pylint: disable=arguments-differ def _apply(self, signal: Signal) -> Signal: """Apply the transfer function to the signal.""" return DiscreteSignal.from_Signal( signal, dt=self._dt, n_samples=self._n_samples, start_time=self._start_time )
[docs] class IQMixer(BaseTransferFunction): """Implements an IQ Mixer. The IQ mixer takes as input three signals: - in-phase signal: I cos(w_if t + phi_I) - quadrature: Q cos(w_if t + phi_Q) - local oscillator: K cos(w_lo t) In this implementation the local oscillator is specified by its frequency w_lo and, without loss of generality we assume K = 1. Furthermore, we require that the carrier frequency of the I and Q be identical. The output RF signal is defined by s_rf = I [cos(wp t + phi_I) + cos(wm t + phi_I)]/2 + Q [cos(wp t + phi_Q - pi/2) + cos(wm t + phi_Q + pi/2)]/2 where wp = w_lo + w_if and wp = w_lo - w_if. The output of this transfer function will produce a piece-wise constant that does not have a carrier frequency or phase. All information is in the samples. Mixer imperfections are not included. """ def __init__(self, lo: float): """ Args: lo: The carrier of the IQ mixer. """ self._lo = lo @property def n_inputs(self): return 2 # pylint: disable=arguments-differ def _apply(self, si: Signal, sq: Signal) -> Signal: """ Args: si: In phase signal sq: Quadrature signal. Returns: The up-converted signal. Raises: QiskitError: if the carriers frequencies of I and Q differ. """ # Check compatibility of the input signals if si.carrier_freq != sq.carrier_freq: raise QiskitError("IQ mixer requires the same sideband frequencies for I and Q.") phi_i, phi_q = si.phase, sq.phase wp, wm = self._lo + si.carrier_freq, self._lo - si.carrier_freq wp *= 2 * np.pi wm *= 2 * np.pi def mixer_func(t): """Function of the IQ mixer.""" osc_i = unp.cos(wp * t + phi_i) + unp.cos(wm * t + phi_i) osc_q = unp.cos(wp * t + phi_q - np.pi / 2) + unp.cos(wm * t + phi_q + np.pi / 2) return si.envelope(t) * osc_i / 2 + sq.envelope(t) * osc_q / 2 return Signal(mixer_func, carrier_freq=0, phase=0)