Setup Anaconda, Jupyter, and Rust

Software Setup

We are taking a practical approach in the following sections. As such, we need the right tools and environments available in order to keep up with the examples and exercises. We will be using Rust along with packages that will form our scientific stack, such as ndarray (for multi-dimensional containers) and plotly (for interactive graphing), etc. We will write all of our code within a Jupyter Notebook, but you are free to use other IDEs.

Jupyter Lab

Figure 1 - A Jupyter Notebook being edited within Jupyter Lab.
Theme from https://github.com/shahinrostami/theme-purple-please

Install Miniconda

There are many different ways to get up and running with an environment that will facilitate our work. One approach I can recommend is to install and use Miniconda.

Miniconda is a free minimal installer for conda. It is a small, bootstrap version of Anaconda that includes only conda, Python, the packages they depend on, and a small number of other useful packages, including pip, zlib and a few others.

https://docs.conda.io/en/latest/miniconda.html

You can skip Miniconda entirely if you prefer and install Jupyter Lab directly, however, I prefer using it to manage other environments too.

You can find installation instructions for Miniconda on their website, but if you're using Linux (e.g. Ubuntu) you can execute the following commands from in your terminal:

wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh
chmod +x Miniconda3-latest-Linux-x86_64.sh
./Miniconda3-latest-Linux-x86_64.sh

This will download the installation files and start the interactive installation process. Follow the process to the end, where you should see the following message:

Thank you for installing Miniconda3!

All that's left is to close and re-open the terminal window.

Create Your Environment

Once Miniconda is installed, we need to create and configure our environment. If you added Miniconda to your PATH environment during the installation process, then you can run these commands directly from Terminal, Powershell, or CMD.

Now we can create and configure our conda environment using the following commands.

conda create -n darn python=3

You can replace darn (Data Analytics with Rust Notebooks) with a name of your choosing.

This will create a conda environment named darn with the latest Python 3 package ready to go. You should be presented with a list of packages that will be installed and asked if you wish to proceed. To do so, just enter the character y. If this operation is successful, you should see the following output at the end:

Preparing transaction: done
Verifying transaction: done
Executing transaction: done
#
# To activate this environment, use
#
#     $ conda activate darn
#
# To deactivate an active environment, use
#
#     $ conda deactivate

As the message suggests, you will need to type the following command to activate and start entering commands within our environment named darn.

conda activate darn

Once you do that, you should see your terminal prompt now leads with the environment name within parentheses:

(darn) melica:~ shahin$

Note

The example above shows the macOS machine name "melica" and the user "shahin". You will see something different on your machine, and it may appear in a different format on a different operating system such as Windows. As long as the prompt leads with "(darn)", you are on the right track.

This will allow you to identify which environment you are currently operating in. If you restart your machine, you should be able to use conda activate darn within your conda prompt to get back into the same environment.

Install Packages

If your environment was already configured and ready, you would be able to enter the command jupyter lab to launch an instance of the Jupyter Lab IDE in the current directory. However, if we try that in our newly created environment, we will receive an error:

(darn) melica:~ shahin$ jupyter lab
-bash: jupyter: command not found

So let's fix that. Let's install Jupyter Lab and use the -y option which automatically says "yes" to any questions asked during the installation process.

conda install -c conda-forge jupyterlab=1.1.4 -y

We'll also need cmake later on.

conda install -c anaconda cmake -y

Finally, let's install nodejs. This is needed to run our Jupyter Lab extension in the next section.

conda install -c conda-forge nodejs -y

Install Jupyer Lab Extensions

There's one last thing we need to do before we move on, and that's installing any Jupyter Lab extensions that we may need. One particular extension that we need is the plotly extension, which will allow our Jupyter Notebooks to render our Plotly visualisations. Within your conda environment, simply run the following command:

jupyter labextension install @jupyterlab/plotly-extension

This may take some time, especially when it builds your jupyterlab assets, so keep an eye on it until you're returned control over the conda prompt, i.e. when you see the following:

(darn) melica:~ shahin$

Optionally, you may wish to install the purple looking theme from Figure 1 above.

jupyter labextension install @shahinrostami/theme-purple-please

Now we're good to go!

Install Rust

Now we'll install Rust using rustup, but you can check out the other installation methods if you need them.

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

You will be given instructions for adding Cargo's bin directory to your PATH environment variable.

source $HOME/.cargo/env

This will work until your close your terminal, so make sure to add it to your shell profile. I use Z shell (Zsh) so this meant adding the following to .zshrc:

export PATH="$HOME/.cargo/bin:$PATH"

You can make sure everything works by closing and re-opening your terminal and typing cargo. If this returns the usage documentation then you're all set.

Note

Don't forget to activate your environment when opening the terminal.

Install the EvCxR Jupyter Kernel

Now we'll install the EvCxR Jupyter Kernel. If you're wondering how it's pronounced, it's been mentioned to be "Evic-ser". This is what will allow us to execute Rust code in a Jupyter Notebook.

You can get other installation methods methods for EvCxR if you need then, but we will be using:

cargo install evcxr_jupyter
evcxr_jupyter --install

A Quick Test

Let's test if everything is working as it should be. In your conda prompt, within your conda environment, run the following command

jupyter lab

This should start the Jupyter Lab server and launch a browser window with the IDE ready to use.

Figure 2 - A fresh installation of Jupyter Lab.

Let's create a new notebook. In the Launcher tab which has opened by default, click "Rust" under the Notebook heading. This will create a new and empty notebook named Untitled.ipynb in the current directory.

If everything is configured as it should be, you should see no errors. Type the following into the first cell and click the "play" button to execute it and create a new cell.

In [2]:
println!("Hello World!");
Hello World!

If we followed all the instructions and didn't encounter any errors, everything should be working. We should see "Hello World!" in the output cell.

Conclusion

In this section, we've downloaded, installed, configured, and tested our environment such that we're ready to run the following examples and experiments. If you ever find that you're missing Jupyter Lab packages, you can install them in the same way as we installed Jupyter Lab and the others in this section.

Preface

Preface

The Rust programming language has become a popular choice amongst software engineers since its release in 2010. Besides being something new and interesting, Rust promised to offer exceptional performance and reliability. In particular, Rust achieves memory-safety and thread-safety through its ownership model. Instead of runtime checks, this safety is assured at compile time by Rust's borrow checker. This prevents undefined behaviour such as dangling pointers!

In [5]:
println!("Hello World!");
Hello World!

I first encountered Rust sometime around 2015 when I was updating my teaching materials on memory management in C. A year later in 2016, I implemented a simple multi-objective evolutionary algorithm in Rust as an academic exercise (available: https://github.com/shahinrostami/simple_ea). I didn't have any formal training with Rust, nor did I complete any tutorial series, I just figured things out using the documentation as I progressed through my project.

Some example code from this project takes ZDT1 from its mathematical expression in Equation 1

$$ \begin{aligned} f_1(x_1) &= x_1 \tag{1} \\ f_2(x) &= g \cdot h \\ g(x_2,\ldots,x_{\mathrm{D}}) &= 1 + 9 \cdot \sum_{d=2}^{\mathrm{D}} \frac{x_d}{(D-1)}\\ h(f_1,g) &= 1 - \sqrt{f1/g} \end{aligned} $$

to the Rust implementation below.

In [3]:
pub fn zdt1(parameters: [f32; 30]) -> [f32; 2] {

    let f1 = parameters[0];
    let mut g = 1_f32;

    for i in 1..parameters.len() {
        g = g + ((9_f32 / (parameters.len() as f32 - 1_f32)) * parameters[i]);
    }

    let h = 1_f32 - (f1 / g).sqrt();
    let f2 = g * h;

    return [f1, f2];
}

It was interesting to see that since writing this code in 2016, some of my dependencies have been deprecated and replaced.

My greatest challenge was breaking away from what I already knew. Until this point, I was familiar with languages such as C, C++, C#, Java, Python, MATLAB, etc., with the majority of my time spent working with memory managed languages. I found myself resisting Rust's intended usage, and it is still something I'm working on.

Now that I am about to commence my sabbatical from my University post, I've decided to try Rust again. This time, I'm going to write a book which focusses on using Rust and Jupyter Notebooks to implemented algorithms and conducted experiments, most likely in the fields of search, optimisation, and machine learning. Can we write and execute all our code in a Jupyter Notebook? Yes! Should we? Probably not. However, I enjoy the workflow, and making this an enjoyable process is important to me.

Note

I aim to generate everything in this book through code. This means you will see the code for all my figures and tables, including things like flowcharts.

This book is currently available in early access form. It is being actively worked on and updated.

Every section is intended to be independent, so you will find some repetition as you progress from one section to another.

YAML for Configuration Files

Preamble

In [ ]:
# used to create block diagrams
%reload_ext xdiag_magic
%xdiag_output_format svg
    
import os

Introduction

In this section, we're going to have a look at YAML, which is a recursive acronym for "YAML Ain't Markup Language". It is a data interchange file format that is often found with a .yaml or .yml file extension.

What this section is most interested in is using YAML for configuration files, enabling us to extract parameters that we use within our programs so that they can be separated. This means that configuration files can be shared between scripts/programs, and they can be modified without needing to modify source code.

In [2]:
%%blockdiag
{
    orientation = portrait
    "config.yaml" <-> "notebook.ipynb"
    "config.yaml" [color = '#ffffcc']
}
blockdiag { orientation = portrait "config.yaml" <-> "notebook.ipynb" "config.yaml" [color = '#ffffcc'] } config.yamlnotebook.ipynb

Of course, there are many alternatives such as JavaScript Object Notation (JSON) or Tom's Obvious, Minimal Language (TOML), and they all have their advantages and disadvantages. We won't do a full comparison of YAML vs. alternatives, but some advantages of YAML are:

  • It is human-readable, making it easy for someone to read or create them.
  • Many popular programming languages have support for managing YAML files.
  • YAML is a superset of JSON, meaning that JSON can be easily converted to YAML if needed.

We can see that we have some key-value mappings, where the key appears before the colon and the value appears after. The first key we have is learning_rate with a value of 0.1, the second is random_seed with a value of 789108, and the third is maintainer with a value of Shahin Rostami. Finally, I have included a key mapping to a sequence, where categories has the value of list which contains the items "hotdog" and "not a hotdog".

You can read more about YAML at the YAML Specification web page.

So that we can work with this file later on in this section, you can paste the above YAML into a file in the same directory as this notebook and name it config.yaml. Alternatively, you can just run the cell below which will do it for you.

In [3]:
config_string = '''learning_rate:  0.1
random_seed: 789108
maintainer: Shahin Rostami
categories:
- hotdog
- not a hotdog'''

with open('config.yaml', 'w') as f:
    f.write(config_string)

Getting Python Ready for YAML

Before we begin working with YAML files in Python, we need to make sure we have PyYAML installed. There are alternatives to PyYAML available, but they may not be compatible with the following instructions. However, you can use ruamel.yaml as a drop-in replacement if you wish.

Some options to install PyYAML are with:

Anaconda

conda install -c conda-forge pyyaml

pip

pip install PyYAML

Once you have the package installed you should be ready to import the PyYAML package within Python.

In [4]:
import yaml

Loading YAML with Python

It is surprisingly easy to load a YAML file into a Python dictionary with PyYAML.

In [5]:
with open('config.yaml') as f:
    config = yaml.load(f, Loader=yaml.FullLoader)

We can confirm that it worked by displaying the contents of the config variable.

In [6]:
config
Out[6]:
{'learning_rate': 0.1,
 'random_seed': 789108,
 'maintainer': 'Shahin Rostami',
 'categories': ['hotdog', 'not a hotdog']}

It's as easy as that. We can now access the various elements of the data structure like a normal Python dictionary.

In [7]:
config['learning_rate']
Out[7]:
0.1
In [8]:
config['categories']
Out[8]:
['hotdog', 'not a hotdog']
In [9]:
config['categories'][0]
Out[9]:
'hotdog'

Updating YAML with Python

Let's say we want to update our learning_rate to 0.2 and add an extra category to our category list. We can do this using the normal Python dictionary manipulation.

In [10]:
config['learning_rate'] = 0.2
In [11]:
config['categories'].append('kind of hotdog')

We can then write this back to the config.yaml file to save our changes.

In [12]:
with open('config.yaml', 'w') as f:
    config = yaml.dump(config, stream=f,
                       default_flow_style=False, sort_keys=False)

All done! We can confirm this by loading our YAML File again and displaying the dictionary.

In [13]:
with open('config.yaml') as f:
    config = yaml.load(f, Loader=yaml.FullLoader)
config
Out[13]:
{'learning_rate': 0.2,
 'random_seed': 789108,
 'maintainer': 'Shahin Rostami',
 'categories': ['hotdog', 'not a hotdog', 'kind of hotdog']}

Conclusion

In this section, we briefly introduced YAML before using the PyYAML package to load, manipulate, and save a collection of configuration settings that we stored in a file named config.yaml. Keeping your configuration settings separate from your source code comes with multiple benefits, e.g. allowing modification of these configurations without modifying source code, automation and search throughout your project, and sharing configurations between multiple bits of work.

Using a Framework with a Custom Objective Function

Preamble

In [1]:
# used to create block diagrams
%reload_ext xdiag_magic
%xdiag_output_format svg
    
import numpy as np                   # for multi-dimensional containers
import pandas as pd                  # for DataFrames
import plotly.graph_objects as go    # for data visualisation
import platypus as plat              # multi-objective optimisation framework

# Optional Customisations
import plotly.io as pio              # to set shahin plot layout
pio.templates['shahin'] = pio.to_templated(go.Figure().update_layout(
    legend=dict(orientation="h",y=1.1, x=.5, xanchor='center'),
    margin=dict(t=0,r=0,b=40,l=40))).layout.template
pio.templates.default = 'shahin'
pio.renderers.default = "notebook_connected" # remove when running locally 

Introduction

When applying multi-objective optimisation algorithms to real-world problems, we will often need to implement the objective functions ourselves. This problem comes in two parts:

  1. We need to design an objective function that correctly represents our real-world problem, taking the problem variables and producing the correct objective values;
  2. We need to implement this objective function in a way that can work with our optimiser.

This comes down to passing the desired number of problem variables to a custom objective function and receiving the desired number of objective values.

In [2]:
%%blockdiag
{
    orientation = portrait
    "Problem Variables" -> "Objective Function" -> "Objective Values"
    "Objective Function" [color = '#ffffcc']
}
blockdiag { orientation = portrait "Problem Variables" -> "Objective Function" -> "Objective Values" "Objective Function" [color = '#ffffcc'] } Problem VariablesObjective FunctionObjective Values

When preparing to implement multi-objective optimisation experiments, it's often more convenient to use a ready-made framework/library instead of programming everything from scratch. Many libraries and frameworks have been implemented in many different programming languages. With our focus on multi-objective optimisation, our choice is an easy one. We will choose Platypus which has a focus on multi-objective problems and optimisation.

Platypus is a framework for evolutionary computing in Python with a focus on multiobjective evolutionary algorithms (MOEAs). It differs from existing optimization libraries, including PyGMO, Inspyred, DEAP, and Scipy, by providing optimization algorithms and analysis tools for multiobjective optimization.

In this section, we will use the Platypus framework to apply the Non-dominated Sorting Genetic Algorithm II (NSGA-II)1 to a custom objective function.

The Custom Objective Function

For our custom objective function we will look to implement F2 from Schaffer 19852, which is described as being a two-valued function of one variable. The function has been listed in Equation 1.

$$ \text{Minimize} = \begin{cases} f_{1}\left(x\right) = x^{2} \\ \tag{1} f_{2}\left(x\right) = \left(x-2\right)^{2} \\ \end{cases} $$

Let's implement this objective function using Python.

In [3]:
def schaffer_f1(x):
    f1 = x[0]**2
    f2 = (x[0]-2)**2
    return [f1, f2]

Now let's get this Python function into the Platypus Problem object which can be used during the evaluation stage of Platypus' optimisation process.

First, we will instantiate an instance of the Problem object, passing in the parameters $1$ and $2$, indicating that we want 1 problem variable and 2 objective values, respectively.

In [4]:
problem = plat.Problem(1, 2)

Next, we need to specify the type of the problem variables and their boundaries. In this case, we want real-valued problem variables between -10 and 10.

In [5]:
problem.types[:] = plat.Real(-10, 10)

Finally, we will assign our implementation of the Schaffer F1 function to our Problem object.

In [6]:
problem.function = schaffer_f1

Now we're ready to apply an optimisation algorithm to the problem. Let's create an instance of the NSGA-II optimiser, and pass in our problem object as a parameter for its configurations.

In [7]:
algorithm = plat.NSGAII(problem)

Now we can execute the optimisation process. Let's give the optimiser a budget of 10,000 function evaluations as a termination criterion. This may take some time to complete depending on your processor speed and the number of function evaluations.

In [8]:
algorithm.run(10000)

Finally, we can display the results. In this case, we will be printing the objective values for each solution in the final population of the above execution.

In [9]:
for solution in algorithm.result:
    print(solution.objectives)
[4.000406116449771, 1.0307637417395e-08]
[2.0661887134436118e-07, 3.998181992676149]
[2.1701411576087213, 0.2775815676054482]
[2.047811296694047, 0.323741001257001]
[3.85795280674386, 0.0012839892422746124]
[0.752300481583038, 1.2828901995136763]
[3.7335101024681214, 0.004592866561791519]
[0.9117035925644464, 1.0923767564542999]
[1.8623633131194153, 0.4036260040056451]
[0.8737830994884715, 1.1347284607040173]
[2.2510520799554907, 0.24964947062424778]
[0.07764548290622653, 2.963047920330557]
[0.6916914011009517, 1.3649719332745425]
[0.23659564877888764, 2.290951698704208]
[1.9585886984225853, 0.3606052065961433]
[0.09141036747786122, 2.882044467658547]
[1.3508179132222, 0.7018302157169464]
[0.05861003020580801, 3.0902296900021624]
[0.009522277661513296, 3.6191936435796506]
[0.20831162577328802, 2.3826648876630343]
[2.3357939109809247, 0.22247218088240192]
[0.17617409041500665, 2.497250194986716]
[0.8222956207433623, 1.1950749310287432]
[2.4348663862579305, 0.19324302529430307]
[0.04722632331267073, 3.1779615920583004]
[1.2013076140510766, 0.8171404383132232]
[1.2398721228004768, 0.7858903126885822]
[1.154527527317117, 0.8565698219558984]
[0.5352317220060879, 1.6088504153241636]
[3.5967413411075397, 0.0107106624376574]
[2.956686953256236, 0.07867915651016248]
[0.06690611713824247, 3.0322574452636397]
[3.955898122980472, 0.00012223575944294592]
[0.9675304533431293, 1.0330054180319967]
[3.663820749193919, 0.007376927999728054]
[2.8433301863412423, 0.09845932164265439]
[0.4532346290789683, 1.760326523468327]
[2.666832761916732, 0.13465669335675234]
[2.9298324600209735, 0.08313111117161856]
[0.3452600320302341, 1.9949067580555013]
[2.7502214336068773, 0.11670479929366886]
[0.021632339349669443, 3.433314882873397]
[3.127081161937991, 0.05365917601011508]
[3.0595161909986706, 0.06292708944506217]
[0.49503002027424053, 1.6806952277543725]
[0.13269953390616224, 2.6755816448873646]
[0.4909892386826973, 1.6881642675289228]
[1.069628745906803, 0.9327144448233435]
[0.21165344974443542, 2.3714210742247115]
[0.00042548244659934515, 3.9179165790143666]
[0.12193226241956984, 2.725180244804017]
[0.2529102418024825, 2.2413029568584952]
[0.3735599386902117, 1.928777940424643]
[0.407215118948672, 1.8546787522775225]
[0.01902321540284389, 3.467324523682896]
[3.4270709706303144, 0.022131029612221954]
[1.3927284792024572, 0.6721717676228457]
[2.310777073648161, 0.2302809443532351]
[0.10663910855534246, 2.80041334824992]
[1.6782466262782314, 0.49636028822992345]
[2.564552083012348, 0.158864506475574]
[1.5990506691201138, 0.5409076594848761]
[1.6466273900678845, 0.5137881912339534]
[2.507086749219784, 0.17357366503684019]
[3.5195942098870043, 0.015361580351052272]
[3.346732876383687, 0.029101700781913598]
[0.16701957396220501, 2.53229844074245]
[1.296487169875423, 0.7419515494189568]
[0.9813285269356339, 1.0188474315901757]
[0.431990830820378, 1.8029504558687022]
[0.005594521150650002, 3.7064083944495607]
[0.028762881843416077, 3.3503779546120858]
[0.002768126489055616, 3.7923161751441232]
[0.6688676445871639, 1.3974944904828173]
[0.6331087140148225, 1.4503835571347692]
[0.14917494460438735, 2.6042480487060096]
[0.03490266701368317, 3.287612447474493]
[1.019691804230725, 0.98050019356262]
[1.7610500734362216, 0.45286759965298273]
[0.2889820244203225, 2.1387000915740364]
[1.9946522245818739, 0.3453659339686975]
[1.8170967153001796, 0.4251075234709829]
[1.7183618263287819, 0.47490979343475437]
[1.539482996149637, 0.5764468348473448]
[1.1180491417008018, 0.888535426483315]
[0.3709436302320389, 1.9347379567076086]
[2.595263224841164, 0.15133495107378572]
[1.1042434065425988, 0.9009239439414535]
[0.5600452166100484, 1.566598463156636]
[0.58496103481646, 1.5256512177068295]
[0.2657901312645098, 2.2035966145856904]
[1.2880316070285809, 0.7483724003857195]
[0.15154734840880954, 2.5943840382383727]
[2.6393378081564633, 0.14092223738050583]
[0.2762477642642471, 2.173876667567008]
[0.014128004691408657, 3.5386828710928633]
[0.8007826736502606, 1.2213242260070742]
[0.30214239286818895, 2.10344316698689]
[0.013468293147486475, 3.54925639071522]
[3.281546468271266, 0.03553077050050727]

Visualising Solutions in Objective Space

In the last section, we concluded by printing the objective values for every solution. This information will be easier to digest using a plot, so let's quickly put the data into a Pandas DataFrame and then use Plotly to create a scatterplot. Let's start by moving our Platypus data structure to a DataFrame.

In [10]:
objective_values = np.empty((0, 2))

for solution in algorithm.result:
    y = solution.objectives
    objective_values = np.vstack([objective_values, y])
    
# convert to DataFrame
objective_values = pd.DataFrame(objective_values, columns=['f1','f2'])

With that complete, we can have a peek at our DataFrame to make sure we've not made any obvious mistakes.

In [11]:
objective_values
Out[11]:
f1 f2
0 4.000406e+00 1.030764e-08
1 2.066189e-07 3.998182e+00
2 2.170141e+00 2.775816e-01
3 2.047811e+00 3.237410e-01
4 3.857953e+00 1.283989e-03
... ... ...
95 1.412800e-02 3.538683e+00
96 8.007827e-01 1.221324e+00
97 3.021424e-01 2.103443e+00
98 1.346829e-02 3.549256e+00
99 3.281546e+00 3.553077e-02

100 rows × 2 columns

With no obvious issues, let's visualise the results using a scatterplot.

In [12]:
fig = go.Figure()
fig.add_scatter(x=objective_values.f1, y=objective_values.f2, mode='markers')
fig.show()

Great! If you search the literature for the true Pareto-optimal front for Schaffer F1, you can see that our approximation is looking as expected.

Conclusion

In this section, we have demonstrated how we can use a popular multi-objective optimisation algorithm, NSGA-II, to approximate multiple trade-off solutions to the Schaffer F1 test problem. We did this using the Platypus framework, and by implementing our custom objective function. You can use this approach to write your own objective functions that can be optimised by any algorithm in the Platypus framework.


  1. Deb, K., Pratap, A., Agarwal, S., & Meyarivan, T. A. M. T. (2002). A fast and elitist multiobjective genetic algorithm: NSGA-II. IEEE transactions on evolutionary computation, 6(2), 182-197. 

  2. Schaffer, J.. (1985). Multiple Objective Optimization with Vector Evaluated Genetic Algorithms. Proceedings of the First Int. Conference on Genetic Algortihms, Ed. G.J.E Grefensette, J.J. Lawrence Erlbraum. 93-100. 

Fourier Transform Algorithm

Preamble

In [55]:
# used to create block diagrams
%reload_ext xdiag_magic
%xdiag_output_format svg
    
import numpy as np                   # for multi-dimensional containers
import pandas as pd                  # for DataFrames
import plotly.graph_objects as go    # for data visualisation
import plotly.io as pio              # to set shahin plot layout
from plotly.subplots import make_subplots
import scipy.fftpack                 # discrete Fourier transforms

pio.templates['shahin'] = pio.to_templated(go.Figure().update_layout(margin=dict(t=0,r=0,b=40,l=40))).layout.template
pio.templates.default = 'shahin'

Fast Fourier Transform

Preamble

In [1]:
# used to create block diagrams
%reload_ext xdiag_magic
%xdiag_output_format svg
    
import numpy as np                   # for multi-dimensional containers
import pandas as pd                  # for DataFrames
import plotly.graph_objects as go    # for data visualisation
import plotly.io as pio              # to set shahin plot layout
from plotly.subplots import make_subplots
import scipy.fftpack                 # discrete Fourier transforms

pio.templates['shahin'] = pio.to_templated(go.Figure().update_layout(margin=dict(t=0,r=0,b=40,l=40))).layout.template
pio.templates.default = 'shahin'
pio.renderers.default = "notebook_connected"

In a previous section we looked at how to create a single Sine Wave and visualise it in the time domain.

In [2]:
sample_rate = 1000
start_time = 0
end_time = 10

time = np.arange(start_time, end_time, 1/sample_rate)

frequency = 3
amplitude = 1
theta = 0

sinewave = amplitude * np.sin(2 * np.pi * frequency * time + theta)

fig = go.Figure(layout=dict(xaxis=dict(title='Time (sec)'),yaxis=dict(title='Amplitude')))
fig.add_scatter(x=time, y=sinewave)
fig.show()

Fourier Transform

In [3]:
freq = scipy.fftpack.fft(sinewave)/len(time)
In [4]:
hz = np.linspace(0, sample_rate/2, int(np.floor(len(time)/2)+1))

fig = go.Figure(
    layout=dict(
        xaxis=dict(title='Frequency (Hz)', range=[0, np.max(frequency) * 1.2]),
        yaxis=dict(title='Amplitude'))
)

fig.add_scatter(x=hz, y=2 * np.abs(freq))
fig.show()
In [ ]:
 

Summing Sine Waves

In [5]:
sample_rate = 1000
start_time = 0
end_time = 10
theta = 0

time = np.arange(start_time, end_time, 1/sample_rate)

frequency = [3, 5, 2, 1, 10]
amplitude = [1, 2, 7, 3, 0.1]

fig = go.Figure(layout=dict(xaxis=dict(title='Time (sec)'),yaxis=dict(title='Amplitude')))
fig.add_scatter(x=time, y=sinewave)
fig.show()
In [6]:
fig = go.Figure(layout=dict(xaxis=dict(title='Time (sec)'),yaxis=dict(title='Amplitude')))
fig.add_scatter(x=time, y=sinewave)
fig.show()