4 min read

Fixed points

In mathematics, a fixed point of a function is an element that gets mapped to itself by that function. For example, the function

\[ f : \mathbb{R} \rightarrow \mathbb{R} \] \[ f(x) = x^2 \]

maps the elements 0 and 1 to themselves, since \(f(0) = 0^2 = 0\) and \(f(1) = 1^2 = 1\).

To illustrate the concept, we could define a function fixed_points which maps functions to the set of their fixed points. We start, however, by defining a function approx_eq, which takes two vectors as input, does a pairwise check of equality within a given tolerance, and returns a boolean vector.

library(tidyverse)
library(gganimate)
approx_eq <- function(x, y, tol = 1e-2) {
  map2_lgl(x, y, ~isTRUE(all.equal(.x, .y, tolerance = tol)))
}

This roundabout solution is necessary in order to deal with the curious non-type-stable nature of all.equal which returns either TRUE or a character string explaining why the two elements are not equal!

With that in place, we now define fixed_points, which takes as input a function f and a domain x over which to evaluate f. It then returns all unique elements x that satisfy approx_eq(x, f(x)).

fixed_points <- function(f, x, ..., tol = 1e-2) {
    f_x <- f(x, ...)
    equal <- approx_eq(x, f_x, tol = tol)
    unique(x[equal])
}

f <- function(x) x^3

(fp <- fixed_points(f, -10:10))
## [1] -1  0  1
fp == f(fp)
## [1] TRUE TRUE TRUE

So we see that the function \(f(x) = x^3\) has the fixed points \({-1, 0, 1}\) over the interval \(-10 \leq x \leq 10\).

For convenience, and to see what’s really going on with the fixed points for various functions, we can define a function that plots a function and its fixed points (ggplot2 provides the convenient function stat_function for plotting arbitrary functions).

plot_fixed_points <- function(f, domain, ...) {
  fp <- fixed_points(f, domain, ...)
  
  ggplot(data.frame(x = domain), aes(x)) +
    geom_hline(yintercept = 0, size = 0.1) +
    geom_vline(xintercept = 0, size = 0.1) +
    stat_function(fun = f, color = "blue") +
    stat_function(fun = function(x) x, color = "red") +
    annotate("point", x = fp, y = fp) +
    annotate("text",  x = fp, y = fp, 
             label = sprintf("(%0.1f,%0.1f)", fp, fp),
             size = 3, hjust = -0.1, vjust = 2,
             check_overlap = TRUE) +
    coord_equal(ylim = range(domain)) +
    theme_minimal()
}

Let’s try it out on some common functions:

domain <- -10:10
plot_fixed_points(function(x) x^2, domain)

plot_fixed_points(function(x) x^3, domain)

id <- function(x) x
plot_fixed_points(id, domain)

const_3 <- function(x) 3
plot_fixed_points(const_3, domain)

plot_fixed_points(abs, domain)

plot_fixed_points(sign, domain)

g <- function(x) x^4 + 3 * x^3 + x^2
domain2 <- seq(-4, 2, length.out = 1000)
plot_fixed_points(g, domain2)

plot_fixed_points(sin, domain)

plot_fixed_points(cos, domain2)

plot_fixed_points(function(x) x * (1 + sin(x)), 
                  seq(0, 20, 0.01))

The key point to notice is that the fixed points are precisely those points where the graph of the function intersects the graph of the identity function (i.e. the 45° line).

Nothing stops us from applying fixed_points to non-numeric arguments. For example, we can confirm that the fixed points of toupper evaluated on all upper- and lower-case letters are exactly all the upper-case letters.

fixed_points(toupper, c(letters, LETTERS))
##  [1] "A" "B" "C" "D" "E" "F" "G" "H" "I" "J" "K" "L" "M" "N" "O" "P" "Q"
## [18] "R" "S" "T" "U" "V" "W" "X" "Y" "Z"

Attractive fixed points

A related concept is that of attractive fixed points. As discussed in the Wikipedia article, if we punch in any number into a calculator and then repeatedly evaluate the cosine of that number, we will eventually get approximately 0.739085133.

afp <- cos(cos(cos(cos(cos(cos(cos(cos(cos(cos(cos(cos(cos(-1)))))))))))))
afp
## [1] 0.7375069

We can illustrate this process with a nice animated graph.

xs <- accumulate(1:10, ~cos(.x), .init = -1) %>% 
  list(., .) %>% 
  transpose() %>% 
  flatten() %>% 
  flatten_dbl()

df <- data_frame(
  x     = head(xs, -1), 
  y     = c(0, tail(xs, -2)),
  frame = seq_along(x)
)

p <- plot_fixed_points(cos, domain) +
  coord_equal(ylim = c(-1, 1), xlim = c(-1, 1)) +
  geom_path(data = df, 
            aes(x, y, frame = frame, cumulative = TRUE),
            color = "orange")