Listening to Chaos

July 07, 2023

I spent the past four months based in Montréal for a semester program in Geometric Group Theory. During that time, Abdul Zalloum, Mariam Al-Hawaj and Giulio Tiozzo organized a special session at the annual meeting of the Canadian Mathematical Society and invited me to give a talk. The purpose of this post is to recap a couple of cool things I learned at the special session and to share an application of that stuff, a "sonification," if you will, as code for monome crow, a Eurorack synth module that runs Lua code.

Table of Contents

1. The code

I'll lead with the crow code, since it's maybe more accessible to readers of this post coming from the orbit of lines. As I mentioned above, this code is designed to be run on monome's crow, and you can copy it into a file and upload it via druid. I recommend the following setup: set public.min[1] = 0, public.max[1] = 2 and output[1].scale({0,2,3,5,7,8,11}). Patch a clock source to crow input 1, an attenuated (bipolar) random voltage source to input 2, patch output 1 to the pitch CV of an oscillator, and patch outputs 2 through 4 (maybe slewed and/or attenuated) to timbral controls.

public { min = { -5, -5, -5, -5 } }:range(-5, 10)
public { max = { 5, 5, 5, 5 } }:range(-5, 10)
public { divisions = { 1 / 8, 1 / 8, 1 / 8, 1 / 8 } }
public { vals = { 0.48, 0.49, 0.51, 0.52 } }:range(0, 1)
param = 0
lambda = 0.25 * (1 + math.sqrt(13) + math.sqrt(2 * (math.sqrt(13) - 1)))
crit = 1 / lambda
function transform(x)
if <= crit then return 1 - lambda * x else return lambda * x - 1 end

function do_tick(i)
while true do
  public.vals[i] = transform(public.vals[i] ^ math.exp(-param)) ^ math.exp(param)
  output[i].volts = public.min[i] + public.vals[i] * (public.max[i] - public.min[i])

function init()
input[1].mode = 'clock'
input[2] { mode = 'stream', time = 0.2, stream = function(x) param = x end }
for i = 1, 4 do, i) end

1.1. What the code does

The code applies a particular chaotic dynamical system, a post-critically finite interval map, to the (internal state of) the four output CVs. In smaller words, the module stores four pieces of data, public.vals, which are numbers between 0 and 1. Every tick of the \(i\)th clock (which is set with the public.divisions values), we update the \(i\)th value by applying the following transformation:

\[f(x) = \begin{cases} 1 - \lambda x & 0 \le x \le \frac{1}{\lambda} \\ \lambda x - 1 & \frac{1}{\lambda} \le x \le 1, \end{cases}\]

where \(\lambda\) is the largest (real) root of the polynomial \(x^4 - x^3 - x^2 - x + 1\), and is about \(1.7\). The module then uses the rest of the data to transform the internal state of public.vals to the CV you can use to control your patch.

CV to input 2 applies a topological conjugacy to the map, specifically it conjugates by the map that raises everything to the power of \(e^p\), where \(-5 \le p \le 10\) is the CV input to input 2. Thus negative CV will tend to concentrate things towards the "top" of the range, while positive CV will push things towards the "bottom".

2. The math

2.1. Sharkovsky's theorem

One very cool theorem I learned about (possibly for the second time, but I don't really know) is Sharkovsky's Theorem, which in one formulation says

Theorem (Sharkovsky). If \(f\colon I \to I\) is a continuous map of a closed interval with a periodic point of period \(3\), then \(f\) has periodic points of every period. More generally, there is an ordering \(\prec\) on the natural numbers such that if \(f\) has a periodic point of period \(m\) and \(m \prec n\), then \(f\) has a periodic point of period \(n\).

The order goes as follows: all the odd natural numbers greater than \(1\) in increasing order, followed by \(2\) times all the odd natural numbers greater than \(1\) in increasing order, then \(4\) times, \(8\) times, and so on. Then the powers of \(2\) in decreasing order, ending with \(4\), then \(2\), then \(1\).

2.2. The map in my code

has one critical point at \(x = \frac{1}{\lambda} = 1 + \lambda + \lambda^2 - \lambda^3\). This critical point turns out to have period \(5\), as the following sketch attempts to illustrate.

the graph of the interval map $f$

As you can see from the definition, the forward image of the critical point is \(0\), then \(1\), then \(\lambda - 1\), then (the sketch lies) \(\lambda^2 - \lambda - 1\), then \(1 + \lambda + \lambda^2 - \lambda^3 = \frac{1}{\lambda}\). Sharkovsky's theorem says therefore that the map \(f\) has a periodic point of every period except possibly period \(3\). I do not know whether this map has a point of period three. I suspect it does.

2.3. Pseudo-Anosov maps

Another cool thing I learned about at this conference is forthcoming work of Ethan Farber and Karl Winsor given a train-track-y proof of a theorem of Boissy and Laneau, which says that within a certain hyperelliptic stratum of the moduli space of quadratic differentials on the Teichmüller space of a surface of genus \(g\), the shortest periodic flow line under the Teichüller geodesic flow has length at least \(\frac{\log 2}{2}\), independent of \(g\). This stands in sharp contrast to Penner's result that \(\log\) of the least dilatation of elements of the mapping class group tends to zero as \(g\) increases.

Farber and Winsor's proof uses classical results in one-dimensional dynamics that say that interval maps like the one I describe above satisfy a uniform lower bound on their topological entropy of \(\frac{\log 2}{2}\). It follows that if a pseudo-Anosov map of a surface has the same topological entropy as such an interval map, its entropy is also bounded below by \(\frac{\log 2}{2}\). By using tight splittings, which Farber introduced in an earlier (very very cool) paper with coauthors, they show that in the situation of Boissy and Laneau's theorem, every pseudo-Anosov map (or really, the pseudo-Anosov braid to which it is closely related) may be realized as a train track map on a train track whose real edges (where the action happens) form a graph homeomorphic to an interval. Isn't that so cool?