Generating Signals with Scipy and Plotting with Matplotlib
Authors
Setup
Import Libraries
import xarray as xr
import matplotlib.pyplot as plt
import numpy as np
from scipy.fft import fft, fftfreq
from scipy.signal import square, sawtooth, chirpSignal analysis is key to uncovering patterns in time series data like EEG or LFP recordings. And to get a better understanding of the signals we are working with, it’s important to visualize them clearly. Here, we will work with different generated signals and make figures that display those signals in a way that makes the plots easy to read.
In this notebook, we will focus on generating various types of signals using Python’s Numpy and Scipy packages as well as Python’s built-in functions and plot them with Matplotlib.
Section 1: Generating Periodic Signals
Periodic waveforms play a vital role in neuroscience as models for rhythmic stimuli and neural oscillations. By generating such waveforms we can, for instance, simulate neural activity that exhibits periodic behavior in response to visual stimuli, such as flashing lights or gradual intensity changes.
This section provides a hands-on introduction to creating and visualizing periodic waveforms using the scipy.signal module. Through a series of exercises, we’ll explore the characteristics of square and sawtooth waves at various frequencies. We will also explore generating chirp (a chirp is a signal in which the frequency increases (‘up-chirp’) or decreases (‘down-chirp’) with time). We will use the same chirp method to create cosine waves too!
| Code | Description |
|---|---|
np.pi |
The mathematical constant π, approximately 3.14159. |
np.linspace(start, stop, n) |
Generates an array of n evenly spaced values from start to stop. |
np.exp(t) |
Generates an exponential over time array t. |
square(2 * np.pi * f * t) |
Generates a square wave with frequency f over time array t. |
sawtooth(2 * np.pi * f * t) |
Produces a sawtooth wave with frequency f over time array t. |
plt.plot(x, y) |
Plots the data in y against the corresponding values in x. |
Exercises
Example: Create a 1 Hz square wave sampled 100 times for 1 second.
f = 1 # Hz
t = np.linspace(0, 1, 100)
w = square(2 * np.pi * f * t)
plt.plot(t, w)This is a square wave of 1 Hz frequency. A frequency of n Hz means that the pattern you see above repeats n times in a second. Let’s see that happen in the following exercises.
Exercise: Create a 2 Hz square wave sampled 100 times for 1 second. How many repetitions of the pattern do you see here?
Solution
f = 2 # Hz
t = np.linspace(0, 1, 100)
w = square(2 * np.pi * f * t)
plt.plot(t, w)Exercise: Create a 10 Hz square wave sampled 100 times for 1 second.
Solution
f = 10 # Hz
t = np.linspace(0, 1, 100)
w = square(2 * np.pi * f * t)
plt.plot(t, w)Exercise: Using the sawtooth function, create a 5 Hz sawtooth signal sampled 100 times for 1 second.
Solution
f = 5 # Hz
t = np.linspace(0, 1, 100)
w = sawtooth(2 * np.pi * f * t)
plt.plot(t, w)Exercise: Numpy arrays are built for mathematical operations. For example, we can re-generate the signal we created before with double the magnitude simply by multiplying the signal w by :
Solution
f = 5 # Hz
t = np.linspace(0, 1, 100)
w = sawtooth(2 * np.pi * f * t)
w_double_magnitude = 2 * w
plt.plot(t, w)Exercise: Numpy package also provides many useful functions that we can use to transform our data. For instance, np.exp() or np.log().
Write the code for , where is the sawtooth waveform and is time. Plot the resulting over time.
Solution
f = 5 # Hz
t = np.linspace(0, 1, 100)
w = np.exp(t) * sawtooth(2 * np.pi * f * t)
plt.plot(t, w)Section 2: Chirp Signal
A chirp is a signal in which the frequency increases or decreases over time. In the context of the scipy.signal.chirp function, the signal starts at frequency f0 and linearly changes to frequency f1 by time t1. The rate of this frequency change can follow different methods; when the ’linear’ method is used, the frequency changes at a constant rate.
Here is a summary table for the examples of chirp signals:
| Example | Start Frequency (f0) |
End Frequency (f1) |
Time (t1) |
Frequency Change | Description |
|---|---|---|---|---|---|
| Constant Frequency | 5 Hz | 5 Hz | 10 s | None (Constant) | A sine wave with a constant frequency. |
| Increasing Frequency | 5 Hz | 10 Hz | 10 s | Linear Increase | A sine wave that linearly increases in frequency over time. |
| Decreasing Frequency | 10 Hz | 5 Hz | 10 s | Linear Decrease | A sine wave that linearly decreases in frequency over time. |
| Code | Description |
|---|---|
chirp(t, f0, f1, t1) |
Generates a chirp signal with start frequency f0 and end frequency f1 over a time period t for the samples given in the array t. |
plt.plot(x, y, label = 'some label name') |
Plots the data in y against the corresponding values in x. label provides a name for the curve to be displayed in the legend of the figure. |
plt.figure() |
Creates a new figure. |
plt.legend() |
Creates a legend for a plot. |
plt.title('some title') |
Creates title for a plot. |
Exercises
Example: Generate constant frequency (1Hz) signal with chirp.
t = np.linspace(0, 1, 1000)
w = chirp(t, f0=1, f1=1, t1=1)
plt.plot(t, w)Exercise: Generate a chirp signal that starts at a frequency of 1 Hz and increases to 10 Hz over a duration of 1 second.
Solution
t = np.linspace(0, 1, 1000)
w = chirp(t, f0=1, f1=10, t1=1)
plt.plot(t, w)Exercise: Generate the same chirp signal as in the last exercise but with a decreasing frequency.
Solution
t = np.linspace(0, 1, 1000)
w = chirp(t, f0=10, f1=1, t1=1)
plt.plot(t, w)What other options are available in the chirp function to control the change of frequency over time? Look up the available methods that you can use in the documentation for the chirp function.
Exercise: Make different chirp signals using the different methods and plot all signals together in one plot.
Solution
t = np.linspace(0, 1, 1000)
w = chirp(t, f0=10, f1=1, t1=1)
plt.plot(t, w)
w = chirp(t, f0=10, f1=1, t1=1, method="quadratic")
plt.plot(t, w)
w = chirp(t, f0=10, f1=1, t1=1, method="logarithmic")
plt.plot(t, w)Exercise: Let’s label each line plot so it’s clear which line comes from which method. We can do this by:
- setting the
labelargument of theplt.plot()function. - calling
plt.legend()at the end to make the legend appear in the plot.
Solution
t = np.linspace(0, 1, 1000)
w = chirp(t, f0=10, f1=1, t1=1)
plt.plot(t, w, label="linear")
w = chirp(t, f0=10, f1=1, t1=1, method="quadratic")
plt.plot(t, w, label="quadratic")
w = chirp(t, f0=10, f1=1, t1=1, method="logarithmic")
plt.plot(t, w, label="logarithmic")
plt.legend()Exercise: As a final step let’s make the figure wider so we can see the difference between the lines a bit more clearly.
This is a figure-level property that we want to change. We can specify figure-level properties using the function plt.figure() which will be placed in the very beginning of the visualization code.
Here, we want to change the figure size which we can do by setting the figsize argument: plt.figure(figsize=(15, 3)). The first value specifies the width, and the second value specifies the height. Let’s try it!
Solution
plt.figure(figsize=(15, 3))
t = np.linspace(0, 1, 1000)
w = chirp(t, f0=10, f1=1, t1=1)
plt.plot(t, w, label="linear")
w = chirp(t, f0=10, f1=1, t1=1, method="quadratic")
plt.plot(t, w, label="quadratic")
w = chirp(t, f0=10, f1=1, t1=1, method="logarithmic")
plt.plot(t, w, label="logarithmic")
plt.legend(bbox_to_anchor=(0.12,0.5))Section 3: Create Composite Signals
In this section, we will learn about combining multiple periodic signals together, creating more complex signals.
We will make use of matplotlib and how powerful it is for customizing plots
| Code | Description |
|---|---|
np.linspace(start, stop, n_points) |
Create n_points equally spaced values between start and stop. |
plt.plot(x, y) |
Plot x on the x-axis and y on the y-axis. |
plt.xlabel('X-axis') |
Set the x-axis label as ‘X-axis’. |
plt.ylabel('Y-axis') |
Set the y-axis label as ‘Y-axis’. |
np.random.randn(n_points) |
Generate n_points random values drawn from a standard normal distribution. |
Exercises
Example: Create a composite wave from two square waves
f1 = 1
f2 = 3
t = np.linspace(0, 1, 1000)
w1 = square(2 * np.pi * f1 * t)
w2 = square(2 * np.pi * f2 * t)
plt.plot(t, w1 + w2)Exercise: Create composite wave made of a 10-Hz sawtooth and a 1-Hz square wave
Solution
f1 = 1.
f2 = 10.
t = np.linspace(0, 1, 1000)
w1 = square(2 * np.pi * f1 * t)
w2 = sawtooth(2 * np.pi * f2 * t)
plt.plot(t, w1 + w2)Exercise: Create a composite signal made of two chirps and add axis labels and title with legend
Solution
t = np.linspace(0, 1, 1000)
w1 = chirp(t, f0=20, f1=1, t1=1)
w2 = chirp(t, f0=1, f1=50, t1=1)
plt.plot(t, w1+w2)
plt.xlabel('Time [s]')
plt.ylabel('Value [some unit]')
plt.title('Composite signal of two chirps')Exercise: Let’s learn a bit more about matplotlib and optimize this figure further.
We already specified some “axes-level” properties in the last exercise. In the following exercises, Let’s change both “figure-level” properties as well the the arguments in the plotting function to improve our plot.
Let’s start by making the figure a bit wider as we did before.
t = np.linspace(0, 1, 1000)
w1 = chirp(t, f0=20, f1=1, t1=1)
w2 = chirp(t, f0=1, f1=50, t1=1)
w = w1 + w2Solution
plt.figure(figsize=(10, 3))
plt.plot(t, w)
plt.xlabel('Time [s]')
plt.ylabel('Value [some unit]')
plt.title('Composite signal of two chirps')Exercise: Make the line a bit thicker by setting the linewidth argument in the plt.plot() function.
Solution
plt.figure(figsize=(10, 3))
plt.plot(t, w, linewidth=3)
plt.xlabel('Time [s]')
plt.ylabel('Value [some unit]')
plt.title('Composite signal of two chirps')Exercise: Change the color to black by setting the color argument.
Solution
plt.figure(figsize=(10, 3))
plt.plot(t, w, linewidth=3, color="k")
plt.xlabel('Time [s]')
plt.ylabel('Value [some unit]')
plt.title('Composite signal of two chirps')Exercise: Make the resolution higher by setting the “figure-level” property dpi. Let’s try a dpi of 200 for instance.
Solution
plt.figure(figsize=(10, 3), dpi=200)
plt.plot(t, w, linewidth=3, color="k")
plt.xlabel('Time [s]')
plt.ylabel('Value [some unit]')
plt.title('Composite signal of two chirps')Exercise: You can play around with the figsize and dpi to adjust the font size in the figure. For instance, a higher dpi combined with a lower figsize results in bigger font size.
Solution
plt.figure(figsize=(6, 2), dpi=200)
plt.plot(t, w, linewidth=3, color="k")
plt.xlabel('Time [s]')
plt.ylabel('Value [some unit]')
plt.title('Composite signal of two chirps')Section 4: Plotting multiple signals together in the same figure with subplots
| Code | Description |
|---|---|
plt.plot(x, y) |
Plot y values against x values. |
plt.subplot(nrows, ncols, nindex) |
plt.subplot(211) tells Python to create a grid of subplots with 2 rows and 1 column and to use the first subplot (top plot) for the current plotting commands. If you were to add another plotting command with plt.subplot(212), it would plot in the second subplot (bottom plot). |
plt.tight_layout() |
Adjust the padding between and around subplots. |
plt.savefig(filename) |
Save the figure as an image or vector graphic file. |
fig, axes = plt.subplots(nrows, ncols, sharex = True) |
Another method to create a figure with subplots (notice the s at the end of subplots here, that makes it a different function). axes can be indexed to refer to a specific subplot, f.ex. the subplot on the first row and second column would be axes[0,1]. Setting sharex or sharey to be True forces the x- or y-axis, respectively, to be the same across different subplots. |
Exercises
Example: Make a figure with subplots where the composite signal is plotted in the third subplot and two original signals w1 and w2 are plotted in the first and second subplot, respectively.
t = np.linspace(0, 1, 1000)
w1 = chirp(t, f0=20, f1=1, t1=1)
w2 = chirp(t, f0=1, f1=50, t1=1)
plt.subplot(311)
plt.plot(t, w1)
plt.title('First chirp')
plt.subplot(312)
plt.plot(t, w2)
plt.title('Second chirp')
plt.subplot(313)
plt.plot(t, w1+w2)
plt.title('Composite signal of two chirps')
plt.xlabel('Time [s]')
plt.ylabel('Value [some unit]')Exercise: As you can see, the different subplots partly cover one another, which is not good. Adding plt.tight_layout to the code adjusts the padding around the subplots so that they don’t overlap.
Solution
t = np.linspace(0, 1, 1000)
w1 = chirp(t, f0=20, f1=1, t1=1)
w2 = chirp(t, f0=1, f1=50, t1=1)
plt.subplot(311)
plt.plot(t, w1)
plt.title('First chirp')
plt.subplot(312)
plt.plot(t, w2)
plt.title('Second chirp')
plt.subplot(313)
plt.plot(t, w1+w2)
plt.title('Composite signal of two chirps')
plt.xlabel('Time [s]')
plt.ylabel('Value [some unit]')
plt.tight_layout()Exercise: Make a figure with 4 subplots where subplots 1 to 3 show three different signals and the 4th subplot shows the composite signal.
Solution
t = np.linspace(0, 1, 1000)
w1 = chirp(t, f0=20, f1=1, t1=1)
w2 = chirp(t, f0=1, f1=50, t1=1)
w3 = chirp(t, f0=3, f1=20, t1=1)
plt.figure(figsize = (8,10))
plt.subplot(411)
plt.plot(t, w1)
plt.title('First chirp')
plt.subplot(412)
plt.plot(t, w2)
plt.title('Second chirp')
plt.subplot(413)
plt.plot(t, w2)
plt.title('Third chirp')
plt.subplot(414)
plt.plot(t, w1+w2+w3)
plt.title('Composite signal of three chirps')
plt.xlabel('Time [s]')
plt.ylabel('Value [some unit]')
plt.tight_layout()Exercise: As a final step let’s save the figure using the plt.savefig() method. You can adjust the figure size, linewidths, and color of the lines using the parameters you learned about above.
Hint: to make sure all the elements of the plot are included in the saved figure set bbox_inches="tight" in the plt.savefig() function.
Solution
t = np.linspace(0, 1, 1000)
w1 = chirp(t, f0=20, f1=1, t1=1)
w2 = chirp(t, f0=1, f1=50, t1=1)
w3 = chirp(t, f0=3, f1=20, t1=1)
plt.figure(figsize=(5, 7), dpi=200)
plt.subplot(411)
plt.plot(t, w1, linewidth=3, color="k")
plt.title('First chirp')
plt.subplot(412)
plt.plot(t, w2, linewidth=3, color="k")
plt.title('Second chirp')
plt.subplot(413)
plt.plot(t, w3, linewidth=3, color="k")
plt.title('Third chirp')
plt.subplot(414)
plt.plot(t, w1+w2+w3, linewidth=3, color="k")
plt.title('Composite signal of three chirps')
plt.xlabel('Time [s]')
plt.ylabel('Value [some unit]')
plt.tight_layout()
plt.savefig("fig1.png", bbox_inches="tight")