# One Dimensional Maps

Creating one-dimensional maps is a very easy and straightforward process that can be used to explore chaotic behavior.

Given some function $$f(x)$$ we take an initial value $$x\_0$$ and use the iterative process

$x\_{n+1} = f\left(x\_n\right)$

One popular map to explore is the Logistic Map, defined as

$x\_{n+1} = \mu x\_n (1 - x\_n)$

# Cobweb Diagrams

We can construct Cobweb Diagrams using our logistic map and the diagonal $$y=x$$. These diagrams are made by “bouncing” around between our map and the diagonal to construct “cobwebs”.

The logistic map expresses chaotic behavior for certain values of $$\mu$$. We can examine “orbits” of this system by looking at what values the map bounces around to. A cobweb diagram is a good way to see these “orbits”.

We can find the parameter values at which the stable period $$2^1$$, $$2^2$$, and $$2^3$$ orbits are first created and label these $$\mu\_1$$, $$\mu\_2$$, $$\mu\_3$$. We’ll use Cython for this process as we need to quickly evaluate a large amount of iterations.

%%cython -a -c=-O3
import numpy as np
cimport numpy as np

cimport cython

@cython.boundscheck(False) # turn off bounds-checking
@cython.wraparound(False)  # turn off negative index wrapping
def cobweb(f, int n=100, int start=0, float initial=0.5):
""" Generate the path for a cobweb diagram """
cdef np.ndarray[np.float64_t, ndim=2] web = np.zeros((n, 2),
dtype=np.float64)
web[0, 0] = initial
web[0, 1] = initial
cdef int state = 1
cdef np.ndarray[np.int64_t, ndim=1] vals = np.arange(1, n)
for i in vals:
if state:
web[i, 0] = web[i - 1, 0]
web[i, 1] = f(web[i - 1, 0])
else:
web[i, 0] = web[i - 1, 1]
web[i, 1] = web[i - 1, 1]
state ^= 1
return web[start:]


Now we can use this function to find our cobwebs and plot.

x = np.linspace(0, 1, 100)
button = ipywidgets.Button(description='Save as File')
@ipywidgets.interact(mu=(1, 4, 0.01))
def plot(mu=3):
f = lambda x: mu * x * (1 - x)
web = cobweb(f, n=1000)
fig = plt.figure(figsize=(8, 8))
ax = fig.add_axes([0.1, 0.1, 0.8, 0.8])
ax.plot(x, x)
ax.plot(x, f(x))
ax.plot(web[:, 0], web[:, 1], linewidth=0.5)
plt.show()
display(button)
button.on_click(lambda b: fig.savefig(f'logistic_cobweb.png'))


And here they are.

x = np.linspace(0, 1, 100)
web1 = cobweb(lambda x: 3.069946 * x * (1 - x), n=1000, start=100)
web2 = cobweb(lambda x: 3.449945 * x * (1 - x), n=1000, start=500)
web3 = cobweb(lambda x: 3.549946 * x * (1 - x), n=1000, start=500)

plt.figure(figsize=(6, 6))
plt.plot(x, x)
plt.plot(x, 3.069946 * x * (1 - x), 'b-')
plt.plot(x, 3.449945 * x * (1 - x), 'g-')
plt.plot(x, 3.549946 * x * (1 - x), 'r-')
plt.plot(web1[:, 0], web1[:, 1], 'b-', linewidth=0.5, label=r'$\mu_1$')
plt.plot(web2[:, 0], web2[:, 1], 'g-', linewidth=0.5, label=r'$\mu_2$')
plt.plot(web3[:, 0], web3[:, 1], 'r-', linewidth=0.5, label=r'$\mu_3$')
plt.legend(loc=0)
plt.savefig('logistic_orbits.png')


# Bifurcation Diagrams

We’ve already noted that the accuracy of finding these bifurcation points was low, let’s instead examine a bifurcation diagram. A bifurcation diagram is essentially a probabilistic view of our map for different values of $$\mu$$. For the following plots, the $$x$$-axis is differing values of $$\mu$$, and the $$y$$-axis is a large number of plotted values after the transient.

%%cython -a -c=-O3

import numpy as np
cimport numpy as np
cimport cython

@cython.boundscheck(False) # turn off bounds-checking
@cython.wraparound(False)  # turn off negative index wrapping
def bifurcation(np.int64_t precision=1000,
np.int64_t keep=500,
np.int64_t num_compute=10000,
np.float64_t xmin=0,
np.float64_t xmax=4,
np.float64_t ymin=0,
np.float64_t ymax=1):
""" Acquire bifurcation points for varying mu for logistic map """
cdef np.ndarray[np.float64_t, ndim=1] mu = np.linspace(xmin, xmax,
precision, dtype=np.float64)
cdef np.float64_t x = 0.5  # unimportant initial x val
cdef np.int64_t i, j, k
cdef np.ndarray[np.float64_t, ndim=2] points = np.zeros((len(mu) * keep, 2),
dtype=np.float64)
k = 0
for i in np.arange(len(mu), dtype=np.int64):
for j in range(num_compute):
x = mu[i] * x * (1 - x)
if j > (num_compute - keep): # we throw away the transient
points[k, 0] = mu[i]
points[k, 1] = x
k += 1
return points


The full bifurcation diagram for the logistic map follows.

points = bifurcation(xmin=1, xmax=4)
plt.figure(figsize=(12, 4))
plt.plot(points[:, 0], points[:, 1], ',', color='k', alpha=0.8)
plt.xlim(1, 4)
plt.savefig('logistic_bifurcation.png')


Now that we can plot the bifurcation diagram, let’s examine the first several bifurcations.

mu_vals = np.array([3,
3.45,
3.544,
3.5645,
3.56875,
3.5697,
3.5698925,
3.569934,
3.56994316,
3.5699452,
3.569945646])

def plot_bifurcation(fig, axarr, index, x, y, xmin, xmax,
ymin, ymax, precision, keep, num):
points = bifurcation(precision=precision, xmin=xmin,
xmax=xmax, keep=keep, num_compute=num)
axarr[x, y].plot(points[:, 0], points[:, 1], ',', color='k', alpha=0.8)
axarr[x, y].set_xlim(xmin, xmax)
axarr[x, y].set_ylim(ymin, ymax)
axarr[x, y].set_title(r'${1} < \mu_{0} < {2}$, $2^{0}$ cycle'.format(
index, xmin, xmax))
axarr[x, y].set_yticks([])
for i, mu in enumerate(mu_vals):
axarr[x, y].plot(np.ones(10) * mu,
np.linspace(0, 1, 10), 'r-', alpha=0.25)
axarr[x, y].annotate(i + 1, xy=(mu, ymax-(0.05 * (ymax - ymin))),
color='red')

fig, axarr = plt.subplots(3, 3, figsize=(12, 12))
plot_bifurcation(fig, axarr, 1, 0, 0, 2.9, 3.5, 0.4, 1, 500, 100, 1000)
plot_bifurcation(fig, axarr, 2, 0, 1, 3.4, 3.6, 0.8, 0.9, 500, 500, 5000)
plot_bifurcation(fig, axarr, 2, 0, 2, 3.53, 3.6, 0.8, 0.9, 500, 500, 5000)
plot_bifurcation(fig, axarr, 3, 1, 0, 3.56, 3.58, 0.888, 0.896, 500, 500, 5000)
plot_bifurcation(fig, axarr, 4, 1, 1, 3.568, 3.575, 0.889, 0.894, 1000, 500, 5000)
plot_bifurcation(fig, axarr, 5, 1, 2, 3.5695, 3.571, 0.890, 0.892, 1200, 500, 5000)
plot_bifurcation(fig, axarr, 6, 2, 0, 3.5698, 3.57, 0.8903, 0.8905, 1500, 500, 10000)
plot_bifurcation(fig, axarr, 7, 2, 1, 3.56992, 3.56997, 0.8903, 0.89045, 1500, 2000, 10_000)
plot_bifurcation(fig, axarr, 8, 2, 2, 3.56994, 3.56995, 0.89041, 0.89043, 1_000, 50_000, 100_000)
plt.tight_layout()
plt.savefig('logistic_bifurcation_points.png')


So what are these values that we’ve found? Let’s record them.

points = bifurcation(xmin=1, xmax=4, precision=3000,
num_compute=20000, keep=100)
plt.figure(figsize=(12, 4))
plt.plot(points[:, 0], points[:, 1], ',',
color='k', alpha=0.8)
for mu in mu_vals:
plt.plot(np.ones(10) * mu, np.linspace(0, 1, 10),
'r-', alpha=0.25)
plt.xlim(2.9, 4)
plt.savefig('logistic_bifurcation_fullpoints.png')


We can plot our cobweb diagrams with these more accurate values.

x = np.linspace(0, 1, 100)
fig, axarr = plt.subplots(3, 3, figsize=(12, 12))
for i in range(9):
axarr[int(i / 3), i % 3].plot(x, x)
mu = mu_vals[i]
f = lambda x: mu * x * (1 - x)
axarr[int(i / 3), i % 3].plot(x, f(x), alpha=1)
web = cobweb(f, n=1000, start=800)
axarr[int(i / 3), i % 3].plot(web[:, 0],
web[:, 1],
linewidth=0.5)
axarr[int(i / 3), i % 3].set_title(r'$\mu = {}$'.format(mu))
plt.savefig('logistic_bifurcation_cobwebs.png')


We could keep recording these values if we wanted, as this will keep going infinitely, and appears to be approaching $$\mu\_\infty \approx 3.57$$.

# Period 3 and 5 Orbits

We found all powers of $$2^n$$, but we haven’t found any odd-numbered orbits. Let’s track these down.

%%cython -a -c=-O3

import numpy as np
cimport numpy as np
cimport cython
from libc.math cimport exp

cdef float map_func(float mu, float x):
return mu * x * (1 - x)

@cython.boundscheck(False) # turn off bounds-checking
@cython.wraparound(False)  # turn off negative index wrapping
def bifurcation(np.int64_t precision=1000, np.int64_t keep=500, np.int64_t num_compute=10000,
np.float64_t xmin=0, np.float64_t xmax=4, np.float64_t ymin=0, np.float64_t ymax=1):
""" Acquire bifurcation points for varying mu for map """
cdef np.ndarray[np.float64_t, ndim=1] mu = np.linspace(xmin, xmax, precision, dtype=np.float64)
cdef np.float64_t x = 0.5
cdef np.int64_t i, j, k
cdef np.ndarray[np.float64_t, ndim=2] points = np.zeros((len(mu) * keep, 2), dtype=np.float64)
k = 0
for i in range(len(mu)):
for j in range(num_compute):
x = map_func(mu[i], x)
if j > (num_compute - keep): # we throw away the transient
points[k, 0] = mu[i]
points[k, 1] = x
k += 1
return points


Now we can perform the same process to find the odd-numbered orbits. I’m not doing 9 different levels of this, because they’re hard to find, and it’s tedious work.

mu_vals = np.array([3.84, 3.74, 3.702, 3.68725])

def plot_bifurcation(fig, axarr, index, x, y, xmin, xmax, ymin, ymax, precision, keep, num):
points = bifurcation(precision=precision, xmin=xmin, xmax=xmax, keep=keep, num_compute=num)
axarr[x, y].plot(points[:, 0], points[:, 1], ',', color='k', alpha=0.8)
axarr[x, y].set_xlim(xmin, xmax)
axarr[x, y].set_ylim(ymin, ymax)
axarr[x, y].set_title(r'${1} < \mu_{0} < {2}$, ${3}$ cycle'.format(index, xmin, xmax, 2 * index + 1))
axarr[x, y].set_yticks([])
for i, mu in enumerate(mu_vals):
axarr[x, y].plot(np.ones(10) * mu, np.linspace(0, 1, 10), 'r-', alpha=0.25)

fig, axarr = plt.subplots(2, 2, figsize=(8, 8))
plot_bifurcation(fig, axarr, 1, 0, 0, 3.8, 3.9, 0, 1, 500, 200, 2000)
plot_bifurcation(fig, axarr, 2, 0, 1, 3.735, 3.75, 0.1, 1, 500, 500, 5000)
plot_bifurcation(fig, axarr, 2, 1, 0, 3.7, 3.705, 0.2, 1, 500, 500, 5000)
plot_bifurcation(fig, axarr, 3, 1, 1, 3.687, 3.6875, 0.2, 1, 500, 300, 5000)
plt.tight_layout()
plt.savefig('logistic_bifurcations_odd.png')


We can see that at each level of odd-numbered orbits, there’s an additional bifurcation series made up of $$n \cdot 2 m$$, where $$n$$ is the number of the initial bifurcation $$(3, 5, 7, 9, \ldots)$$, and $$m$$ is the next number in the series. This is especially clear for $$n=3$$. In other words, each odd numbered bifurcation follows the same pattern that the base-$$2$$ orbits do, they increase exponentially, while converging to a number, and the devolve into chaos as soon as you’re outside their fixed orbit values.

This means that for any $$n$$, there are an infinite number of corresponding bifurcations.

points = bifurcation(xmin=3.825, xmax=3.859)
plt.figure(figsize=(12, 4))
plt.plot(points[:, 0], points[:, 1], ',', color='k', alpha=0.8)
plt.plot(np.ones(10) * mu_vals[0], np.linspace(0, 1, 10), 'r-', alpha=0.25)
plt.xlim(3.825, 3.859)
plt.savefig('logistic_bifurcation_odd_zoomed.png')


To show where these values actually occur in the entire bifurcation plot, we can plot our red lines.

points = bifurcation(xmin=1, xmax=4, precision=3000, num_compute=20000, keep=100)
plt.figure(figsize=(12, 4))
plt.plot(points[:, 0], points[:, 1], ',', color='k', alpha=0.8)
for mu in mu_vals:
plt.plot(np.ones(10) * mu, np.linspace(0, 1, 10), 'r-', alpha=0.5)
plt.xlim(2.9, 4)
plt.savefig('logistic_bifurcation_odd_full.png')


It seems that these odd-numbered orbits approach the supercritical point at around around $$3.6$$.

Let’s look at the implications of these orbits on our cobweb diagram. We need to rewrite our cobweb function slightly, as it’s not precise enough.

%%cython -a -c=-O3
import numpy as np
cimport numpy as np

cdef f(np.float64_t mu, np.float64_t x, int n):
cdef int i
cdef np.float64_t x0 = x
for i in range(n):
x0 = mu * x0 * (1 - x0)
return x0

def cobweb(np.float64_t mu, int n=1, int num=100, int keep=100, np.float64_t initial = 0.5):
""" Generate the path for a cobweb diagram """
cdef np.ndarray[np.float64_t, ndim=2] web = np.zeros((keep, 2))
cdef np.float64_t x = initial
cdef np.float64_t y = initial
cdef int offset = num - keep
cdef int state = 1
if num == keep:
offset = num - keep + 1
for i in range(1, num):
if state:
y = f(mu, x, n)
else:
x = y
state ^= 1
if i >= offset:
web[i - offset, 0] = x
web[i - offset, 1] = y
return web


Here’s an interactive version to play with.

x = np.linspace(0, 1, 5000)

def f(x, mu, n):
x1 = x
for i in range(n):
x1 = mu * x1 * (1 - x1)
return x1

button = ipywidgets.Button(description='Save as File')
@ipywidgets.interact(mu=(2.5, 4, 0.01), n=(1, 5, 1))
def plot(mu=3.74, n=3):
fig = plt.figure(figsize=(6, 6))
plt.plot(x, x)
plt.plot(x, f(x, mu, n))
web = cobweb(mu, n=n, num=1000, keep=999)
plt.plot(web[:, 0], web[:, 1], linewidth=0.5)
plt.show()
display(button)
button.on_click(lambda b: fig.savefig(f'logistic_N_cobweb.png'))


Here are our $$\mu$$ values with $$f(x)$$.

x = np.linspace(0, 1, 5000)
n = 1

def f(x, mu, n):
x1 = x
for i in range(n):
x1 = mu * x1 * (1 - x1)
return x1

fig, axarr = plt.subplots(2, 2, figsize=(8, 8))
for index, i, j in [(i, int(i / 2), i % 2) for i in range(4)]:
axarr[i, j].plot(x, x)
axarr[i, j].plot(x, f(x, mu_vals[index], n))
web = cobweb(mu_vals[index], n=n, num=5000, keep=1000, initial=0.8)
axarr[i, j].plot(web[:, 0], web[:, 1], linewidth=0.5)
axarr[i, j].set_title(r'$\mu_{}$={}, {} cycle'.format(index + 1, mu_vals[index], (index + 1) * 2 + 1))
plt.tight_layout()
plt.savefig('logistic_N_odd_cycles_cobweb.png')


We can clearly see that we have period $$\{3, 5, 7, 9\}$$ cycles here, but if we plot with $$n=3$$ we obtain something else entirely.

x = np.linspace(0, 1, 5000)
n = 3

def f(x, mu, n):
x1 = x
for i in range(n):
x1 = mu * x1 * (1 - x1)
return x1

fig, axarr = plt.subplots(2, 2, figsize=(8, 8))
for index, i, j in [(i, int(i / 2), i % 2) for i in range(4)]:
axarr[i, j].plot(x, x)
axarr[i, j].plot(x, f(x, mu_vals[index], n))
web = cobweb(mu_vals[index], n=n, num=5000, keep=1000, initial=0.8)
axarr[i, j].plot(web[:, 0], web[:, 1], linewidth=0.5)
axarr[i, j].set_title(r'$\mu_{}$={}, {} cycle'.format(index + 1, mu_vals[index], (index + 1) * 2 + 1))
plt.tight_layout()
plt.savefig('logistic_n_3_odd_webs.png')


In the $$f^{3}$$ case, since we’re plotting $$f(f(f(x)))$$, our period 3 or bit becomes a fixed point, and all orbits that are share a root of $$3$$ become the previous orbit. So in this case our period 3 orbit becomes a period 0 (fixed point) orbit, and our period $$9$$ orbit becomes our period 3 orbit.