Skip to content

Commit

Permalink
reformatting docs in signals module
Browse files Browse the repository at this point in the history
  • Loading branch information
DanPuzzuoli committed Feb 28, 2024
1 parent c82e1ef commit ea0d71e
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 124 deletions.
99 changes: 49 additions & 50 deletions qiskit_dynamics/signals/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
.. currentmodule:: qiskit_dynamics.signals
This module contains classes for representing the time-dependent coefficients in
matrix differential equations.
This module contains classes for representing the time-dependent coefficients in matrix differential
equations.
These classes, referred to as *signals*, represent classes of real-valued functions, either
of the form, or built from functions of the following form:
These classes, referred to as *signals*, represent classes of real-valued functions, either of the
form, or built from functions of the following form:
.. math::
s(t) = \textnormal{Re}[f(t)e^{i(2 \pi \nu t + \phi)}],
Expand All @@ -34,23 +34,22 @@
* :math:`\nu \in \mathbb{R}` is the *carrier frequency*, and
* :math:`\phi \in \mathbb{R}` is the *phase*.
Furthermore, this module contains *transfer functions* which transform one or more signal
into other signals.
Furthermore, this module contains *transfer functions* which transform one or more signal into other
signals.
Signal API summary
==================
All signal classes share a common API for evaluation and visualization:
* The signal value at a given time ``t`` is evaluated by treating the ``signal`` as a
callable: ``signal(t)``.
* The signal value at a given time ``t`` is evaluated by treating the ``signal`` as a callable:
``signal(t)``.
* The envelope :math:`f(t)` is evaluated via: ``signal.envelope(t)``.
* The complex value :math:`f(t)e^{i(2 \pi \nu t + \phi)}` via: ``signal.complex_value(t)``.
* The ``signal.draw`` method provides a common visualization interface.
In addition to the above, all signal types allow for algebraic operations, which should be
understood in terms of algebraic operations on functions. E.g. two signals can be added
together via
understood in terms of algebraic operations on functions. E.g. two signals can be added together via
.. code-block:: python
Expand All @@ -62,12 +61,12 @@
signal_sum(t) == signal1(t) + signal2(t)
Signal multiplication is defined similarly, and signals can be added or multiplied with constants
as well.
Signal multiplication is defined similarly, and signals can be added or multiplied with constants as
well.
The remainder of this document gives further detail about some special functionality of
these classes, but the following table provides a list of the different signal classes,
along with a high level description of their role.
The remainder of this document gives further detail about some special functionality of these
classes, but the following table provides a list of the different signal classes, along with a high
level description of their role.
.. list-table:: Types of signal objects
:widths: 10 50
Expand All @@ -82,11 +81,11 @@
performance.
* - :class:`~qiskit_dynamics.signals.SignalSum`
- A sum of :class:`~qiskit_dynamics.signals.Signal` or
:class:`~qiskit_dynamics.signals.DiscreteSignal` objects.
Evaluation of envelopes returns an array of envelopes in the sum.
:class:`~qiskit_dynamics.signals.DiscreteSignal` objects. Evaluation of envelopes returns an
array of envelopes in the sum.
* - :class:`~qiskit_dynamics.signals.DiscreteSignalSum`
- A sum of :class:`~qiskit_dynamics.signals.DiscreteSignal` objects with the same
start time, number of samples, and sample duration. Implemented with array-based operations.
- A sum of :class:`~qiskit_dynamics.signals.DiscreteSignal` objects with the same start time,
number of samples, and sample duration. Implemented with array-based operations.
Constant Signal
Expand All @@ -98,53 +97,50 @@
const = Signal(2.)
This initializes the object to always return the constant ``2.``, and allows constants to be
treated on the same footing as arbitrary :class:`~qiskit_dynamics.signals.Signal` instances.
A :class:`~qiskit_dynamics.signals.Signal` operating in constant-mode can be checked via the
boolean attribute ``const.is_constant``.
This initializes the object to always return the constant ``2.``, and allows constants to be treated
on the same footing as arbitrary :class:`~qiskit_dynamics.signals.Signal` instances. A
:class:`~qiskit_dynamics.signals.Signal` operating in constant-mode can be checked via the boolean
attribute ``const.is_constant``.
Algebraic operations
====================
Algebraic operations are supported by the :class:`~qiskit_dynamics.signals.SignalSum`
object. Any two signal classes can be added together, producing a
:class:`~qiskit_dynamics.signals.SignalSum`. Multiplication is also supported
via :class:`~qiskit_dynamics.signals.SignalSum` using the identity:
Algebraic operations are supported by the :class:`~qiskit_dynamics.signals.SignalSum` object. Any
two signal classes can be added together, producing a :class:`~qiskit_dynamics.signals.SignalSum`.
Multiplication is also supported via :class:`~qiskit_dynamics.signals.SignalSum` using the identity:
.. math::
Re[f(t)e^{i(2 \pi \nu t + \phi)}] \times &Re[g(t)e^{i(2 \pi \omega t + \psi)}]
\\&= Re[\frac{1}{2} f(t)g(t)e^{i(2\pi (\omega + \nu)t + (\phi + \psi))} ]
+ Re[\frac{1}{2} f(t)\overline{g(t)}e^{i(2\pi (\omega - \nu)t + (\phi - \psi))} ].
I.e. multiplication of two base signals produces a :class:`~qiskit_dynamics.signals.SignalSum`
with two elements, whose envelopes, frequencies, and phases are as given by the above formula.
I.e. multiplication of two base signals produces a :class:`~qiskit_dynamics.signals.SignalSum` with
two elements, whose envelopes, frequencies, and phases are as given by the above formula.
Multiplication of sums is handled via distribution of this formula over the sum.
In the special case that
:class:`~qiskit_dynamics.signals.DiscreteSignal`\s with compatible sample structure
(same number of samples, ``dt``, and start time) are added together,
a :class:`~qiskit_dynamics.signals.DiscreteSignalSum` is produced.
In the special case that :class:`~qiskit_dynamics.signals.DiscreteSignal`\s with compatible sample
structure (same number of samples, ``dt``, and start time) are added together, a
:class:`~qiskit_dynamics.signals.DiscreteSignalSum` is produced.
:class:`~qiskit_dynamics.signals.DiscreteSignalSum` stores a sum of compatible
:class:`~qiskit_dynamics.signals.DiscreteSignal`\s by joining the underlying arrays,
so that the sum can be evaluated using purely array-based operations. Multiplication
of :class:`~qiskit_dynamics.signals.DiscreteSignal`\s with compatible sample structure
is handled similarly.
:class:`~qiskit_dynamics.signals.DiscreteSignal`\s by joining the underlying arrays, so that the sum
can be evaluated using purely array-based operations. Multiplication of
:class:`~qiskit_dynamics.signals.DiscreteSignal`\s with compatible sample structure is handled
similarly.
Sampling
========
Both :class:`~qiskit_dynamics.signals.DiscreteSignal` and
:class:`~qiskit_dynamics.signals.DiscreteSignalSum` feature constructors
(:meth:`~qiskit_dynamics.signals.DiscreteSignal.from_Signal` and
:meth:`~qiskit_dynamics.signals.DiscreteSignalSum.from_SignalSum` respectively)
which build an instance by sampling a :class:`~qiskit_dynamics.signals.Signal` or
:class:`~qiskit_dynamics.signals.SignalSum`. These constructors have the
option to just sample the envelope (and keep the carrier analog), or to also
sample the carrier. Below is a visualization of a signal superimposed with
sampled versions, both in the case of sampling the carrier, and in the case of
sampling just the envelope (and keeping the carrier analog).
:meth:`~qiskit_dynamics.signals.DiscreteSignalSum.from_SignalSum` respectively) which build an
instance by sampling a :class:`~qiskit_dynamics.signals.Signal` or
:class:`~qiskit_dynamics.signals.SignalSum`. These constructors have the option to just sample the
envelope (and keep the carrier analog), or to also sample the carrier. Below is a visualization of a
signal superimposed with sampled versions, both in the case of sampling the carrier, and in the case
of sampling just the envelope (and keeping the carrier analog).
.. jupyter-execute::
:hide-code:
Expand All @@ -154,18 +150,21 @@
# discretize a signal with and without samplying the carrier
signal = Signal(lambda t: t, carrier_freq=2.)
discrete_signal = DiscreteSignal.from_Signal(signal, dt=0.1, start_time=0.,
n_samples=10, sample_carrier=True)
discrete_signal = DiscreteSignal.from_Signal(
signal, dt=0.1, start_time=0., n_samples=10, sample_carrier=True
)
discrete_signal2 = DiscreteSignal.from_Signal(signal, dt=0.1, start_time=0., n_samples=10)
# plot the signal against each discretization
fig, axs = plt.subplots(1, 2, figsize=(14, 4))
signal.draw(t0=0., tf=1., n=100, axis=axs[0])
discrete_signal.draw(t0=0., tf=1., n=100, axis=axs[0],
title='Signal v.s. Sampled envelope and carrier')
discrete_signal.draw(
t0=0., tf=1., n=100, axis=axs[0], title='Signal v.s. Sampled envelope and carrier'
)
signal.draw(t0=0., tf=1., n=100, axis=axs[1])
discrete_signal2.draw(t0=0., tf=1., n=100, axis=axs[1],
title='Signal v.s. Sampled envelope')
discrete_signal2.draw(
t0=0., tf=1., n=100, axis=axs[1], title='Signal v.s. Sampled envelope'
)
Transfer Functions
Expand Down
87 changes: 42 additions & 45 deletions qiskit_dynamics/signals/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,17 @@ class Signal:
- :math:`\nu` is the carrier frequency.
- :math:`\phi` is the phase.
The envelope function can be specified either as a constant numeric value
(indicating a constant function), or as a complex-valued callable,
and the frequency and phase must be real.
The envelope function can be specified either as a constant numeric value (indicating a constant
function), or as a complex-valued callable, and the frequency and phase must be real.
.. note::
:class:`~qiskit_dynamics.signals.Signal` assumes the envelope ``f`` is
*array vectorized* in the sense that ``f`` can operate on arrays of arbitrary shape
and satisfy ``f(x)[idx] == f(x[idx])`` for a multidimensional index ``idx``. This
can be ensured either by writing ``f`` to be vectorized, or by using the ``vectorize``
function in ``numpy`` or ``jax.numpy``.
:class:`~qiskit_dynamics.signals.Signal` assumes the envelope ``f`` is *array vectorized* in
the sense that ``f`` can operate on arrays of arbitrary shape and satisfy
``f(x)[idx] == f(x[idx])`` for a multidimensional index ``idx``. This can be ensured either
by writing ``f`` to be vectorized, or by using the ``vectorize`` function in ``numpy`` or
``jax.numpy``.
For example, for an unvectorized envelope function ``f``:
Expand All @@ -80,10 +79,10 @@ def __init__(
Args:
envelope: Envelope function of the signal, must be vectorized.
carrier_freq: Frequency of the carrier. Subclasses such as SignalSums
represent the carriers of each signal in an array.
phase: The phase of the carrier. Subclasses such as SignalSums
represent the phase of each signal in an array.
carrier_freq: Frequency of the carrier. Subclasses such as SignalSums represent the
carriers of each signal in an array.
phase: The phase of the carrier. Subclasses such as SignalSums represent the phase of
each signal in an array.
name: Name of the signal.
"""
self._name = name
Expand Down Expand Up @@ -125,8 +124,9 @@ def carrier_freq(self) -> ArrayLike:

@carrier_freq.setter
def carrier_freq(self, carrier_freq: ArrayLike):
"""Carrier frequency setter. List handling is to support subclasses storing a
list of frequencies."""
"""Carrier frequency setter. List handling is to support subclasses storing a list of
frequencies.
"""
self._carrier_freq = unp.asarray(carrier_freq)
self._carrier_arg = 1j * 2 * np.pi * self._carrier_freq

Expand All @@ -137,8 +137,7 @@ def phase(self) -> ArrayLike:

@phase.setter
def phase(self, phase: ArrayLike):
"""Phase setter. List handling is to support subclasses storing a
list of phases."""
"""Phase setter. List handling is to support subclasses storing a list of phases."""
self._phase = unp.asarray(phase)
self._phase_arg = 1j * self._phase

Expand Down Expand Up @@ -205,8 +204,7 @@ def draw(
):
"""Plot the signal over an interval.
The ``function`` arg specifies which function to
plot:
The ``function`` arg specifies which function to plot:
- ``function == 'signal'`` plots the full signal.
- ``function == 'envelope'`` plots the complex envelope.
Expand Down Expand Up @@ -262,9 +260,9 @@ class DiscreteSignal(Signal):
The envelope is specified by an array of samples ``s = [s_0, ..., s_k]``, sample width ``dt``,
and a start time ``t_0``, with the envelope being evaluated as
:math:`f(t) =` ``s[floor((t - t0)/dt)]`` if ``t`` is in the interval with endpoints
``start_time`` and ``start_time + dt * len(samples)``, and ``0.0`` otherwise.
By default a :class:`~qiskit_dynamics.signals.DiscreteSignal` is defined to start at
:math:`t=0` but a custom start time can be set via the ``start_time`` kwarg.
``start_time`` and ``start_time + dt * len(samples)``, and ``0.0`` otherwise. By default a
:class:`~qiskit_dynamics.signals.DiscreteSignal` is defined to start at :math:`t=0` but a custom
start time can be set via the ``start_time`` kwarg.
"""

def __init__(
Expand All @@ -282,11 +280,11 @@ def __init__(
dt: The duration of each sample.
samples: The array of samples.
start_time: The time at which the signal starts.
carrier_freq: Frequency of the carrier. Subclasses such as SignalSums
represent the carriers of each signal in an array.
phase: The phase of the carrier. Subclasses such as SignalSums
represent the phase of each signal in an array.
name: name of the signal.
carrier_freq: Frequency of the carrier. Subclasses such as SignalSums represent the
carriers of each signal in an array.
phase: The phase of the carrier. Subclasses such as SignalSums represent the phase of
each signal in an array.
name: Name of the signal.
"""
self._dt = dt

Expand Down Expand Up @@ -325,8 +323,8 @@ def from_Signal(
):
r"""Constructs a ``DiscreteSignal`` object by sampling a ``Signal``\.
The optional argument ``sample_carrier`` controls whether or not to include the carrier
in the sampling. I.e.:
The optional argument ``sample_carrier`` controls whether or not to include the carrier in
the sampling. I.e.:
- If ``sample_carrier == False``\, a ``DiscreteSignal`` is constructed with:
- ``samples`` obtained by sampling ``signal.envelope``\.
Expand Down Expand Up @@ -521,8 +519,8 @@ class SignalSum(SignalCollection, Signal):
frequencies/phases for each term in the sum, and the ``envelope`` method returns an
``ArrayLike`` of the envelopes for each summand.
Internally, the signals are stored as a list in the ``components`` attribute, which can
be accessed via direct subscripting of the object.
Internally, the signals are stored as a list in the ``components`` attribute, which can be
accessed via direct subscripting of the object.
"""

def __init__(self, *signals, name: Optional[str] = None):
Expand Down Expand Up @@ -612,8 +610,8 @@ def merged_env(t):


class DiscreteSignalSum(DiscreteSignal, SignalSum):
"""Represents a sum of piecewise constant signals, all with the same
time parameters: dt, number of samples, and start time.
"""Represents a sum of piecewise constant signals, all with the same time parameters: dt, number
of samples, and start time.
"""

def __init__(
Expand All @@ -626,13 +624,13 @@ def __init__(
name: str = None,
):
r"""Directly initialize a ``DiscreteSignalSum``\. Samples of all terms in the
sum are specified as a 2d array, with 0th axis indicating time, and 1st axis
indicating a term in the sum.
sum are specified as a 2d array, with 0th axis indicating time, and 1st axis indicating a
term in the sum.
Args:
dt: The duration of each sample.
samples: The 2d array representing a list whose elements are all envelope values
at a given time.
samples: The 2d array representing a list whose elements are all envelope values at a
given time.
start_time: The time at which the signal starts.
carrier_freq: Array with the carrier frequencies of each term in the sum.
phase: Array with the phases of each term in the sum.
Expand Down Expand Up @@ -682,8 +680,8 @@ def from_SignalSum(
):
r"""Constructs a ``DiscreteSignalSum`` object by sampling a ``SignalSum``\.
The optional argument ``sample_carrier`` controls whether or not to include the carrier
in the sampling. I.e.:
The optional argument ``sample_carrier`` controls whether or not to include the carrier in
the sampling. I.e.:
- If ``sample_carrier == False``, a ``DiscreteSignalSum`` is constructed with:
- ``samples`` obtained by sampling ``signal_sum.envelope``\.
Expand Down Expand Up @@ -960,18 +958,17 @@ def signal_multiply(sig1: Signal, sig2: Signal) -> SignalSum:


def base_signal_multiply(sig1: Signal, sig2: Signal) -> Signal:
r"""Utility function for multiplying two elementary (non ``SignalSum``\) signals.
This function assumes ``sig1`` and ``sig2`` are legitimate instances of ``Signal``
subclasses.
r"""Utility function for multiplying two elementary (non ``SignalSum``\) signals. This function
assumes ``sig1`` and ``sig2`` are legitimate instances of ``Signal`` subclasses.
Special cases:
- Multiplication of two constant ``Signal``\s returns a constant ``Signal``\.
- Multiplication of a constant ``Signal`` and a ``DiscreteSignal`` returns
a ``DiscreteSignal``\.
- Multiplication of a constant ``Signal`` and a ``DiscreteSignal`` returns a
``DiscreteSignal``\.
- If two ``DiscreteSignal``\s have compatible parameters, the resulting signals are
``DiscreteSignal``\, with the multiplication being implemented by array multiplication of
the samples.
``DiscreteSignal``\, with the multiplication being implemented by array multiplication of
the samples.
- Lastly, if no special rules apply, the two ``Signal``\s are multiplied generically via
multiplication of the envelopes as functions.
Expand Down
Loading

0 comments on commit ea0d71e

Please sign in to comment.