Jump to my Home Page Send me a message Check out stuff on GitHub Check out my photography on Instagram Check out my profile on LinkedIn Check out my profile on reddit Check me out on Facebook

Mitch Richling: Brusselator

Author: Mitch Richling
Updated: 2025-02-09

brusselator3_strip.png

Table of Contents

1. Introduction

The Brusselator is a reaction-diffusion model for an autocatalytic chemical reaction in space and time. This system was was proposed by Ilya Romanovich Prigogine and Rene Lefever from Universite libre de Bruxelles – hence the name. The model is given by the following equations:

\[ \begin{eqnarray} \frac{\partial u_1}{\partial t} & = & 1-(b+1)u_1+au_1^2u_2+d_1 \left(\frac{\partial^2 u_1}{\partial x_1^2}+\frac{\partial^2 u_1}{\partial x_2^2}\right) \\ \frac{\partial u_2}{\partial t} & = & bu_1-au_1^2u_2+d_2 \left(\frac{\partial^2 u_2}{\partial x_1^2}+\frac{\partial^2 u_2}{\partial x_2^2}\right) \end{eqnarray} \]

The functions \( u_1 \) and \( u_2 \) represent dimensionless concentrations of two of the reactants – an activator and an inhibitor. The values \( a \) and \( b \) represent normalized reaction rates. The values \( d_1 \) and \( d_2 \) are "diffusion coefficients", and may be combined into a single value \( d=\frac{d_2}{d_1} \) like this:

\[ \begin{eqnarray} \frac{\partial u_1}{\partial t} & = & 1-(b+1)u_1+au_1^2u_2 \left(\frac{\partial^2 u_1}{\partial x_1^2}+\frac{\partial^2 u_1}{\partial x_2^2}\right) \\ \frac{\partial u_2}{\partial t} & = & bu_1-au_1^2u_2+d \left(\frac{\partial^2 u_2}{\partial x_1^2}+\frac{\partial^2 u_2}{\partial x_2^2}\right) \end{eqnarray} \]

In the code below, we use the following values for the parameters:

\[ \begin{array}{lcc} a & = & 9 \\ b & = & \frac{98}{10} \\ d & = & 3 \end{array} \]

We use a simple finite differences method to solve the equations with a step size of \( \Delta t=0.007 \) over 3000 steps. The spatial grid has a grid size of \( h=0.3 \). The spatial second derivatives at a point \( (x,y) \) are approximated with the five point stencil":

\[ \frac{\partial^2 u_j}{\partial x_1^2}+\frac{\partial^2 u_j}{\partial x_2^2} \approx \frac{u_j(x+h, y)+u_j(x-h, y)+u_j(x, y+h)+u_j(x, y-h)-4u_j(x,y)}{h^2} \]

The code uses four images to store the state of the system at two steps. At each time step it uses two of the images to update the other two. This doubles the RAM required, but simplifies the code and eliminates the need for swapping data. At the end of the run the last updated state images are combined into a 24-bit RGB image which is written to disk.

2. Code

#include "ramCanvas.hpp"

typedef mjr::ramCanvas3c8b rcT;

int main(void) {
  std::random_device rd;
  std::minstd_rand0 rEng(rd());
  std::uniform_real_distribution<double> uniform_dist_double(1.0e-5, 1.0);
  int width  = 7680/32;
  int height = 4320/32;

  std::chrono::time_point<std::chrono::system_clock> startTime = std::chrono::system_clock::now();
  rcT theRamCanvas(width, height);

  std::vector<mjr::ramCanvas1c64F> imgu1{mjr::ramCanvas1c64F(width, height), mjr::ramCanvas1c64F(width, height)};
  std::vector<mjr::ramCanvas1c64F> imgu2{mjr::ramCanvas1c64F(width, height), mjr::ramCanvas1c64F(width, height)};

  mjr::ramCanvas1c64F::pointIntVecType st { {0,-1}, {0,1}, {-1,0}, {1,0}};

  int    N = 3000;
  double h = 0.3;
  double t = 0.007;
  double a = 9.0;
  double b = 9.8;
  double d = 3.0;

  for(rcT::coordIntType y=0;y<theRamCanvas.getNumPixY();y++) 
    for(rcT::coordIntType x=0;x<theRamCanvas.getNumPixX();x++) {
      imgu1[0].drawPoint(x, y, uniform_dist_double(rEng));
      imgu2[0].drawPoint(x, y, uniform_dist_double(rEng));
    }

  int i_in, i_ou;
  double maxv = 0;
  for(int step=0; step<N; step++) {
    i_in = (step%2);
    i_ou = ((step+1)%2);
    if (0==(step%100))
      std::cout << "STEP: " << step << std::endl;
# pragma omp parallel for schedule(static,1)
    for(rcT::coordIntType y=0;y<theRamCanvas.getNumPixY();y++) {
      for(rcT::coordIntType x=0;x<theRamCanvas.getNumPixX();x++) {

        double u1_sum = 0;
        double u2_sum = 0;
        for(mjr::ramCanvas1c64F::pointIntType const &p: st) {
          u1_sum += imgu1[i_in].getPxColorChanWrap<0>(x+p.x, y+p.y);
          u2_sum += imgu2[i_in].getPxColorChanWrap<0>(x+p.x, y+p.y);
        }

        double u1_c = imgu1[i_in].getPxColor(x, y).getC0();
        double u2_c = imgu2[i_in].getPxColor(x, y).getC0();

        double du1_c = t*(1.0-(b+1)*u1_c+a*u1_c*u1_c*u2_c+(u1_sum-4*u1_c)/(h*h));
        double du2_c = t*(b*u1_c-a*u1_c*u1_c*u2_c+d*(u2_sum-4*u2_c)/(h*h));

        double u1 = u1_c + du1_c;
        double u2 = u2_c + du2_c;

        maxv = std::max(maxv, std::max(u1, u2));

        imgu1[i_ou].drawPoint(x, y, u1);
        imgu2[i_ou].drawPoint(x, y, u2);
      }
    }
    if (maxv > 1e10) {
      std::cout << "Solution Failure at step " << step << std::endl;
      return 1;
    } else if (maxv < 0.0001) {
      std::cout << "Zero Solution at step " << step << std::endl;
      return 1;
    }
  }

  imgu1[i_ou].autoHistStrech();
  imgu2[i_ou].autoHistStrech();
  for(int y=0;y<theRamCanvas.getNumPixY();y++)
    for(int x=0;x<theRamCanvas.getNumPixX();x++) {
      rcT::colorChanType r = static_cast<rcT::colorChanType>(255*imgu1[i_ou].getPxColorNC(x, y).getC0());
      rcT::colorChanType b = static_cast<rcT::colorChanType>(255*imgu2[i_ou].getPxColorNC(x, y).getC0());
      theRamCanvas.drawPoint(x, y, mjr::color3c8b(r, 0, b));
    }
  theRamCanvas.writeTIFFfile("brusselator.tiff");

  std::chrono::duration<double> runTime = std::chrono::system_clock::now() - startTime;
  std::cout << "Total Runtime " << runTime.count() << " sec" << std::endl;
  return 0;
}

Source Link: brusselator.cpp

brusselator3_20.png brusselator5_20.png

3. References

Check out the fractals section of my reading list.

All the code used to generate everything on this page may be found on github.