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: Attracting Torus

Author: Mitch Richling
Updated: 2023-11-18

attracting_torus_shadowX0_4.png attracting_torus_shadowZ0_4.png attracting_torus_shadowY0_4.png
attracting_torus_shadowXY_4.png attracting_torus_shadowXZ_4.png attracting_torus_shadowYZ_4.png

Table of Contents

1. Introduction

I first came across this strange attractor in Sprott's book "Elegant Automation" where it is the topic of chapter 43 starting on page 311. The simplified equations are:

\[\begin{align*} \frac{dx}{dt} & = y \\ \frac{dy}{dt} & = -x-yz \\ \frac{dz}{dt} & = y^2+bz-a \end{align*}\]

Traditional paramater values are: \[\begin{align*} a & = 4 \\ b & = \frac{1}{10} \end{align*}\]

Traditional initial conditions are: \[\begin{align*} x_0 & = 0 \\ y_0 & = 2 \\ z_0 & = 1 \end{align*}\]

This attractor is an attractive one (pun intended):

at_50.gif

What inspired me to code up this thing was an illustration on page 315 of Sprott's book showing the intersection of the attractor and the \(z=0\) plain.

2. Algorithms

We can solve this system numerically in much the same way we solved the lorenz system; however, in order to render the intersection of the attractor with the coordinate axis plains we need to tightly control the step size. I have taken a super simple, and slow approach: At each step I expand \(\Delta t\) until the step is too big, then I shrink it till it is small enough. The code runs fast enough that I have not investigated how optimal this scheme is. ;) Here is what the code looks like:

#include "ramCanvas.hpp"

int main(void) {
  std::chrono::time_point<std::chrono::system_clock> startTime = std::chrono::system_clock::now();
  const int XSIZ = 7680/1;
  const int YSIZ = 4320/1;
  mjr::ramCanvas3c8b theRamCanvasX0(XSIZ, YSIZ, -10, 10, -10, 10);
  mjr::ramCanvas3c8b theRamCanvasY0(XSIZ, YSIZ, -10, 10, -10, 10);
  mjr::ramCanvas3c8b theRamCanvasZ0(XSIZ, YSIZ, -10, 10, -10, 10);
  mjr::ramCanvas3c8b theRamCanvasXY(XSIZ, YSIZ, -10, 10, -10, 10);
  mjr::ramCanvas3c8b theRamCanvasXZ(XSIZ, YSIZ, -10, 10, -10, 10);
  mjr::ramCanvas3c8b theRamCanvasYZ(XSIZ, YSIZ, -10, 10, -10, 10);

  double isectDistToGo = 1e8; /* How long should the curve be for Z0 ? */
  double curveDistToGo = 1e4; /* How long should the curve be for XY, XZ, & YZ? */

  double a = 4.0; /* Equation paramaters */
  double b = 0.1; /* Equation paramaters */
  double x = 0.0; /* Initial  conditions */
  double y = 2.0; /* Initial  conditions */
  double z = 1.0; /* Initial  conditions */

  double targetDist    = 0.025; /* Size of each step on curve */
  int maxNumUpBisect   = 5;     /* Max times we double tDelta to get > targetDist. */
  int maxNumDownBisect = 10;    /* Max times we half tDelta to get < targetDist. */

  /*  Solve the equations..... */
  double tDelta = 1.0;
  double dist = 0.0;
  double xOld = x;
  double yOld = y;
  double zOld = z;
  double Xdelta, Ydelta, Zdelta, movDist;
  while (dist < isectDistToGo) {
    /*  Find tDelta that gets us bigger than targetDist */
    int numUpBisect = 0;
    do {
      tDelta += (2 * tDelta);
      Xdelta = (y) * tDelta;
      Ydelta = (-x-z*y) * tDelta;
      Zdelta = (y*y-a+b*z) * tDelta;
      movDist = sqrt (fabs (Xdelta * Xdelta + Ydelta * Ydelta + Zdelta * Zdelta));
      numUpBisect++;
    } while ((numUpBisect < maxNumUpBisect) && (movDist < targetDist));

    /* Find tDelta that gets us a jump of <targetDist. */
    int numDownBisect = 0;
    do {
      tDelta = tDelta / 2;
      Xdelta = (y) * tDelta;
      Ydelta = (-x-z*y) * tDelta;
      Zdelta = (y*y-a+b*z) * tDelta;
      movDist = sqrt (fabs (Xdelta * Xdelta + Ydelta * Ydelta + Zdelta * Zdelta));
      numDownBisect++;
    } while ((numDownBisect < maxNumDownBisect) && (movDist > targetDist));

    /* Update and draw */
    dist += movDist;
    x = x + Xdelta;
    y = y + Ydelta;
    z = z + Zdelta;
    if (dist < curveDistToGo) {
      theRamCanvasXY.drawPoint(x, y, mjr::ramCanvas3c8b::colorType::cornerColorEnum::WHITE);
      theRamCanvasXZ.drawPoint(x, z, mjr::ramCanvas3c8b::colorType::cornerColorEnum::WHITE);
      theRamCanvasYZ.drawPoint(y, z, mjr::ramCanvas3c8b::colorType::cornerColorEnum::WHITE);
    }
    if(z*zOld<=0.0) {
      if (z < 0) {
        theRamCanvasZ0.drawPoint((x+xOld)/2.0, (y+yOld)/2.0, theRamCanvasZ0.getPxColor((x+xOld)/2.0, (y+yOld)/2.0).setC0(255));
      } else {
        theRamCanvasZ0.drawPoint((x+xOld)/2.0, (y+yOld)/2.0, theRamCanvasZ0.getPxColor((x+xOld)/2.0, (y+yOld)/2.0).setC2(255));        
      }
      theRamCanvasZ0.drawPoint((x+xOld)/2.0, (y+yOld)/2.0, theRamCanvasZ0.getPxColor((x+xOld)/2.0, (y+yOld)/2.0).setC1(255));
    }
    if(x*xOld<=0.0) {
      if (x < 0) {
        theRamCanvasX0.drawPoint((y+yOld)/2.0, (z+zOld)/2.0, theRamCanvasX0.getPxColor((y+yOld)/2.0, (z+zOld)/2.0).setC0(255));
      } else {
        theRamCanvasX0.drawPoint((x+xOld)/2.0, (y+yOld)/2.0, theRamCanvasX0.getPxColor((y+yOld)/2.0, (z+zOld)/2.0).setC2(255));        
      }
      theRamCanvasX0.drawPoint((y+yOld)/2.0, (z+zOld)/2.0, theRamCanvasX0.getPxColor((y+yOld)/2.0, (z+zOld)/2.0).setC1(255));
    }
    if(y*yOld<=0.0) {
      if (y < 0) {
        theRamCanvasY0.drawPoint((x+xOld)/2.0, (z+zOld)/2.0, theRamCanvasY0.getPxColor((x+xOld)/2.0, (z+zOld)/2.0).setC0(255));
      } else {
        theRamCanvasY0.drawPoint((x+xOld)/2.0, (y+yOld)/2.0, theRamCanvasY0.getPxColor((x+xOld)/2.0, (z+zOld)/2.0).setC2(255));        
      }
      theRamCanvasY0.drawPoint((x+xOld)/2.0, (z+zOld)/2.0, theRamCanvasY0.getPxColor((x+xOld)/2.0, (z+zOld)/2.0).setC1(255));
    }
    xOld = x;
    yOld = y;
    zOld = z;
  }

  theRamCanvasXY.writeTIFFfile("attracting_torus_shadowXY.tiff");
  theRamCanvasXZ.writeTIFFfile("attracting_torus_shadowXZ.tiff");
  theRamCanvasYZ.writeTIFFfile("attracting_torus_shadowYZ.tiff");
  theRamCanvasZ0.writeTIFFfile("attracting_torus_shadowZ0.tiff");
  theRamCanvasX0.writeTIFFfile("attracting_torus_shadowX0.tiff");
  theRamCanvasY0.writeTIFFfile("attracting_torus_shadowY0.tiff");

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

  return 0;
}

The above program will generate the following pictures:

attracting_torus_shadowX0_4.png attracting_torus_shadowZ0_4.png attracting_torus_shadowY0_4.png
attracting_torus_shadowX0_BW_4.png attracting_torus_shadowZ0_BW_4.png attracting_torus_shadowY0_BW_4.png
attracting_torus_shadowXY_4.png attracting_torus_shadowXZ_4.png attracting_torus_shadowYZ_4.png

3. A Movie

Now that we can see what the intersection looks like for \(z=0\), how about a movie of the intersection of a plane moving up and down parallel to that plain? Here is the code:

#include "ramCanvas.hpp"

int main(void) {
  std::chrono::time_point<std::chrono::system_clock> startTime = std::chrono::system_clock::now();
  const int XSIZ = 7680/4;
  const int YSIZ = 4320/4;

  const int    NUMFRM = 120; /* Number of movie frames */

  const double zLevStart = -4.0;
  const double zLevEnd   =  4.0;

  const double isectDistToGo = 5e7; /* How long should the curve be for Z0 ? */

  const double a = 4.0; /* Equation paramaters */
  const double b = 0.1; /* Equation paramaters */

  const double x0 = 0.0; /* Initial  conditions */
  const double y0 = 2.0; /* Initial  conditions */
  const double z0 = 1.0; /* Initial  conditions */

  const double targetDist    = 0.025; /* Size of each step on curve */
  const int maxNumUpBisect   = 5;     /* Max times we double tDelta to get > targetDist. */
  const int maxNumDownBisect = 10;    /* Max times we half tDelta to get < targetDist. */

# pragma omp parallel for schedule(static,1)
  for(int frame=0; frame<NUMFRM; frame++) {
    std::chrono::time_point<std::chrono::system_clock> frameStartTime = std::chrono::system_clock::now();
    mjr::ramCanvas3c8b theRamCanvas(XSIZ, YSIZ, -10, 10, -10, 10);
    double zLev = zLevStart + frame*(zLevEnd-zLevStart)/(NUMFRM-1);

    /*  Solve the equations..... */
    double x = x0;
    double y = y0;
    double z = z0;
    double tDelta = 1.0;
    double dist = 0.0;
    double xOld = x;
    double yOld = y;
    double zOld = z;
    double Xdelta, Ydelta, Zdelta, movDist;
    while (dist < isectDistToGo) {
      /*  Find tDelta that gets us bigger than targetDist */
      int numUpBisect = 0;
      do {
        tDelta += (2 * tDelta);
        Xdelta = (y) * tDelta;
        Ydelta = (-x-z*y) * tDelta;
        Zdelta = (y*y-a+b*z) * tDelta;
        movDist = sqrt (fabs (Xdelta * Xdelta + Ydelta * Ydelta + Zdelta * Zdelta));
        numUpBisect++;
      } while ((numUpBisect < maxNumUpBisect) && (movDist < targetDist));

      /* Find tDelta that gets us a jump of <targetDist. */
      int numDownBisect = 0;
      do {
        tDelta = tDelta / 2;
        Xdelta = (y) * tDelta;
        Ydelta = (-x-z*y) * tDelta;
        Zdelta = (y*y-a+b*z) * tDelta;
        movDist = sqrt (fabs (Xdelta * Xdelta + Ydelta * Ydelta + Zdelta * Zdelta));
        numDownBisect++;
      } while ((numDownBisect < maxNumDownBisect) && (movDist > targetDist));

      /* Update and draw */
      dist += movDist;
      x = x + Xdelta;
      y = y + Ydelta;
      z = z + Zdelta;
      if((z-zLev)*(zOld-zLev)<=0.0)
        theRamCanvas.drawPoint((x+xOld)/2.0, (y+yOld)/2.0, mjr::ramCanvas3c8b::colorType::cornerColorEnum::WHITE);
      xOld = x;
      yOld = y;
      zOld = z;
    }

    theRamCanvas.writeTIFFfile("attracting_torus_shadowM_" + mjr::fmtInt(frame, 3, '0') + ".tiff");
    std::chrono::duration<double> frameRunTime = std::chrono::system_clock::now() - frameStartTime;
#   pragma omp critical
    std::cout << "Frame " << frame << " of " << NUMFRM << " Runtime " << frameRunTime.count() << " sec" << std::endl;
  }

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

  return 0;
}

And the result

attracting_torus_shadowM_25.gif

4. 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.