Mitch Richling: Attracting Torus
Author: | Mitch Richling |
Updated: | 2023-11-18 |
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):
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:
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