As mentioned last time I want to look at using larger numbers of transformations for the latest image variation. Maybe play with adding some chaos. Maybe…

But, I also wanted to be able to control things a little. So I refactored some of the code from the last post to make use of those between interface commands I added/introduced a post or three ago. E.G. bwl, bws.

I was able to use the previous commands for multiple quadrants and messing with symmetry without needing to modify the code used for this image. Those are looked after at a higher level in the application code.

Refactor

One of the complications I have is keeping track of the data needed to ensure I am going to save exactly the image I was just viewing. Because I do generate some of the image parameters (variables) randomly, that does mean saving the bits and pieces I need in case I decide to save an image to file. And, of course they need to be updated whenever I generate a new image.

I ran into some of those issues when refactoring the code/functions for this current image type. Not to mention more bugs than I would have liked to see. I don’t know that I do it the best way possible. But I thought I would show you how I currently do things. I am guessing that if I was using an object oriented approach things would be somewhat tidier. But, I have never really gotten into programming in an object oriented style. Procedural me.

I am leaving my many print statements in the code that follows. I save all the information printed to the command window, when saving an image to file, so that I can, if desired, regenerate the base image in future.

You may have noticed that global line in some of the various functions’ code. Those are the module variables I am using to track some of the bits and pieces I need to regenerate the same image when saving to file.

      def btw_qds(axt, rxs, rys1, rys2, mlt=False, sect=1):
        global c_mlts, i_data, rys2_i, t_c_ndx, b42_rows

        print(f"\t\tbtw_qds(..., mlt={mlt}, sect={sect}) (cnt_42: {cnt_42})")

        # number of data rows
        n_rws = len(rxs)

        # on each iteration, jump to the colour c_jmp away,
        # rolling to the front of cycle when necessary (i.e. modulus)
        c_ndx = 0
        c_len = len(cycle)
        c_jmp = c_len // 20
        if c_jmp % 2 == 0:
          c_jmp += 1

        if not t_sv:
        # if not saving current image to file, generate new values for some of the parameters
          # starting index for colour cycle
          c_ndx = (c_ndx + c_jmp) % c_len
          t_c_ndx = c_ndx

Because I call this function numerous times for a single image, I needed a way to make sure I didn’t change important values during the repeated calls. In my initial code, the multipliers and the order for plotting data rows would change on each call. That made it rather difficult to recreate the image on saving to file. And a great deal more data to track for each image invocation. So, I chose to use a single set of multipliers for each function call while working on the same image. Ditto the plotting order. And a few other bits and pieces.

To do so, I chose to use a counter variable. It is set to zero when the image code block is invoked from the user interface. Then it is incremented following each call to btw_qds().

          if cnt_42 == 0:
            # generate plotting order for second quadrant data
            rys2_i = list(range(len(rys2)))
            random.shuffle(rys2_i)
            # sort plotting row(s) for first quadrant data
            # I used the bwl[s, m, e, r] and bwd[#] interface commands to give me some control
            b42_rows = [] # list of rows for the first quadrant
            if bw_l == 's':
              rw1 = 0
            elif bw_l == 'e':
              rw1 = -1
            elif bw_l == 'm':
              rw1 = n_rws // 2
              if n_rws % 2 == 1:
                rw1 += 1
            elif bw_l == 'r':
              rw1 = random.choice(rys2_i)
            b42_rows.append(rw1)
            if bw_dx > 1:
              t_rys = rys2_i[:]
              t_rys.remove(rw1)
              n_2get = bw_dx - 1
              mrws = random.sample(t_rys, n_2get)
              b42_rows.extend(mrws)
            if mlt:
              c_mlts, i_data = incr_data(rys2)
            else:
              c_mlts = [1.0 for _ in range(len(rys2))]
              i_data = rys2
          else:
            rw1 = b42_rows[0]
        else:
          c_ndx = t_c_ndx
          rw1 = b42_rows[0]

But because the quadrant data changes with each call, I also needed to apply the multipliers if they are called for. So I had to add code to look after that on subsequent function invocations.

          if do_plt == 42:
            if mlt:
              i_data = []
              n_rw = len(rys2)
              for i in range(n_rw):
                c_mlt = c_mlts[i]
                i_data.append(rys2[i] * c_mlt)
            else:
              i_data = rys2

        print(f"\t\tDEBUG: rys2_i: {rys2_i}; rw1: {rw1} -> {b42_rows}")

        s_sz = len(rxs[0]) // sect

        print(f"\t\tDEBUG: mlt={mlt}, c_mlts={c_mlts}; s_sz: {s_sz}")
        if bw_dx > 1:
          print(f"\t\tDEBUG: will be plotting against extra rows: {b42_rows[1:]}")

        for i in range(len(rxs)):

          # don't plot between the same data row
          if i == rys2_i[i] and c_mlts[i] == 1.0:
            continue
          if sect == 1:
            if do_dbg:
              print(f"\t\tax.fill_between(rxs[{i}], rys1[{i}], rys2[{rys2_i[i]}]*{c_mlts[i]}, alpha={bw_lph}, c={c_ndx})")
            axt.fill_between(rxs[rw1], rys1[rw1], i_data[rys2_i[i]], alpha=bw_lph, color=cycle[c_ndx])
            c_ndx = (c_ndx + c_jmp) % c_len
            if bw_dx > 1:
              # if more than one row being used for the first dataset deal with it
              for crw in b42_rows[1:]:
                axt.fill_between(rxs[crw], rys1[crw], i_data[rys2_i[i]], alpha=bw_lph, color=cycle[c_ndx])
                c_ndx = (c_ndx + c_jmp) % c_len
          else:
            for j in range(sect-1):
              s_st = s_sz * j
              s_nd = s_sz * (j+1)
              if do_dbg:
                print(f"\t\tax.fill_between(rxs[{i}][{s_st}:{s_nd}], rys1[{i}][{s_st}:{s_nd}]*{c_mlts[i]}, rys2[{rys2_i[i]}][{s_st}:{s_nd}]*{c_mlts[i]}, alpha={bw_lph}, c={c_ndx})")
              axt.fill_between(rxs[rw1][s_st:s_nd], rys1[rw1][s_st:s_nd], i_data[rys2_i[i]][s_st:s_nd], alpha=bw_lph, color=cycle[c_ndx])
              c_ndx = (c_ndx + c_jmp) % c_len
              if bw_dx > 1:
                # if more than one row being used for the first dataset deal with it
                for crw in b42_rows[1:]:
                  axt.fill_between(rxs[crw][s_st:s_nd], rys1[crw][s_st:s_nd], i_data[rys2_i[i]][s_st:s_nd], alpha=bw_lph, color=cycle[c_ndx])
                  c_ndx = (c_ndx + c_jmp) % c_len

            if do_dbg:
              print(f"\t\tax.fill_between(rxs[{i}][{s_nd}:], rys1[{i}][{s_nd}:]*{c_mlts[i]}, rys2[{rys2_i[i]}][{s_nd}:]*{c_mlts[i]}, alpha={bw_lph}), c={c_ndx}")
            axt.fill_between(rxs[rw1][s_nd:], rys1[rw1][s_nd:], i_data[rys2_i[i]][s_nd:], alpha=bw_lph, color=cycle[c_ndx])
            c_ndx = (c_ndx + c_jmp) % c_len
            if bw_dx > 1:
              for crw in b42_rows[1:]:
                axt.fill_between(rxs[crw][s_nd:], rys1[crw][s_nd:], i_data[rys2_i[i]][s_nd:], alpha=bw_lph, color=cycle[c_ndx])
                c_ndx = (c_ndx + c_jmp) % c_len
      
        return rys2_i

I was unhappy with the fact that the two lists for the quadrant plotting order sometimes overlapped at a given position, as I was not plotting anything in that case (didn’t think it made sense to do so). So some images were only getting a portion of the possible colour between sections. So, I decided to write a function to make sure there was not such overlap. Took me a bit more debugging than I expected it would take.

One of the big mistakes was using t_qds = qds1. That didn’t create a new copy of the list, it created an alias to it. So, because lsits are mutable, when I was removing entries from t_qds I was also doing so to qds1. And, should have realized that sooner, as I believe I have mentioned it in a previous post and read it often enough. I felt the easiest way to get an actual new list is to use a slice. In this a case a slice of the whole original list.

And periodically I’d end up in an infinite loop when calling this new function. Turns out I was not dealing with a specific edge case. If I was at the end of t_qds and the value was equal to the last element in qds1, that while block could not ever exit. So, a little extra checking and messing about.

def get_rnd_qds(qds1):
  n_qds = len(qds1)
  mx_lp = n_qds - 1
  # remember to get copy not alias
  t_qds = qds1[:]
  qds2 = []
  for i in range(n_qds):
    t_q = random.choice(t_qds)
    if i == mx_lp and qds1[i] == t_q: 
      t_lst = [t_q]
      t_lst.extend(qds2)
      qds2 = t_lst[:]
    else:
      while qds1[i] == t_q:
        t_q = random.choice(t_qds)
      qds2.append(t_q)
      t_qds.remove(t_q)

  return qds2  

In the main image block the relevant code now looks like the following. I am showing the lines that were removed as comments. The two module variables, do_qd and do_qd2 track the plotting order for the two quadrants passed to btw_qds(). And are not changed if I am saving the image to file.

... ...
      # sort number of quadrants and plotting order
      if not t_sv:
        do_qd = range(nbr_qds)
        if tr_qd and tq_r:
          do_qd = range(tq_r)
          nbr_qds = max(list(do_qd)) + 1
        # randomize plotting order
        do_qd = list(do_qd)
        random.shuffle(do_qd)
        # do_qd2 = list(range(nbr_qds))
        # random.shuffle(do_qd2)
        # while do_qd2 == do_qd:
        #   do_qd2 = list(range(nbr_qds))
        #   random.shuffle(do_qd2)
        do_qd2 = get_rnd_qds(do_qd)
... ...

Example Images

Be warned, these images take some time to generate and even longer for matplotlib to render on screen. Add a save to file and the time involved gets considerably longer. I was using 1024 data points for each row of the curve. (As the images were being generated on October 24th, seemed appropriate.) Increase the number of wheels and the time increases. Ditto for the number of rotations. Or if more than one row from the first quadrant is plotted against. And a little longer if multipliers are used. Good thing I am a patient sort.

Bug

When I started to generate the first set of example images, the image being saved was not the same as I image I had just seen. A quick look at the code and I found the problem. Strangely enough it was something I thought I had already corrected a few days ago. I had the revelant code under an if block it didn’t belong to. I’ll blame the editor. Fixed now.

But I thought I’d show you one of the incorrect saved images as it reminded me of a landscape painting of sorts. It is based on 8 wheels: equilateral triangles.

Section multipliers enabled
rys2_i: [3, 1, 0, 6, 2, 5, 4]; rw1: 0 -> [0]
mlt=True, c_mlts=[1.4, 2.0, 2.2, 1.6, 1.2, 1.4, 1.0]; s_sz: 32
image generated using plot between functionality and affine transforms

No Bug

Most of the following images are based on the same underlying curve. It uses 11 equilateral triangle shaped wheels.

9 transformations, middle row, multipliers enabled, 32 colour sections
image generated using plot between functionality and affine transforms
5 transformations, last row, multipliers enabled, 32 colour sections
image generated using plot between functionality and affine transforms
8 transformations, rows [9, 8, 3], multipliers enabled, 32 colour sections
image generated using plot between functionality and affine transforms
chaos enabled, 8 transformations, row 7, multipliers enabled, 32 colour sections
image generated using plot between functionality and affine transforms
chaos enabled, 12 transformations, row 9, multipliers disabled, 32 colour sections
image generated using plot between functionality and affine transforms
chaos enabled, 12 transformations, row 5, multipliers enabled, 32 colour sections
image generated using plot between functionality and affine transforms

The following use the same number of transformations as the preceding image, but are based on different underlying curves. By way of a sort of comparison how wheel shape might affect things.

wheels: 12 ellipses
chaos enabled, 12 transformations, row 7, multipliers enabled, 32 colour sections
image generated using plot between functionality and affine transforms
wheels: 9 rhombuses
chaos enabled, 12 transformations, row 3, multipliers enabled, 32 colour sections
image generated using plot between functionality and affine transforms
wheels: 8 tetracuspids
chaos enabled, 12 transformations, row 6, multipliers enabled, 32 colour sections
image generated using plot between functionality and affine transforms
wheels: 8 tetracuspids
chaos disabled, 12 transformations, row 5, multipliers enabled, 32 colour sections
image generated using plot between functionality and affine transforms

A great deal of similarity between these images. Regardless of wheel shape and other image parameters. But, if you want something colourful and a touch abstract and/or chaotic, these would likely suit you just fine.

Done

I am beginning to believe that this is truly the end for my blogging about spirograph images and their variations. But, time will tell.

Until next time, be happy and be coding.

Resources