Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Attractors

attractors.livemd

Attractors

Background

This example is looking at implementing a Lorenz ‘strange’ attactor. It is notable for having chaotic solutions for certain parameter values and initial conditions.

The equations are quite simple and can be implemented quite easily in a recursive function.

For examples see - Wikipedia, Lorenz System

The Equations

In a three dimensional space $${x,y,z} $$ each value is dimension is updated by the following formulas: $$ \frac{dx}{dt} = \sigma(y-x)$$ $$ \frac{dy}{dt} = x(\rho -z) -y$$ $$ \frac{dz}{dt} = xy - \beta z$$

*note in the following code $$\sigma (sigma) $$ $$\rho (rho)$$ $$\beta (beta) $$

defmodule Lorenz do
  @sigma 10.0
  @rho 28.0
  @beta 8.0 / 3.0

  def update(_pos, 0, acc) do
    acc
  end

  def update(pos, iter, acc) do
    new_pos = calc_update(pos)
    update(new_pos, iter - 1, acc ++ [new_pos])
  end

  def calc_update({x, y, z} = pos) do
    {x + calc_x(pos), y + calc_y(pos), z + calc_z(pos)}
  end

  def calc_x({x, y, _z}) do
    @sigma * (y - x)
  end

  def calc_y({x, y, z}) do
    x * (@rho - z) - y
  end

  def calc_z({x, y, z}) do
    x * y - @beta * z
  end
end

Lets try it out - lets start with a sample point {1, 1, 1} and run for 20 iterations

Lorenz.update({1, 1, 1}, 20, [])

We have come up with a problem - Elixir does not support arbitary precision floats, so we have some issues with the above implementation, but help is at hand.

The Decimal package lets try and implement our module with that library. First it needs to be installed.

Mix.install([
  {:decimal, "~> 2.0"},
  {:vega_lite, "~> 0.1.0"},
  {:kino, "~> 0.1.0"}
])

# The graphing deps are installed here that will be used later
defmodule Lorenz do
  alias Decimal, as: D

  @sigma 10
  @rho 28
  @beta Decimal.from_float(8 / 3)

  def update(_pos, 0, acc) do
    acc
  end

  def update(pos, iter, acc) do
    new_pos = calc_update(pos)
    IO.inspect(iter)
    IO.inspect(new_pos)
    update(new_pos, iter - 1, acc ++ [new_pos])
  end

  def calc_update({x, y, z} = pos) do
    {D.add(calc_x(pos), x), D.add(calc_y(pos), y), D.add(calc_z(pos), z)}
  end

  def calc_x({x, y, _z}) do
    D.mult(@sigma, D.sub(y, x))
  end

  def calc_y({x, y, z}) do
    rhoz = D.sub(@rho, z)
    D.sub(D.mult(x, rhoz), y)
  end

  def calc_z({x, y, z}) do
    D.sub(D.mult(x, y), D.mult(@beta, z))
  end
end
one = Decimal.from_float(1.0)
Lorenz.update({one, one, one}, 20, [])

The problem with this is the implicit $${dt}$$ which is “1” in the above - which is a massive time step in calculus the delta means an infinitesimal change, So for a first attempt we can add a delta_t param and do some rough calculations.

defmodule Lorenz do
  alias Decimal, as: D

  @sigma 10
  @rho 28
  @beta Decimal.from_float(8 / 3)
  @delta_t Decimal.from_float(0.002)

  def update(_pos, 0, acc) do
    acc
  end

  def update(pos, iter, acc) do
    new_pos = calc_update(pos)
    IO.inspect(iter)
    IO.inspect(new_pos)
    update(new_pos, iter - 1, acc ++ [new_pos])
  end

  def calc_update({x, y, z} = pos) do
    dx = D.mult(calc_x(pos), @delta_t)
    dy = D.mult(calc_y(pos), @delta_t)
    dz = D.mult(calc_z(pos), @delta_t)
    {D.add(dx, x), D.add(dy, y), D.add(dz, z)}
  end

  def calc_x({x, y, _z}) do
    D.mult(@sigma, D.sub(y, x))
  end

  def calc_y({x, y, z}) do
    rhoz = D.sub(@rho, z)
    D.sub(D.mult(x, rhoz), y)
  end

  def calc_z({x, y, z}) do
    D.sub(D.mult(x, y), D.mult(@beta, z))
  end
end
one = Decimal.from_float(1.0)
Lorenz.update({one, one, one}, 150, [])

Lets try adding a Graph to see the output, so instead of printing the values out we will plot the x, y co-ords on a graph and see if we get anything interesting.

alias VegaLite, as: Vl
vl_widget =
  Vl.new(width: 400, height: 400)
  |> Vl.mark(:point)
  |> Vl.encode_field(:x, "x", type: :quantitative)
  |> Vl.encode_field(:y, "y", type: :quantitative)
  |> Kino.VegaLite.start()
defmodule Lorenz do
  alias Decimal, as: D

  @sigma 10
  @rho 28
  @beta Decimal.from_float(8 / 3)
  @delta_t Decimal.from_float(0.002)

  def update(_pos, 0, _w) do
    :ok
  end

  def update(pos, iter, vl_widget) do
    {x, y, z} = calc_update(pos)
    Kino.VegaLite.push(vl_widget, %{x: D.to_float(x), y: D.to_float(y)})
    update({x, y, z}, iter - 1, vl_widget)
  end

  def calc_update({x, y, z} = pos) do
    dx = D.mult(calc_x(pos), @delta_t)
    dy = D.mult(calc_y(pos), @delta_t)
    dz = D.mult(calc_z(pos), @delta_t)
    {D.add(dx, x), D.add(dy, y), D.add(dz, z)}
  end

  def calc_x({x, y, _z}) do
    D.mult(@sigma, D.sub(y, x))
  end

  def calc_y({x, y, z}) do
    rhoz = D.sub(@rho, z)
    D.sub(D.mult(x, rhoz), y)
  end

  def calc_z({x, y, z}) do
    D.sub(D.mult(x, y), D.mult(@beta, z))
  end
end
one = Decimal.from_float(1.0)
Lorenz.update({one, one, one}, 1000, vl_widget)