As mentioned last time, I am going to look at using 2D transforms with the basic and gnarly image types. Not sure what it will do for the basic image type, but likely worth a look. Expect using transforms on the gnarly image will generate some additional interest as it did for the mosaic style images.

Bug

I have corrected this bug I don’t know how many times. At least I think I have. The code should run every time btw_n_apart() gets called. But for some reason it keeps getting shifted into the if block above. And, only runs once. Future calls then do not generate the proper second data set for the colouring between datasets. Don’t know if it’s me periodically reverting something or some autoformatting issue in the editor.

The malfunctioning code looked like this.

  if not g.u_again:
    if cnt_btw == 0 or len(c_mlts) == 0:
      if mlt:
        c_mlts, i_data = incr_data(rys, mlt=mlt)
      else:
        c_mlts = [1.0 for _ in range(len(rys))]
        i_data = rys
      c_ndx = (c_ndx + c_jmp) % c_len
      t_c_ndx = c_ndx
      cnt_btw += 1

      c_ndx = t_c_ndx
      c_mlts, i_data = incr_data(rys, mlt=mlt, nw_mlt=nw_mlt)

It should look like the following.

  if not g.u_again:
    if cnt_btw == 0 or len(c_mlts) == 0:
      if mlt:
        c_mlts, i_data = incr_data(rys, mlt=mlt)
      else:
        c_mlts = [1.0 for _ in range(len(rys))]
        i_data = rys
      c_ndx = (c_ndx + c_jmp) % c_len
      t_c_ndx = c_ndx
      cnt_btw += 1

  c_ndx = t_c_ndx
  c_mlts, i_data = incr_data(rys, mlt=mlt, nw_mlt=nw_mlt)

I also decided to refactor incr_data() and consequently btw_n_apart(). I was beginning to keep track of or modify cnt_btw in too many places. Thought I should change that. They now look as follows.

def btw_n_apart(axt, rxs, rys, dx=1, ol=True, r=False, fix=None, mlt=False, sect=1):
  global c_mlts, i_data, t_c_ndx, cnt_btw

  # between 0,0+dx ; 1,1+dx; etc
  c_ndx = 0
  c_len = len(g.cycle)
  c_ndx = rng.integers(0, c_len-2)
  c_jmp = c_len // 20
  if c_jmp % 2 == 0:
    c_jmp += 1

  if r:
    n_fs = len(rxs) - dx - 1
  else:
    n_fs = len(rxs) - dx
  if dx == 1 and not ol:
    if r:
      b_rng = range(n_fs, 0, -2)
    else:
      b_rng = range(1, n_fs, 2) 
  else:
    if r:
      b_rng = range(n_fs, 0, -1)
    else:
      b_rng = range(1, n_fs)

  nw_mlt = cnt_btw == 0 or len(c_mlts) == 0

  if not g.u_again:
    if mlt:
      _, i_data = incr_data(rys, mlt=mlt, nw_mlt=nw_mlt)
    else:
      c_mlts = [1.0 for _ in range(len(rys))]
      i_data = rys
    c_ndx = (c_ndx + c_jmp) % c_len
    t_c_ndx = c_ndx
    cnt_btw += 1

  nw_mlt = cnt_btw == 0 or len(c_mlts) == 0
  c_ndx = t_c_ndx
  _, i_data = incr_data(rys, mlt=mlt, nw_mlt=nw_mlt)

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

  for i in b_rng:
    if i == i+dx and c_mlts[i] == 1.0:
      continue
    if fix is not None:
      axt.fill_between(rxs[fix], rys[i], i_data[i+dx], alpha=1, color=g.cycle[c_ndx])
      c_ndx = (c_ndx + c_jmp) % c_len
    else:
      if sect == 1:
        axt.fill_between(rxs[i], rys[i], i_data[i+dx], alpha=1, color=g.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)
          axt.fill_between(rxs[i][s_st:s_nd], rys[i][s_st:s_nd], i_data[i+dx][s_st:s_nd], alpha=1, color=g.cycle[c_ndx])
          c_ndx = (c_ndx + c_jmp) % c_len

        axt.fill_between(rxs[i][s_nd:], rys[i][s_nd:], i_data[i+dx][s_nd:], alpha=1, color=g.cycle[c_ndx])

def incr_data(a_rows, mlt=False, nw_mlt=False):
  # multiply data rows by random value between 1 and 2 inclusive
  # only going to use a few values, with prob of 1 slightly higher than rest
  global cnt_btw, c_mlts
  m_rows = []
  n_rw = len(a_rows)
  r_hlf = n_rw // 2

  if nw_mlt:
    # if new multipliers requested
    if mlt:
        mlts_f = rng.choice(p_mlt2, r_hlf)
        mlts_b = rng.choice(p_mlt1, n_rw - r_hlf)
        c_mlts = np.concatenate((mlts_f, mlts_b))
    else:
      c_mlts = [1.0 for _ in range(n_rw)]
  # else use the existing ones
  
  for i in range(n_rw):
      c_mlt = c_mlts[i]
      m_rows.append(a_rows[i] * c_mlt)

  return c_mlts, m_rows

Gnarly Image Transforms

Back to the matter at hand. I will start with the gnarly images. I expect whatever I end up coding/refactoring will carry over directly to the basic image case.

Refactor

I wasn’t going to do so, but in the end I decided to move some of the repetitive bits into their own functions. So, rather than show you the before and after, I am going to show you the two new functions. Both functions were added to the library module, sp_app_lib.py. Refactoring the mosaic transforms route should be easy enough, so it’s not included in the post.

... ...
def get_transform_base(rxs, rys):
  # get angles
  lld_rot = get_angles(g.nbr_tr)
  lld_deg = [math.degrees(ang) for ang in lld_rot]
  # get init point for linear translations
  mnx41, mxx41, mny41, mxy41 = get_plot_bnds(rxs, rys, x_adj=0)
  mdy = max(mxx41, mxy41)
  y_mlt = rng.choice([.875, .75, .625, .5, .375])
  trdy = mdy * y_mlt

  return lld_rot, lld_deg, trdy, y_mlt


def get_transform_data(rxs, rys, ang, trdy):
  a = np.array([[np.cos(ang), -np.sin(ang)],
              [np.sin(ang), np.cos(ang)]])

  dstx, dsty = affine_transformation(rxs, rys, a)
  dx, dy = rotate_pt(0, trdy, angle=ang, cx=0, cy=0)
  dstx = dstx + dx
  dsty = dsty + dy

  return dstx, dsty
... ...

New Route

I am going to start by coding the new route, /spirograph/gnarly_t. I will reuse as much existing code as possible. Refactoring any duplicate code in the new route and /spirograph/mosaic_t. With any luck, no really new code will be needed.

That was in fact quite painless. Copied the original gnarly route. Refactored title and such. Added bits and pieces related to the transfroms from sp_mosaic_t() and bingo. Though it is pretty slow generating the image.

@app.route('/spirograph/gnarly_t', methods=['GET', 'POST'])
def sp_gnarly_t():
  pg_ttl = "Gnarly Spirograph Image with 2D Transforms"
  if request.method == 'GET':
    f_data = {'pttl': pg_ttl, 'shapes': splt.shp_nm, 'cmaps': g.clr_opt,
        'mntr': g.mn_tr, 'mxtr': g.mx_tr
    }
    return render_template('get_curve.html', type='gnarly', f_data=f_data)
  elif request.method == 'POST':
    # print(f"POST[shape]: {request.form.get('shape')}, POST[shp_mlt]: {request.form.get('shp_mlt')}")

    sal.proc_curve_form(request.form, 'gnarly')
    t_xs, t_ys, t_shp = sal.init_curve()

    r_xs, r_ys, _, _, _, _ = sal.get_gnarly_dset(t_xs, t_ys, g.r_skp, g.drp_f, g.t_sy)

    ax.clear()
    # ax.patch.set_alpha(0.01)
    g.rcm, g.cycle = sal.set_clr_map(ax, g.lc_frq, g.rcm)

    # get angles and such
    lld_rot, lld_deg, trdy, y_mlt = sal.get_transform_base(r_xs, r_ys)
    
    # get rand quadrant order
    do_qd = list(range(g.nbr_tr))
    do_qd2 = sal.get_rnd_qds(do_qd)

    # Generate transformed datasets
    for i in do_qd2:
      dstx, dsty = sal.get_transform_data(r_xs, r_ys, lld_rot[i], trdy)

      ax.plot(dstx, dsty, lw=g.ln_w, alpha=g.alph, zorder=g.pz_ord)
      ax.autoscale()

    ax.text(0.5, 0.01, "© koach (barkncats) 2022", ha="center", transform=ax.transAxes,
            fontsize=10, alpha=.3, c=g.cycle[len(g.cycle)//2])
    ax.autoscale()

    bax2, _ = sal.set_bg(ax)

    # convert image to base64 for embedding in html page
    data = sal.fig_2_base64(fig)

    d_end = "top" if g.drp_f else "bottom"
    c_data = sal.get_curve_dtl()
    i_data = sal.get_image_dtl(pg_ttl)
    p_data = {
              'd_end': d_end, 'd_nbr': g.r_skp,
              'ntr': g.nbr_tr, 'trang': lld_deg, 'trpt': f'(0, {round(trdy, 3)}) ({y_mlt})',
              'trord': do_qd2
             }

    return render_template('disp_image.html', sp_img=data, type='gnarly', c_data=c_data, i_data=i_data, p_data=p_data)

This route is much slower than the others at producing an image. Not sure why. But, given I am doing much the same here as I am with the mosaic images, I expect it has more to do with matplotlib than my code. Though the Numpy operations are likely fairly demanding from a resource point of view.

When I put the code into production on Google cloud, this route had more problems. Still very slow. But every so often Google App Engine would generate an error and tell me to wait 30 seconds and try again. I expect the code is taking too long to complete (safety valve) or surpassing some other resource restriction. Consequently the engine stops processing the current request and resets.

Examples

Curve Parameters
  Wheels: 6 wheels (shape(s): ellipse (e))
  Symmetry: k_fold = 6, congruency = 2
  Frequencies: [-4, 26, 26, -10, 20, -22]
  Widths: [1, 0.585398292599137, 0.40456082845342, 0.32837345296429793j, 0.24474847367573913, 0.17312780508562073j]
  Heights: [1, 0.6883351014520809, 0.5468725087278253j, 0.4397271991308957j, 0.3361310521506485j, 0.33389191435322474]

Image Type Parameters
  Drop data rows: 1 datarow
  From: dropped from the 'top' of the curve dataset

Drawing Parameters
  Colour map: turbo
  BG colour map: Greys
  Line width (if used): 3

Transformations
  Number of transforms: 6
  Angles: [29.999999999999996, 90.0, 149.99999999999997, 210.0, 270.0, 330.0]
  Order: [5, 2, 4, 3, 1, 0]
  Base linear translation point: (0, 1.194) (0.5)
gnarly image using 2D transforms
Curve Parameters
  Wheels: 4 wheels (shape(s): equilateral triangle (q))
  Symmetry: k_fold = 2, congruency = 1
  Frequencies: [3, 7, 5, 7]
  Widths: [1, 0.5615310720304078, 0.2929231230865153j, 0.1532381553031087j]
  Heights: [1, 0.5396588769681948j, 0.5300024753641639j, 0.30874204909562275j]

Image Type Parameters
  Drop data rows: 1 datarow
  From: dropped from the 'top' of the curve dataset

Drawing Parameters
  Colour map: plasma
  BG colour map: magma
  Line width (if used): 3

Transformations
  Number of transforms: 5
  Angles: [36.0, 108.0, 180.0, 252.0, 324.0]
  Order: [1, 3, 4, 0, 2]
  Base linear translation point: (0, 0.376) (0.375)
gnarly image using 2D transforms
Curve Parameters
  Wheels: 5 wheels (shape(s): circle (c))
  Symmetry: k_fold = 2, congruency = 1
  Frequencies: [-1, 5, -1, 5, 5]
  Widths: [1, 0.6613879650713143, 0.561885389505404, 0.3918693298474105j, 0.29481877528508743]
  Heights: [1, 0.6877778639704673j, 0.3894099039436909, 0.20619387731560404, 0.19292089024513723]

Image Type Parameters
  Drop data rows: 1 datarow
  From: dropped from the 'top' of the curve dataset

Drawing Parameters
  Colour map: viridis
  BG colour map: Greys
  Line width (if used): 8

Transformations
  Number of transforms: 3
  Angles: [29.999999999999996, 149.99999999999997, 270.0]
  Order: [0, 1, 2]
  Base linear translation point: (0, 0.909) (0.625)
gnarly image using 2D transforms
Curve Parameters
  Wheels: 10 wheels (shape(s): tetracuspid (t))
  Symmetry: k_fold = 2, congruency = 1
  Frequencies: [-1, -7, -5, -7, 9, -1, -1, 5, 7, 3]
  Widths: [1, 0.721895311630224j, 0.5915976836746908, 0.310120723976356, 0.2778588634471638, 0.20841542658112058, 0.1301579320539617j, 0.125, 0.125, 0.125]
  Heights: [1, 0.6755032300724298, 0.5442945816944038, 0.44844213045951997, 0.3100814075221635, 0.16794712566017175, 0.125, 0.125j, 0.125, 0.125]

Image Type Parameters
  Drop data rows: 1 datarow
  From: dropped from the 'top' of the curve dataset

Drawing Parameters
  Colour map: magma
  BG colour map: magma
  Line width (if used): 2

Transformations
  Number of transforms: 3
  Angles: [29.999999999999996, 149.99999999999997, 270.0]
  Order: [1, 0, 2]
  Base linear translation point: (0, 0.904) (0.5)
gnarly image using 2D transforms

Basic Spirograph Transforms

New Route

Basically repeated what I did above for the route /spirograph/basic_t. Minor bit of refactoring with respect to available curve data variables. For now I have not bothered to move the really repetitive bits into their own functions.

@app.route('/spirograph/basic_t', methods=['GET', 'POST'])
def sp_basic_t():
  pg_ttl = "Basic Spirograph Image with 2D Transforms"
  if request.method == 'GET':
    f_data = {'pttl': pg_ttl, 'shapes': splt.shp_nm, 'cmaps': g.clr_opt,
        'mntr': g.mn_tr, 'mxtr': g.mx_tr
    }
    return render_template('get_curve.html', type='basic', f_data=f_data)
  elif request.method == 'POST':
    # app.logger.info(f"POST[shape]: {request.form.get('shape')}, POST[shp_mlt]: {request.form.get('shp_mlt')}")
    # print(f"POST[shape]: {request.form.get('shape')}, POST[shp_mlt]: {request.form.get('shp_mlt')}")

    sal.proc_curve_form(request.form, 'basic')
    t_xs, t_ys, t_shp = sal.init_curve()

    ax.clear()
    # ax.patch.set_alpha(0.01)
    g.rcm, g.cycle = sal.set_clr_map(ax, g.lc_frq, g.rcm)

    # get angles and such
    lld_rot, lld_deg, trdy, y_mlt = sal.get_transform_base(t_xs, t_ys)

    # get rand quadrant order
    do_qd = list(range(g.nbr_tr))
    do_qd2 = sal.get_rnd_qds(do_qd)

    # Generate transformed datasets
    for i in do_qd2:
      dstx, dsty = sal.get_transform_data(t_xs, t_ys, lld_rot[i], trdy)

      ax.plot(dstx[-1], dsty[-1], lw=g.ln_w, alpha=1)
      ax.autoscale()

    ax.text(0.5, 0.01, "© koach (barkncats) 2022", ha="center", transform=ax.transAxes,
            fontsize=10, alpha=.3, c=g.cycle[len(g.cycle)//2])
    ax.autoscale()

    bax2, _ = sal.set_bg(ax)

    # convert image to base64 for embedding in html page
    data = sal.fig_2_base64(fig)

    c_data = sal.get_curve_dtl()
    i_data = sal.get_image_dtl(pg_ttl)
    p_data = {'ntr': g.nbr_tr, 'trang': lld_deg, 'trpt': f'(0, {round(trdy, 3)}) ({y_mlt})',
              'trord': do_qd2
    }

    return render_template('disp_image.html', sp_img=data, type='basic', c_data=c_data, i_data=i_data, p_data=p_data)

Examples

Curve Parameters
  Wheels: 7 wheels (shape(s): tetracuspid (t))
  Symmetry: k_fold = 2, congruency = 1
  Frequencies: [3, 5, 5, 3, 5, -7, 3]
  Widths: [1, 0.5066047593394428, 0.3470390727382054, 0.20839418705227622, 0.13185322148862264, 0.125, 0.125]
  Heights: [1, 0.5309519557205008j, 0.3696601849776622, 0.2767212513533699j, 0.20717680378545328j, 0.1401937286385749j, 0.125]

Drawing Parameters
  Colour map: hot
  BG colour map: YlOrRd
  Line width (if used): 6

Transformations
  Number of transforms: 5
  Angles: [36.0, 108.0, 180.0, 252.0, 324.0]
  Order: [1, 3, 2, 4, 0]
  Base linear translation point: (0, 0.458) (0.375)
basic spirograpn image using 2D transforms
Curve Parameters
  Wheels: 12 wheels (shape(s): circle (c))
  Symmetry: k_fold = 3, congruency = 1
  Frequencies: [-2, 10, 7, 13, -5, -5, 1, 10, 7, -8, -2, 10]
  Widths: [1, 0.5505461490785912, 0.4075101704769578j, 0.3637199617567362j, 0.28199291996722764j, 0.16910511223006836, 0.1615143241039201, 0.125j, 0.125, 0.125, 0.125, 0.125]
  Heights: [1, 0.5635565664960167, 0.44300320225242357, 0.3394684842989719, 0.3007103196449747, 0.25912386857968395j, 0.15366790628340057j, 0.1278398208792956j, 0.125, 0.125, 0.125, 0.125j]

Drawing Parameters
  Colour map: gist_earth
  BG colour map: cividis
  Line width (if used): 5

Transformations
  Number of transforms: 6
  Angles: [29.999999999999996, 90.0, 149.99999999999997, 210.0, 270.0, 330.0]
  Order: [2, 1, 3, 0, 5, 4]
  Base linear translation point: (0, 1.335) (0.75)
basic spirograpn image using 2D transforms
Curve Parameters
  Wheels: 6 wheels (shape(s): square (s))
  Symmetry: k_fold = 5, congruency = 4
  Frequencies: [-1, 9, 9, -11, 19, 24]
  Widths: [1, 0.6743617994418798j, 0.5308580908391798, 0.35950437842250804j, 0.21510661297314643j, 0.14782096011581788]
  Heights: [1, 0.6347747861660389, 0.5197903000456291j, 0.3960090088200574j, 0.36970219868766924j, 0.35058217332304425j]

Drawing Parameters
  Colour map: GnBu
  BG colour map: bone
  Line width (if used): 3

Transformations
  Number of transforms: 6
  Angles: [29.999999999999996, 90.0, 149.99999999999997, 210.0, 270.0, 330.0]
  Order: [3, 2, 1, 4, 0, 5]
  Base linear translation point: (0, 1.431) (0.875)
basic spirograpn image using 2D transforms

Done

I think that’s it for another one. Must say I do like the images. Especially the gnarly and mosaic ones. Though the basic spirograph with transforms has potential. However, I think in that case I should likely be using the same colour to draw each transform. Differing colours seem a touch distracting, probably ruining the look and feel.

I am also not happy with the interface for using the same curve for subsequent images. Once I have selected my state variables (with some of them being random) on the form and generated the first image, I don’t normally go back to the form. I simply do a reload in the browser window to get another image. Hopefully a different image.

Sometimes I want to change one of the parameters (e.g. use or do not use multipliers for the mosaic images). But then I get the same curve over and over. I’d like to be able for instance to leave the curve shape random for repeated images and just change the multiplier state. Can’t do that right now. Something for next time.

I’d also like to add one or more the cycling line width variations to the app.

Be happy and keep those fingers tapping.