Lissajous curves

Jose M Sallan 2021-07-16 6 min read

A Lissajous curve is a graph of a two-dimensional set of parametric equations of the form:

\[\begin{eqnarray} x &=& A sin \left( w_1t + \delta \right) \\ y &=& B sin \left( w_2t \right) \end{eqnarray}\]

These curves were first investigated in 1815 by Nathaniel Bowditch. Jules Antoine Lissajous studied them in more detail by 1857 and they were named after him since then.

Lissajous curves represent complex harmonic mation. \(A\) and \(B\) determine the width to height ratio, but the curve is more sensitive to the frequency ratio \(w_1/w_2\) ratio. Lissajous curves are closed if the frequency ratio is rational, and \(w_1\) and \(w_2\) determine the number of vertical and horizontal lobes, respectively.

Here I will be using Lissajous curves to introduce some features of data handling and visualization with the tidyverse family packages:

  • tabular data handling with dplyr,
  • functional programming with the pmap_df function of purrr,
  • plotting Lissajous curves with geom_path() and facet_grid() from ggplot2
  • and creating aminated GIFs with gganimate and transformr.
library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(purrr)
library(ggplot2)
library(gganimate)
library(transformr)

Plotting a Lissajous curve

We can determine the points of a Lissajous curve with the lissajous function. The function returns a data frame with values of x and y, and ratio and phase variables are added for later plots

lissajous <- function(w1, w2, diff_phase, ratio, phase){
  t <- seq(0, 2*pi, length.out = 100)
  x <- sin(w1*t + diff_phase)
  y <- sin(w2*t)
  df <- data.frame(x = x, y = y, ratio = ratio, phase = phase)
  return(df)
}

Once we have the coordinates of the plots, we can represent the curve with geom_path(). This geom connects observations in the order they appear in the data. If we use geom_line() points are connected by order of the x axis. I use theme_void() because I don’t need coordinate axis in this kind of plots.

ggplot(lissajous(w1 = 1, w2 = 3, diff_phase = pi/4, ratio = "1/3", phase = "pi/4"), aes(x,y)) +
  geom_path() +
  theme_void() 

The above plot is a Lissajous curve with \(w_1=1\) and \(w_2=3\). Note that the figure has one vertical lobe and three horizontal lobes.

Plotting several Lissajous curves at once

Let’s try to plot several Lissajous curves together. I will be plotting eight ratios of frequencies for five different phase differences. I am using expand.grid for generating all combinations and mutate to obtain function parameters from ratio and phase labels.

lissajous_values <- expand.grid(ratio = c("1/1", "1/2", "1/3", "2/3", "3/4", "3/5", "4/5", "5/6"), phase = c("0", "pi/4", "pi/2", "3pi/4", "pi"))

lissajous_values <- lissajous_values %>%
  mutate(w1 = as.numeric(substr(ratio, 1, 1)),
         w2 =  as.numeric(substr(ratio, 3, 3)),
         phase_num = case_when(phase == "0" ~ 0,
                               phase == "pi/4" ~ pi/4,
                               phase == "pi/2" ~ pi/2,
                               phase == "3pi/4" ~ 3*pi/4,
                               phase == "pi" ~ pi))

Let’s glimpse at the table:

lissajous_values %>% glimpse()
## Rows: 40
## Columns: 5
## $ ratio     <fct> 1/1, 1/2, 1/3, 2/3, 3/4, 3/5, 4/5, 5/6, 1/1, 1/2, 1/3, 2/3, …
## $ phase     <fct> 0, 0, 0, 0, 0, 0, 0, 0, pi/4, pi/4, pi/4, pi/4, pi/4, pi/4, …
## $ w1        <dbl> 1, 1, 1, 2, 3, 3, 4, 5, 1, 1, 1, 2, 3, 3, 4, 5, 1, 1, 1, 2, …
## $ w2        <dbl> 1, 2, 3, 3, 4, 5, 5, 6, 1, 2, 3, 3, 4, 5, 5, 6, 1, 2, 3, 3, …
## $ phase_num <dbl> 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.000…

Now we need to apply the lissajous function to each row of the table, and integrate the resulting 40 data frames of 100 rows into a single data frame. We can do that using the pmap_df function of the purrr package:

lissajous_table <- pmap_df(lissajous_values, function(ratio, phase, w1, w2, phase_num) lissajous(w1 = w1, w2 = w2, diff_phase = phase_num, ratio = ratio, phase = phase))

The resulting data frame lissajous_table has 40 x 100 = 4,000 rows:

lissajous_table %>% glimpse()
## Rows: 4,000
## Columns: 4
## $ x     <dbl> 0.00000000, 0.06342392, 0.12659245, 0.18925124, 0.25114799, 0.31…
## $ y     <dbl> 0.00000000, 0.06342392, 0.12659245, 0.18925124, 0.25114799, 0.31…
## $ ratio <fct> 1/1, 1/1, 1/1, 1/1, 1/1, 1/1, 1/1, 1/1, 1/1, 1/1, 1/1, 1/1, 1/1,…
## $ phase <fct> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…

To plot the forty Lissajous curves we use a single plot:

  • x and y as aesthetics of each plot, created with geom_path() and theme_void().
  • we use ratio and phase as arguments of facet_grid() (that’s why they are added to the lissajous function). The switch = "y" argument sets the labels of frequency ratio to the left instead of to the right.
ggplot(lissajous_table, aes(x,y)) +
  geom_path() + 
  theme_void() +
  facet_grid(ratio ~ phase, switch = "y")

Examining the effect of phase with an animated GIF

To finish this gallery of Lissajous plots I have shown the effect of phase difference for a Lissajous curve. To run this on your computer you need:

  • the gganimate package to create the plots to be wrapped into the animation,
  • the transformr package to generate smooth transitions between contiguous plots of the animation
  • and the gifski package to create the GIF.

The code to build the GIF is wrapped into the lissajous_gif function:

  • The values data frame has points values of diff_phase between 0 and \(2\pi\). We will be using 100 images to build the GIF, one for each value of phase difference.
  • The l_gif function returns the points of a Lissajous curve for each diff_phase using a number of points. These values are stored in the table data frame.
  • I use the transition_states function of gganimate to generate the plots to build the GIF. The function will generate one plot for each value of diff_phase.
  • The function returns the set of plots of the anim variable.
lissajous_gif <- function(w1, w2, points = 100){
  
  values <- data.frame(w1, w2, diff_phase = seq(0, 2*pi, length.out = 100))
  
  l_gif <- function(w1, w2, diff_phase){
    t <- seq(0, 2*pi, length.out = points)
    x <- sin(w1*t + diff_phase)
    y <- sin(w2*t)
    df <- data.frame(x = x, y = y, diff_phase = diff_phase)
    return(df)
  }
  
  table <- pmap_df(values, function(w1, w2, diff_phase) l_gif(w1, w2, diff_phase))
  
  anim <- ggplot(table, aes(x, y)) +
    geom_path() +
    theme_void() +
    transition_states(diff_phase,
                      transition_length = 2,
                      state_length = 1)
  
  return(anim)
}

Let’s see an animation for a Lissajous curve of \(w_1 = 2\) and “w_2 = 3”

lissajous_gif(w1=2, w2=3, points=100)

And an animation for \(w_1 = 4\) and \(w_2 = 5\):

lissajous_gif(w1=4, w2=5, points=500)

Built with R 4.1.0, dplyr 1.0.7, gganimate 1.0.7, ggplot2 3.3.4, purrr 0.3.4, and transformr 0.1.3