<jupyter_start><jupyter_text>Playing around with signal classesHere we are prototyping representing mixed complex valued signals. A signal is a complex valued function of time, of the form $s(t) = f(t)e^{i 2 \pi \nu t}$, where $f$ is the envelope function, and $\nu$ is the carrier frequency.<jupyter_code>import numpy as np
from qiskit_ode.signals import Signal, Constant, PiecewiseConstant
from matplotlib import pyplot as plt

%matplotlib inline<jupyter_output><empty_output><jupyter_text>1. Signals specified as functionsThe most general signal object is given by specification of the envelope as a python callable function, and a frequency.<jupyter_code>test_sig = Signal(envelope=lambda t: t**2, 
                  carrier_freq=2.)<jupyter_output><empty_output><jupyter_text>We can plot it (this gives the real and complex part).<jupyter_code>test_sig.plot(0, 5, 100)<jupyter_output><empty_output><jupyter_text>We can also plot just the envelope.<jupyter_code>test_sig.plot_envelope(0, 5, 100)<jupyter_output><empty_output><jupyter_text>2. ConstantsConstants are implemented as a subclass of `Signal` with a constant value and `0` carrier frequency. This allows a parameter to be recognized as constant but still be worked with in the same way as a general mixed signal.<jupyter_code>constant = Constant(2.)
constant.plot(0, 5, 100)<jupyter_output><empty_output><jupyter_text>3. SignalA signal is a signal with an envelope specified by a function and with potentially non-zero carrier frequency.<jupyter_code>constant_sig = Signal(2., carrier_freq=0.1)
constant_sig.plot(0, 5, 100)<jupyter_output><empty_output><jupyter_text>4. PiecewiseConstantSignals with piecewise constant envelopes specified as a list of samples, along with a `dt` value specifying the length of each sample.<jupyter_code>dt = 1.
samples = np.array([0.,0.,1.,2.,1.,0.,0.])
carrier_freq = 0.5

piecewise_const = PiecewiseConstant(dt=dt, samples=samples, carrier_freq=carrier_freq)
piecewise_const.plot(0, 5, 100)
piecewise_const.plot_envelope(0, 5, 100)<jupyter_output><empty_output><jupyter_text>5. Adding and multiplying signalsMultiplying and adding signals produces a new signal. The most general version of this simply treats them as general signals. E.g. to multiply two general signals, a new envelope function is constructed from the old, and a new signal is instantiated with this envelope function with the carrier frequency given by the sum.Addition is a bit less clean - when two signals are added it is checked if they have the same carrier frequency - if they do the envelope functinos are simply added together. If the carrier frequencies are different, the current behaviour is to create a new signal with $0$ carrier frequency and envelope function constructed from the full signals added together.The addition/multiplication routines also attempt to preserve special types. E.g. if two Constants are added together the result is a new Constant. The current special rules are very simple, but regardless of type if a special rule doesn't exist we can always add/multiply them as general signals.<jupyter_code># adding constants together produces another constant
constant + constant
# multiply two signals and plot the resulting envelope
(test_sig*piecewise_const).plot_envelope(0, 6, 100)
(test_sig*piecewise_const).plot(0, 6, 300)
# add a constant to a signal
(2-2j + test_sig).plot(0, 5, 100)
#add two compatible piecewise constant signals (same start time, duration, dt)
type(piecewise_const + piecewise_const)
piecewise_const
(20*piecewise_const).plot(0, 7, 1000)
(20*piecewise_const).plot_envelope(0, 7, 1000)<jupyter_output><empty_output><jupyter_text>Observe that by doing `test_sig + 20*piecewise_const` we lose the resolution of the carrier since we needed to move it into the envelope due to the addition.<jupyter_code>(test_sig + 20*piecewise_const).plot(0, 7, 1000)<jupyter_output><empty_output><jupyter_text>6. Transfer functions<jupyter_code>from qiskit_ode.models.transfer_functions import Convolution
def gaus(t):
    sigma = 10
    dt = 0.1
    return 2.*dt/np.sqrt(2.*np.pi*sigma**2)*np.exp(-t**2/(2*sigma**2))
ts = np.linspace(0, 100, 1000)
np.sum([gaus(_) for _ in ts])
plt.plot(ts, gaus(ts))
convolution = Convolution(gaus)

samples = [0. if t<20. or t > 80. else 1 for t in ts]
piecewise_const = PiecewiseConstant(dt=ts[1]-ts[0], samples=samples, carrier_freq=0.0, start_time=0)
piecewise_const.plot(0, 100, 200)
piecewise_const.carrier_freq
convolved = convolution.apply(piecewise_const)
convolved.plot(0, 150, 200)
convolved.carrier_freq
convolved.carrier_freq = 0.3
convolved.plot(0, 200, 500)
convolved.carrier_freq
convolved.duration<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08729989327641409, 'fraction_numerical': 0.032017075773746, 'mean_word_length': 6.046616541353384, 'pattern_counts': {'":': 0, '<': 42, '<?xml version=': 0, '>': 42, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '10', 'dir': 'jupyter-structured-clean-dedup', 'id': '515437', 'license': 'Apache-2.0', 'n_tokens_mistral': 1555, 'n_tokens_neox': 1416, 'n_words': 511, 'path': 'example_notebooks/Signals.ipynb', 'repo_name': 'eggerdj/qiskit-dynamics', 'size': '4685'}