Strange Attractors: an R experiment about maths, recursivity and creative coding

[This article was first published on R on Coding Club UC3M, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

by Antonio Sánchez

Learning to code can be quite hard. Apart from the difficulties of learning a new language, following a book can be quite boring. From my point of view, one of the bests ways to become a good programmer is choosing small and funny experiments oriented to train specific techniques of programming. This is what I usually do in my blog Fronkonstin. In this tutorial, we will learn to combine C++ with R to create efficient loops. We will also learn interesting things about ggplot2, a very important package for data visualization. The excuse to do all this is to create beautiful images using a mathematical concept called Strange Attractors, that we will discuss briefly also. Combining art and coding is often called generative art, so be ready because after following this tutorial you can discover a new use of R to fall for as I am. Here we go!

Introduction

Iteration is an indispensable technique in every programming language. Mastering it will help you to produce robust, fast, concise and easy to understand code. Iterating is repeating the same task over and over and the first way to do that efficiently is writing a loop, which is a way to resume many lines of code in just a few of them. In R, you can do this using the for function. For example, this loop calculates the mean of each column of a data frame called df:

library(tidyverse)

n <- 1000 # Rows of data frame

# df with 4 columns and n rows sampled from N(0,1)
df <- tibble(
  a = rnorm(n),
  b = rnorm(n),
  c = rnorm(n),
  d = rnorm(n)
)

output <- vector("double", ncol(df)) # Empty output
# For loop
for (i in 1:ncol(df)) {
  output[[i]] <- mean(df[[i]])
}
output
## [1] -0.0069680221  0.0247380568  0.0014428622 -0.0002675645

In the previous example, we repeated 4 times the task (which is the piece of code inside the curly brackets). Another way to do iterations in R, sometimes more efficient than a for loop, is by using the apply family. This is how the previous loop can be rewritten using the apply function:

output <- apply(df, 2, mean)
output

These approaches have limitations. For example, if you want to use the result of an iteration as the input of the next one, you can not use apply. Another one is that sometimes R is not enough fast, which makes the for function quite inefficient. When you face this kind of situations, a good option is combinnig C++ and R. To illustrate this, let’s briefly introduce a stunning mathematical creature: the strange attractors.

What are strange attractors?

In mathematics, a dynamical system is a set of functions which describes the time dependence of a point in a geometrical space. An attractor is a set of numerical values towards a dynamical system tend to evolve in discrete time regardless of their starting conditions. Simple attractors can be fixed points, sets of points or limit cycles. More interesting attractors are strange, chaotic or itinerant attractors, which span an array of possible states in which a dynamical system can roam around without repeating itself.

One of the best-known examples of strange attractors was discovered by Kiyohiro Ikeda 1 and bears his name. It is defined by the next equations:

\[ x_{n+1}=1+u(x_{n}\cos t_{n}-y_{n}\sin t_{n}) \\ y_{n+1}=u(x_{n}\sin t_{n}+y_{n}\cos t_{n}) \]

with \(u\ge0.6\) and

\[ t_{n}=0.4-{\frac {6}{1+x_{n}^{2}+y_{n}^{2}}} \]

In this example, our imaginary point moves in a 2D dimensional space, since we just need (x, y) coordinates to describe its movement. In the previous system, time is represented by n. As you can see, the Ikeda’s system only depends on one parameter, called u. The location of the point at n depends only on its location at n-1. For example, this image 2 shows the trajectories of 2000 random points following this previous equation with \(u = 0.918\):

All points, regardless of their starting place, tend to end following the same path, represented in bright white color. Another interesting property of strange attractors is that points that are very close at some instant, can differ significantly in the next step. This is why strange attractors are also known as chaotic attractors.

Today we will experiment with Clifford attractors, which are defined by these equations:

\[ x_{n+1} = sin(a \: y_{n}) + c \: cos(a \: x_{n}) \\ y_{n+1} = sin(b \: x_{n}) + d \: cos(b \: y_{n}) \]

These attractors were discovered by Clifford Pickover, an american scientific divulgator and depend on four parameters; a, b, c and d.

Populating attractors: an inefficient approach

To represent the trajectory of a particular point, we need to store in a data frame its locations at every single (discrete) moment of time. For example, this is how we can do it using a for loop:

# Parameters setting
a <- -1.25
b <- -1.25
c <- -1.82
d <- -1.91

# Number of jumps
n <- 5

# Initialization of df to store locations of point
df <- data.frame(x = double(n+1), y = double(n+1))

# The point starts at (0,0)
df[1, "x"] <- 0
df[1, "y"] <- 0

# For loop to store locations
for (i in 1:n) {
  df[i+1, "x"] <- sin(a*df[i, "y"])+c*cos(a*df[i, "x"])
  df[i+1, "y"] <- sin(b*df[i, "x"])+d*cos(b*df[i, "y"])
}

df
##            x          y
## 1  0.0000000  0.0000000
## 2 -1.8200000 -1.9100000
## 3  1.8629450  2.1543131
## 4  0.8172482  0.9946399
## 5 -1.8969558 -1.4673201
## 6  2.2716156  1.1936322

As you can see, our point started at (0,0) and jumped 5 times (the number of jumps is represented by n). You can try to change n from 5 to 10000000 and see what happens (disclaimer: be patient).

A better approach to do it is using purrr, a functional programming package which provides useful tools for iterating through lists and vectors, generalizing code and removing programming redundancies. The purrr tools work in combination with functions, lists and vectors and results in code that is consistent and concise. This can be a purrr approach to our problem:

library(purrrlyr)

n <- 5

# Initialization of our data frame
df <- tibble(x = numeric(n+1),
             y = numeric(n+1))

# Convert our data frame into a list
by_row(df, function(v) list(v)[[1L]], .collate = "list")$.out -> df

# This function computes current location depending of previous one
f <- function(j, k, a, b, c, d) {
  tibble(
    x = sin(a*j$y)+c*cos(a*j$x),
    y = sin(b*j$x)+d*cos(b*j$y)
  )
}

# We apply accumulate on our list to store all steps and convert into a data frame
accumulate(df, f, a, b, c, d) %>% map_df(~ .x) -> df

df
## # A tibble: 6 x 2
##        x      y
##    <dbl>  <dbl>
## 1  0      0    
## 2 -1.82  -1.91 
## 3  1.86   2.15 
## 4  0.817  0.995
## 5 -1.90  -1.47 
## 6  2.27   1.19

Even this approach is not efficient since the accumulate function must be applied on an existing object (the df in our case)

Combining C++ and R: the good way

The next paragraph, extracted from Hadley Wickham’s Advanced R, explains the motivations of combining C++ and R:

Rcpp makes it very simple to connect C++ to R and provides a clean, approachable API that lets you write high-performance code, insulated from R’s complex C API. Typical bottlenecks that C++ can address include:

  • Loops that can’t be easily vectorised because subsequent iterations depend on previous ones.

  • Recursive functions, or problems which involve calling functions millions of times. The overhead of calling a function in C++ is much lower than in R.

  • Problems that require advanced data structures and algorithms that R doesn’t provide. Through the standard template library (STL), C++ has efficient implementations of many important data structures, from ordered maps to double-ended queues.

This code populates our data frame using C++:

# Load in libaries
library(Rcpp)
library(tidyverse)

# Here comes the C++ code
cppFunction('DataFrame createTrajectory(int n, double x0, double y0,
            double a, double b, double c, double d) {
            // create the columns
            NumericVector x(n);
            NumericVector y(n);
            x[0]=x0;
            y[0]=y0;
            for(int i = 1; i < n; ++i) {
            x[i] = sin(a*y[i-1])+c*cos(a*x[i-1]);
            y[i] = sin(b*x[i-1])+d*cos(b*y[i-1]);
            }
            // return a new data frame
            return DataFrame::create(_["x"]= x, _["y"]= y);
            }
            ')

a <- -1.25
b <- -1.25
c <- -1.82
d <- -1.91

# Call the function starting at (0,0) to store 10M jumps
df <- createTrajectory(10000000, 0, 0, a, b, c, d)

head(df)
##            x          y
## 1  0.0000000  0.0000000
## 2 -1.8200000 -1.9100000
## 3  1.8629450  2.1543131
## 4  0.8172482  0.9946399
## 5 -1.8969558 -1.4673201
## 6  2.2716156  1.1936322

Note that, apart from Rcpp, we also loaded the tidyverse package. This is because we will use some of its features, as dplyr or ggplot2, later.

The function createTrajectory is written in C++ and stores the itinerary of an initial point. You can try it to see that It’s extremely fast. In the previous example, I populated on the fly our data frame with 10 million points starting at (0,0) in less than 5 seconds in a conventional laptop.

Rendering the plot with ggplot

Once we have built our data frame, it’s time to represent it with ggplot.

Making graphics is a necessary skill and for this ggplot is an amazing tool, which by the way, I encourage you to learn. Usually, a good visualization is the best way to show your results. R is an extraordinary tool for visualizing data and one of the most important libraries to do it is ggplot2: it is powerful, flexible and is updated to include functionalities regularly. Although its syntax may seem a bit strange at first, here are four important concepts from my point of view to help you out with understanding it:

  • It’s truly important to have clear the type of graph you want to plot: for each one there’s a different geometry: geom_point to represent points, geom_line to represent time series, geom_bar to do bar chars … Have a look to ggplot2 package, there are many other types.
  • Each of those geometries needs to define what data is to be used and how. For example, as you will see in our graph, we placed on the x-axis the x column of our data frame df and y on the y-axis, so we did aes(x = x, y = y).
  • You can also combine geometries. We didn’t do that for our example but you may want, for example, to combine bars and lines on the same plot. To do it you can just use + operator. This operator is very important: it doesn’t only combine geometries, it’s also used to add features to the plot as we did to define its aspect ratio (with coord_equal) or its appearance (with theme_void).
  • You can modify every detail of the look and feel of your graph but be patient. To begin with, I recommend the use of predefined themes as I did with theme_void. There are many of them and it’s is a quick and easy way to change the appearance of your plot: just a way of making life easier and simpler.

The ggplot2 package is a new world by itself and takes part of the tidyverse, a collection of efficient libraries designed to analyze data in a consistent way.

This is how we can represent the points of our data frame:

ggplot(df, aes(x = x, y = y)) +
  geom_point(shape = 46, alpha = .01) +
  coord_equal() +
  theme_void() -> plot

ggsave("strange1.png", plot, height = 5, width = 5, units = 'in')

The syntax of our plot is quite simple. Just two comments about it:

  • Shape is set to 46, the smallest filled point. More info about shapes, here
  • Since there are a lot of points, I make them transparent with alpha to take advantage on its overlapping and reveal patterns of shadows.

Playing with parameters

Strange attractors are extremely sensitive to their parameters. This feature makes them very entertaining as well as an infinite source of surprises. This is what happens to Clifford attractor when setting a, b, c and d parameters to -1.2, -1.9, 1.8 and -1.6 respectively:

a <- -1.2
b <- -1.9
c <- 1.8
d <- -1.6

df <- createTrajectory(10000000, 0, 0, a, b, c, d)

ggplot(df, aes(x = x, y = y)) +
  geom_point(shape = 46, alpha = .01) +
  coord_equal() +
  theme_void() -> plot

ggsave("strange2.png", plot, height = 5, width = 5, units = 'in')

Don’t you feel like playing with parameters? From my point of view, this is a very classy way of spending your time.

The General 2D-Map

Once we know what strange attractors look like, let’s try another family of them. This is the new formula we will experiment with:

\[ x_{n+1} = a_{1} + a_{2}x_{n} + a_{3}y_{n} + a_{4}|x_{n}|^{a_{5}} + a_{6}|y_{n}|^{a_{7}} \\ y_{n+1} = a_{8} + a_{9}x_{n} + a_{10}y_{n} + a_{11}|x_{n}|^{a_{12}} + a_{13}|y_{n}|^{a_{14}} \]

These attractors are localized mostly to a small region of the XY plane with tentacles that stretch out to large distances. If any of the exponents are negative and the attractor intersects the line along which the respective variable is zero, a point on the line maps to infinity. However, large values are visited infrequently by the orbit, so many iterations are required to determine that the orbit is unbounded. For this reason most of the attractors in the figures have holes in their interiors where their orbits are precluded from coming too close to their origins.

cppFunction('DataFrame createTrajectory(int n, double x0, double y0,
            double a1, double a2, double a3, double a4, double a5,
            double a6, double a7, double a8, double a9, double a10,
            double a11, double a12, double a13, double a14) {
            // create the columns
            NumericVector x(n);
            NumericVector y(n);
            x[0]=x0;
            y[0]=y0;
            for(int i = 1; i < n; ++i) {
            x[i] = a1+a2*x[i-1]+ a3*y[i-1]+ a4*pow(fabs(x[i-1]), a5)+ a6*pow(fabs(y[i-1]), a7);
            y[i] = a8+a9*x[i-1]+ a10*y[i-1]+ a11*pow(fabs(x[i-1]), a12)+ a13*pow(fabs(y[i-1]), a14);
            }
            // return a new data frame
            return DataFrame::create(_["x"]= x, _["y"]= y);
            }
            ')
a1 <- -0.8
a2 <- 0.4
a3 <- -1.1
a4 <- 0.5
a5 <- -0.6
a6 <- -0.1
a7 <- -0.5
a8 <- 0.8
a9 <- 1.0
a10 <- -0.3
a11 <- -0.6
a12 <- -0.3
a13 <- -1.2
a14 <- -0.3

df <- createTrajectory(10000000, 1, 1, a1, a2, a3, a4, a5, a6,
                       a7, a8, a9, a10, a11, a12, a13, a14)

ggplot(df) +
  geom_point(aes(x = x, y = y), shape = 46, alpha = .01, color = "black") +
  coord_fixed() +
  theme_void() -> plot

ggsave("strange3.png", plot, height = 5, width = 5, units = 'in')

Ops! What happened? Why our image is so small now? The reason is because some few points are located quite far from the vast majority (you can think about them as outliers). Let’s remove 5% of extreme points from both tails independently:

mx <- quantile(df$x, probs = 0.05)
Mx <- quantile(df$x, probs = 0.95)
my <- quantile(df$y, probs = 0.05)
My <- quantile(df$y, probs = 0.95)

df %>% filter(x > mx, x < Mx, y > my, y < My) -> df

ggplot(df) +
  geom_point(aes(x = x, y = y), shape = 46, alpha = .01, color = "black") +
  coord_fixed() +
  theme_void() -> plot

ggsave("strange3.png", plot, height = 5, width = 5, units = 'in')

Now a stunning image appears.

Sometimes you will need to process the data frame after you generate it since:

  • Extreme values can hide the pattern as happened before or even scape to infinite (solution: remove outliers).
  • Some images can be quite boring: from my point of view, the more distributed ae points on the XY plane, the nicer is the resulting image (solution: try with a small data frame and keep it if some measure of dispersion over the plane or the individual axis is high enough; in this case, compute the big picture).

To conclude, let’s change the appearance of our plot. We will use the theme function to customize it setting the background color to black and adding a white line as frame.

opt <-  theme(legend.position  = "none",
              panel.background = element_rect(fill="black", color="white"),
              plot.background  = element_rect(fill="black"),
              axis.ticks       = element_blank(),
              panel.grid       = element_blank(),
              axis.title       = element_blank(),
              axis.text        = element_blank())

plot <- ggplot(df) +
  geom_point(aes(x = x, y = y), shape = 46, alpha = .01, color = "white") +
  coord_fixed() +
  opt

ggsave("strange4.png", plot, height = 5, width = 5, units = 'in')

Summary

Further reading

More about recursivity and ggplot:

  • Hadley’s R for Data Science and Advanced R: the first one covers iteration methods (including functional ones) as well as ggplot and the second one introduces Rcpp to use C++ when R is not fast enough.

More about strange attractors:


  1. K.Ikeda, Multiple-valued Stationary State and its Instability of the Transmitted Light by a Ring Cavity System, Opt. Commun. 30 257-261 (1979); K. Ikeda, H. Daido and O. Akimoto, Optical Turbulence: Chaotic Behavior of Transmitted Light from a Ring Cavity, Phys. Rev. Lett. 45, 709–712 (1980)

  2. Accelerometer CC BY-SA 3.0

To leave a comment for the author, please follow the link and comment on their blog: R on Coding Club UC3M.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)