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

Mandelbrot

mandelbrot.livemd

Mandelbrot

Mix.install([
  {:nx, "~> 0.4.1"},
  {:stb_image, "~> 0.6.0"},
  {:kino, "~> 0.11.3"},
  {:tint, "~> 1.1"},
  {:flow, "~> 1.2"}
])

Introduction

Inspiration:

Setup

Mandelbrot scale (which Mandelbrot window to calculate):

west = -2.00
east = 0.6
north = 1.2
south = -1.2
{west, east, north, south}
{mb_width, mb_height} = {east - west, north - south}

Canvas size:

{canvas_width, canvas_height} = {trunc(mb_width * 300), trunc(mb_height * 300)}

Iteration limit:

max_iterations = 360
palette_input = Kino.Input.select("Palette for coloring:", hue: "Hue", greyscale: "Greyscale")
method_input =
  Kino.Input.select("Calculation method:",
    flow: "Concurrently with 'flow' (9 min on my laptop)",
    serial: "Serial (33 min on my laptop)"
  )

Palette

Greyscale palette:

palette_greyscale = fn value, max ->
  index = trunc(255 * value / max)
  [index, index, index]
end

Hue-based palette

palette_hue = fn
  max, max ->
    [0, 0, 0]

  value, max ->
    Tint.HSV.new(359.9999 * value / max, 1, 1)
    |> Tint.to_rgb()
    |> Tint.RGB.to_tuple()
    |> Tuple.to_list()
end

Pick selected palette:

palette =
  case Kino.Input.read(palette_input) do
    :greyscale -> palette_greyscale
    :hue -> palette_hue
  end

Mandelbrot Function

mandelbrot = fn canvas_x, canvas_y, maxi ->
  x0 = west + canvas_x / canvas_width * mb_width
  y0 = south + canvas_y / canvas_height * mb_height

  recurser = fn recurser, x0, y0, x, y, i, maxi ->
    case {x * x + y * y, i < maxi} do
      {value, true} when value <= 4 ->
        xtemp = x * x - y * y + x0
        y = 2 * x * y + y0
        x = xtemp
        recurser.(recurser, x0, y0, x, y, i + 1, maxi)

      _ ->
        i
    end
  end

  recurser.(recurser, x0, y0, 0.0, 0.0, 0, maxi)
end

Methods

method_serial = fn canvas_height, canvas_width, max_iterations ->
  0..(canvas_height - 1)
  |> Enum.map(fn y ->
    0..(canvas_width - 1)
    |> Enum.map(fn x ->
      mandelbrot.(x, y, max_iterations)
      |> palette.(max_iterations)
    end)
  end)
end
method_flow = fn canvas_height, canvas_width, max_iterations ->
  0..(canvas_height - 1)
  |> Flow.from_enumerable(min_demand: 1, max_demand: 20)
  |> Flow.map(fn y ->
    payload =
      0..(canvas_width - 1)
      |> Enum.map(fn x ->
        mandelbrot.(x, y, max_iterations)
        |> palette.(max_iterations)
      end)

    {y, payload}
  end)
  |> Enum.to_list()
  |> Enum.sort(fn {i1, _}, {i2, _} -> i1 >= i2 end)
  |> Enum.map(fn {_, p} -> p end)
end
method =
  case Kino.Input.read(method_input) do
    :serial -> method_serial
    :flow -> method_flow
  end

Calculate

This is the part that takes time:

data = method.(canvas_height, canvas_width, max_iterations)

Rendering

image =
  Nx.tensor(
    data,
    type: {:u, 8},
    names: [:height, :width, :channels]
  )
StbImage.from_nx(image)
|> StbImage.to_binary(:png)
|> Kino.Image.new("image/png")