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: Symmetric Fractals

Author: Mitch Richling
Updated: 2024-10-31

sic_22c_960.jpg

Table of Contents

1. Symmetric Fractals

The symmetric fractals featured on this page were inspired by the book "Symmetry in Chaos" by Michael Field and Martin Golubitsky. All of the fractals here are generated from the same equation:

\[z_{k+1} = (\lambda + \alpha z_k \overline{z_k} + \beta\, \Re(z_k^n) + \omega i) z_k + \gamma \overline{z_k}^{(n-1)}\]

Where the following are constants (n is an integer and the rest are real):

\[\lambda\,\, \alpha\,\, \beta\,\, \omega\,\, \gamma\,\, n\]

and the iteration begins with:

\[z_0 = \frac{1}{100} + \frac{1}{100} i \]

Just as with the Peter de Jong attractors we look at the probability density function, \(P(x,y)\), defined by the probability that some \((x_n, y_n)\) from the map will be in a neighborhood of \((x,y)\). While that sounds complicated, producing a visualization of this PDF is quite simple in practice – we simply visualize a histogram based on computed values of the map. The first step is to define a rectangular region of the plane over which we wish to visualize the PDF, and then define a set of discrete pixels over that region. Then we compute a few million, or billion, iterations of the map, and count the number of times we hit each pixel.

2. Code: Drawing Things

Some code will make the above discussion clear:

#include "ramCanvas.hpp"

typedef mjr::ramCanvas3c8b rct;
typedef rct::colorType ct;

std::vector<std::array<mjr::ramCanvas1c16b::coordFltType, 12>> params {
  /*  lambda       alpha      beta     gamma      omega   n    ipw   xmin  xmax   ymin  ymax    1=mean */
  { 1.375390, -0.4212800,  0.26969,  0.08352,  0.338347,  6, 15.00, -1.30, 1.30, -1.30, 1.30, 1.0}, // 0  |
  { 1.600230, -1.1340800, -0.17506,  0.67872,  0.049490,  6, 14.00,  0.10, 0.70,  0.43, 0.90, 0.0}, // 1  |  WACKY
  { 1.000890, -0.7678100,  1.89425, -1.13943, -0.688827,  7, 10.00, -0.90, 0.90, -0.90, 0.90, 0.0}, // 2  |
  {-1.563400,  1.7238200,  0.74440,  0.71874,  0.258907,  3,  8.00, -0.75, 0.75, -0.75, 0.75, 1.0}, // 3  |
  { 1.361000, -1.8225400, -0.92635, -1.74108, -0.761120,  5,  2.00, -0.70, 0.70, -0.70, 0.70, 1.0}, // 4  |
  { 1.609310, -1.6924900,  0.85055,  0.26523, -0.716769,  5,  4.00, -0.90, 0.90, -0.90, 0.90, 0.0}, // 5  |
  { 1.464900, -1.8825700, -1.47205, -0.83559, -0.701477,  4,  3.00, -0.80, 0.80, -0.80, 0.80, 1.0}, // 6  |
  { 1.622110, -1.0694500, -0.11181,  0.62253,  0.784032,  4,  2.00, -1.20, 1.20, -1.20, 1.20, 1.0}, // 7  |
  {-1.380150,  0.7472100,  1.17094,  0.05782, -0.574188,  6, 10.00, -1.00, 1.00, -1.00, 1.00, 0.0}, // 8  |
  { 0.335010, -1.3467500, -0.65137, -0.87848, -1.231490,  5, 10.00, -1.20, 1.20, -1.20, 1.20, 1.0}, // 9  |
  {-1.496290,  1.8701600, -1.93938, -0.88084, -0.139602,  3, 10.00, -0.55, 0.55, -0.55, 0.55, 1.0}, // 10 |
  {-1.248610,  1.0816100, -0.50731,  0.97272, -0.853955, 11, 10.00, -1.00, 1.00, -1.00, 1.00, 0.0}, // 11 |
  {-1.991900,  1.8219100, -0.38273,  0.68143,  0.493416, 11, 10.00, -1.00, 1.00, -1.00, 1.00, 0.0}, // 12 |
  { 1.694170, -1.2730200,  0.75035, -1.28615,  0.561096,  6,  2.50, -1.10, 1.10, -1.10, 1.10, 1.0}, // 13 |
  {-0.386753, -0.0610025, -1.02847,  0.78619, -0.984828,  3,  5.00, -1.20, 1.20, -1.20, 1.20, 0.0}, // 14 | special
  { 0.982876, -1.1125600, 0.500411, -1.44111, -1.008560,  4,  7.00, -0.80, 0.80, -0.80, 0.80, 0.0}, // 15 | special
  {-1.257170,  0.0390811,  0.13892, -0.87058,  0.023977,  7,  2.60, -1.20, 1.20, -1.20, 1.20, 1.0}, // 16 | Neat
  { 1.008500,  0.2275230, -0.56604, -0.62146,  0.693161,  5,  3.00, -1.20, 1.20, -1.20, 1.20, 1.0}, // 17 |
  { 1.618250, -1.6035300,  1.13920, -1.60120, -0.761638,  5,  2.80, -1.00, 1.00, -1.00, 1.00, 0.0}, // 18 |
  {-0.960670,  0.0834029,  1.01438,  1.05748, -0.599590, 21,  2.60, -1.20, 1.20, -1.20, 1.20, 1.0}, // 19 |
  { 0.908230,  0.3047990,  1.03560,  0.29872, -0.473521,  7,  3.00, -1.20, 1.20, -1.20, 1.20, 0.0}, // 20 |
  { 1.873130, -1.2822600, -1.28938, -1.36355, -0.141951, 21,  2.00, -1.10, 1.10, -1.10, 1.10, 1.0}, // 21 | NICE
  { 1.536250, -0.6547000, -1.43255, -0.24270,  0.128769, 31,  2.50, -1.00, 1.00, -1.00, 1.00, 0.0}, // 22 | NICE
  {-2.080000,  1.0000000, -0.10000,  0.16700,  0.000000,  7,  4.00, -1.20, 1.20, -1.20, 1.20, 0.0}, // 23 | d7 SIC BOOK
  {-2.500000,  5.0000000, -1.90000,  1.00000,  0.180000,  5,  2.75, -0.75, 0.75, -0.75, 0.75, 0.0}, // 24 | Z5 SIC BOOK
  { 2.500000, -2.5000000,  0.00000,  0.90000,  0.000000,  3,  2.75, -1.40, 1.40, -1.40, 1.40, 0.0}, // 25 | D3 SIC BOOK
  { 2.600000, -2.0000000,  0.00000, -0.50000,  0.000000,  5,  4.75, -1.30, 1.30, -1.30, 1.30, 1.0}, // 26 | D5 SIC BOOK
};

// This is *identical* to what we did in pickoverPopcorn.cpp -- just way shorter.  It is longer still because we don't make
// this a subclass of ramCanvasTpl::rcConverterHomoBase in order to illustrate how to implement a RC converter from scratch.
// Also note we didn't need to DIY the color gradient with cmpRGBcornerDGradiant() as this gradient (0RYBCW) is available as
// a pre-built color scheme: csCCfractal0RYBCW.
class g2rgb8 {
  private:
    mjr::ramCanvas1c16b& attachedRC;
    int factor;
  public:
    g2rgb8(mjr::ramCanvas1c16b& aRC, uint64_t newFactor) : attachedRC(aRC), factor(static_cast<int>(newFactor)) {  }
    inline bool isIntAxOrientationNaturalX() { return attachedRC.isIntAxOrientationNaturalX(); }
    inline bool isIntAxOrientationNaturalY() { return attachedRC.isIntAxOrientationNaturalY(); }
    inline mjr::ramCanvas1c16b::coordIntType getNumPixX() { return attachedRC.getNumPixX(); }
    inline mjr::ramCanvas1c16b::coordIntType getNumPixY() { return attachedRC.getNumPixY(); }
    typedef mjr::colorRGB8b colorType;
    inline colorType getPxColorNC(rct::coordIntType x, rct::coordIntType y) { 
      colorType retColor;
      rct::csIntType tmp = static_cast<rct::csIntType>(attachedRC.getPxColorNC(x, y).getC0() * 1275 / factor);
      return retColor.cmpRGBcornerDGradiant(tmp, "0RYBCW");
    }
};

int main(void) {
  std::chrono::time_point<std::chrono::system_clock> startTime = std::chrono::system_clock::now();
  const int BSIZ = 7680/4;
  mjr::ramCanvas1c16b::colorType aColor;
  aColor.setChans(1);
  for(decltype(params.size()) j=0; j<params.size(); ++j) {
    mjr::ramCanvas1c16b theRamCanvas(BSIZ, BSIZ, params[j][7], params[j][8], params[j][9], params[j][10]);
    typename mjr::ramCanvas1c16b::coordFltType lambda = params[j][0];
    typename mjr::ramCanvas1c16b::coordFltType alpha  = params[j][1];
    typename mjr::ramCanvas1c16b::coordFltType beta   = params[j][2];
    typename mjr::ramCanvas1c16b::coordFltType gamma  = params[j][3];
    typename mjr::ramCanvas1c16b::coordFltType w      = params[j][4];
    double ipw   = static_cast<double>(params[j][6]);
    int n        = static_cast<int>(  params[j][5]);
    int filter   = static_cast<int>(  params[j][11]);

    std::complex<typename mjr::ramCanvas1c16b::coordFltType> cplxi (0,1);

    uint64_t maxitr = 10000000000ul;

    std::complex<typename mjr::ramCanvas1c16b::coordFltType> z(.01,.01);
    uint64_t maxII = 0;
    for(uint64_t i=0;i<maxitr;i++) {
      z = (lambda + alpha*z*std::conj(z)+beta*std::pow(z, n).real() + w*cplxi)*z+gamma*std::pow(std::conj(z), n-1);
      typename mjr::ramCanvas1c16b::coordFltType x=z.real(), y=z.imag();
      if(i>1000)
        theRamCanvas.drawPoint(x, y, theRamCanvas.getPxColor(x, y).tfrmAdd(aColor));
      if(theRamCanvas.getPxColor(x, y).getC0() > maxII) {
        maxII = theRamCanvas.getPxColor(x, y).getC0();
        if(maxII > 16384) { // 1/4 of max possible intensity
          std::cout << "ITER(" << j <<  "): " << i << " MAXS: " << maxII << " EXIT: Maximum image intensity reached" << std::endl;
          break;
        }
      }
      if((i % 10000000) == 0)
        std::cout << "ITER(" << j <<  "): " << i << " MAXS: " << maxII << std::endl;
    }

    std::cout << "ITER(" << j <<  "): " << "Big TIFF" << std::endl;
    theRamCanvas.writeTIFFfile("sic_" + mjr::math::str::fmt_int(j, 2, '0') + ".tiff");

    // Root image transform
    std::cout << "ITER(" << j <<  "): " << "TFRM & SCALE" << std::endl;
    theRamCanvas.autoHistStrech();
    //theRamCanvas.applyHomoPixTfrm(&mjr::ramCanvas1c16b::colorType::tfrmLn);
    theRamCanvas.applyHomoPixTfrm(&mjr::ramCanvas1c16b::colorType::tfrmStdPow, 1/ipw);
    if(filter)
      theRamCanvas.scaleDownMean(4);
    else
      theRamCanvas.scaleDownMax(4);
    theRamCanvas.autoHistStrech();

    // Compte new image max intensity
    std::cout << "ITER(" << j <<  "): " << "MAX" << std::endl;
    maxII = 0;
    for(auto& pixel : theRamCanvas)
      if(pixel.getC0() > maxII)
        maxII = pixel.getC0();

    std::cout << "ITER(" << j <<  "): " << "TIFF" << std::endl;
    /* Dump the 16-bit grayscale TIFF */
    theRamCanvas.writeTIFFfile("sicM_" + mjr::math::str::fmt_int(j, 2, '0') + ".tiff");
    /* Now we would like a false color version (24-bit RGB).   We could create a new canvas like this:
               rct cRamCanvas(theRamCanvas.getNumPixX(), theRamCanvas.getNumPixY());
               for(mjr::ramCanvas1c16b::coordIntType y=0;y<theRamCanvas.getNumPixY();y++)
                 for(mjr::ramCanvas1c16b::coordIntType x=0;x<theRamCanvas.getNumPixX();x++) {
                   auto ci = static_cast<rct::csIntType>(theRamCanvas.getPxColorRefNC(x, y).getC0() * 1275 / maxII)
                   cRamCanvas.getPxColorRefNC(x, y).cmpRGBcornerDGradiant(ci, "0RYBCW");
                 }
               cRamCanvas.writeTIFFfile("sicC_" + mjr::math::str::fmt_int(j, 2, '0') + ".tiff");
       We have a better way.  One that dosen't require the RAM to create a brand new canvas.  We can use
       the filter option of writeTIFFfile! */
    g2rgb8 rcFilt(theRamCanvas, maxII);
    theRamCanvas.writeTIFFfile("sicCC_" + mjr::math::str::fmt_int(j, 2, '0') + ".tiff", rcFilt, false);
  }
  std::chrono::duration<double> runTime = std::chrono::system_clock::now() - startTime;
  std::cout << "Total Runtime " << runTime.count() << " sec" << std::endl;
  return 0;
}

3. Gallery

Here are a few examples. Most of these were discovered via an automated search program using the technique briefly outlined in the next section. When you draw one of these images with a random set of parameters, the odds are that you are the first human being ever to see that precise image – with five, 32-bit parameters the odds of someone else picking the same parameters is 1 in 2^160.

sic_0_274.jpg sic_1_274.jpg sic_2_274.jpg sic_3_274.jpg sic_4_274.jpg sic_5_274.jpg sic_6_274.jpg sic_7_274.jpg sic_8_274.jpg sic_9_274.jpg sic_10_274.jpg sic_11_274.jpg sic_12_274.jpg sic_13_274.jpg sic_14_274.jpg sic_15_274.jpg sic_16_274.jpg sic_17_274.jpg sic_18_274.jpg sic_19_274.jpg sic_20_274.jpg sic_21_274.jpg sic_22_274.jpg sic_23_274.jpg sic_24_274.jpg sic_25_274.jpg sic_26_274.jpg

4. Code: Finding Things

The following code will help to find interesting candidates for parameters:

#include "ramCanvas.hpp"

int main(void) {
  std::chrono::time_point<std::chrono::system_clock> startTime = std::chrono::system_clock::now();
  const int BSIZ = 2048;

  std::random_device rd;
  std::mt19937 rEng(rd());
  std::uniform_real_distribution<double> uniform_dist_double(-2.0, 2.0);
  std::uniform_int_distribution<int>     uniform_dist_int(3, 7);

  mjr::ramCanvas1c16b theRamCanvas(BSIZ, BSIZ, -2, 2, -2, 2); // Just used for coordinate conversion. ;)

  uint64_t maxCnt = 0;
  for(int j=0; j<100000; j++) {
    std::map<uint64_t, uint64_t> ptcnt;
    double lambda = uniform_dist_double(rEng);
    double alpha  = uniform_dist_double(rEng);
    double beta   = uniform_dist_double(rEng);
    double gamma  = uniform_dist_double(rEng);
    double w      = uniform_dist_double(rEng);
    int    n      = uniform_dist_int(rEng);
    std::complex<double> z(0.01,0.01);
    for(uint64_t i=0;i<1000;i++) {
      z = (lambda + alpha*z*std::conj(z)+beta* std::pow(z, n).real() + w*std::complex<double>(0,1))*z+gamma*static_cast<std::complex<double>>(std::pow(std::conj(z), n-1));
      ptcnt[((uint64_t)theRamCanvas.real2intX(z.real()))<<32 | ((uint64_t)theRamCanvas.real2intY(z.imag()))] = 1;
    }
    if(ptcnt.size() > maxCnt) {
      maxCnt = ptcnt.size();
      std::cout << j << " " << maxCnt << " " << lambda << "," <<  alpha << "," <<  beta << "," <<  gamma << "," <<  w << "," << n << std::endl;
    }
  }
  std::chrono::duration<double> runTime = std::chrono::system_clock::now() - startTime;
  std::cout << "Total Runtime " << runTime.count() << " sec" << std::endl;
  return 0;
}

5. Some Fine Points

This fractal system presents most of the same practical problems we encountered with the Peter de Jong attractors but with a few extra wrinkles:

  • The image processing pipeline is both more sophisticated, and varies for each set of parameters.
  • We can't always start with a fixed region of the plane, so this too varies with each set of parameters.
  • More hand work is required when evaluating the results of automated parameter searching – the highest number of turned on pixels may not be the most interesting image..

6. References

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

Check out the fractals section of my reading list.