Attractors panel

View a running version of this notebook. | Download this project.


Panel/Numba/Datashader Strange Attractors app

Strange attractors are a type of iterative equation that traces the path of a particle through a 2D space, forming interesting patterns in the trajectories.

There are many attractor families determined by a set of attractor equations, each with associated numerical parameters. To make the parameter space easy to explore, we'll build an app that selects between the attractor families and allows you to manipulate their parameter values. Using this app requires conda install -c pyviz/label/dev panel param datashader and a live, running Python process (not just a static web page or anaconda.org viewer). You may wish to check out the much simpler Clifford-only app first, to understand the basic structure of an app and of how to compute an attractor.

Attractor definitions

First, we'll define a function for collecting the values of any attractor function with up to six parameters, using Numba to make it 50X faster than bare Python:

In [1]:
import numpy as np, pandas as pd
from numba import jit

@jit(nopython=True)
def trajectory_coords(fn, x0, y0, a, b, c, d, e, f, n):
    x, y = np.zeros(n), np.zeros(n)
    x[0], y[0] = x0, y0
    for i in np.arange(n-1):
        x[i+1], y[i+1] = fn(x[i], y[i], a, b, c, d, e, f)
    return x, y

def trajectory(fn, x0, y0, a, b=None, c=None, d=None, e=None, f=None, n=1000000):
    xs, ys = trajectory_coords(fn, x0, y0, a, b, c, d, e, f, n)
    return pd.DataFrame(dict(x=xs,y=ys))

Next, we'll define a class hierarchy to provide a shared interface for the various attractor types:

In [2]:
import param, panel as pn, inspect
pn.extension('katex')

class Attractor(param.Parameterized):
    """A parameterized object that can evaluate an attractor trajectory"""
    
    x = param.Number(0,  softbounds=(-2, 2), doc="Starting x value", precedence=-1)
    y = param.Number(0,  softbounds=(-2, 2), doc="Starting y value", precedence=-1)

    a = param.Number(1.7, bounds=(-3, 3), doc="Attractor parameter a")
    b = param.Number(1.7, bounds=(-3, 3), doc="Attractor parameter b")

    colormap = param.ObjectSelector("kgy", precedence=0.7, check_on_set=False,
        objects=['bgy', 'bmw', 'bgyw', 'bmy', 'fire', 'gray', 'kgy', 'kbc', 'viridis', 'inferno'])

    equations = param.List([], class_=str, precedence=-1, readonly=True)
    
    __abstract = True
    
    def __call__(self, n, x=None, y=None):
        """Return a dataframe with *n* points"""
        if x is not None: self.x=x
        if y is not None: self.y=y
        args = [getattr(self,p) for p in self.sig()]
        return trajectory(self.fn, *args, n=n)
    
    def vals(self):
        return [self.__class__.name] + [self.colormap] + [getattr(self,p) for p in self.sig()]

    def sig(self):
        """Returns the calling signature expected by this attractor function"""
        return list(inspect.signature(self.fn).parameters.keys())[:-1]
    
class FourParamAttractor(Attractor):
    """Base class for most four-parameter attractors"""
    c = param.Number(0.6, softbounds=(-3, 3), doc="Attractor parameter c")
    d = param.Number(1.2, softbounds=(-3, 3), doc="Attractor parameter d")

    __abstract = True