Continuing on from the last post, I will have a look at adding randomly selected line styles for each line in the plot. Then I will look at generating a figure with subplots showing what happens if we change the radii or frequencies for a fixed set of frequencies or radii. As we did earlier for plots of the base curve. I will do that in the same module we were working on previously.

I will also likely add some more command line parameters to control the use of random line widths and styles.

And, I did decide to move both get_ln_keep() and set_colour_map() to the spiro_plotlib package. Consequently, I had to modify the calls to those functions in the current module to reflect their new home. I also commented out the line: # ax.set_position([0, 0, .9, 1], which='both'). This centers the plot a little better on the chart. Though at the cost of some wasted space; but, all good.

Saving Files

I am also tired of manually saving plots to files for posts or just because. So, I have written some additional functions (in the plotting package). And added a command line parameter to allow me to specify whether or not the script should save the current plot to a file (default is not to save). I had previously created a directory structure for this purpose, so I am reusing it in these new functions. I was also saving meta-data regarding the plots to a text file. Just in case I wanted to try and recreate one of them. So, one of the functions will do just that.

So, in spiro_plotlib I added the following:

def make_fnm(ftype, c_meta):
  dt_tm = datetime.now().strftime("%m%d%H%M")
  f_nm = f"sp_{c_meta['wh']}_{c_meta['kf']}_{c_meta['cgv']}_{dt_tm}.{ftype}"
  return f_nm


def get_save_path(f_dir, f_typ, wh, kf, cgv):
  sv_final = ['ani', 'chg_ls', 'mline', 'plain', 'sb_mln', 'sb_pln']
  rel_pth = pathlib.Path('curves_me_like/spiro_4')
  if f_dir in sv_final:
    d_pth = rel_pth / f_dir
    sv_nm = make_fnm(f_typ, {'wh': wh, 'kf': kf, 'cgv': cgv})
    f_pth = d_pth / pathlib.Path(sv_nm)
    return f_pth
  else:
    # raise error?
    return None


def sv_dtls(c_pth, wh, spr, kf, cgv, spf, rcm, dt, tpt, rk=None, lcf=None, lcm=None):
  f_pth = pathlib.Path("curves_me_like/spiro_4/curve_dtls.txt")
  with open(f_pth, 'a') as fh:
    fh.write(f"\n{c_pth}\n")
    fh.write(f"\tget_radii({wh}) -> {spr}\n")
    fh.write(f"\tget_freqs(nbr_w={wh}, kf={kf}, mcg={cgv}) -> {spf}\n")
    fh.write(f"\tcolour map: {rcm}\n")
    fh.write(f"\ttest: {dt}, t_pts: {tpt}\n")
    if dt == 'chg_ls':
      fh.write(f"\tlc_frq: {lcf}, lc_mult: {lcm}\n")
    if dt == 'mline':
      fh.write(f"\tr_keep: {rk}\n")

For the command line parameter, I added a new default variable and updated the argparse code. I will show this further down as there is another command line parameter on the horizon. Don’t want to clutter the post.

And just before plot.show() I added the following:

if sv_plot:
  f_pth = splt.get_save_path('mline', 'png', nbr_c, k_f, cgv)
  print(f_pth)
  splt.sv_dtls(f_pth, nbr_c, sp_rds, k_f, cgv, sp_frq, rcm, 'mline', t_pts, rk=r_keep)
  plt.savefig(f_pth)

Now let’s get back to the business at hand. Let’s start with randomly selecting a line type for each line.

Random Line Styles

I could use the prop_cycle for this. But unlike for the colours, I didn’t want to cycle through the line styles. So, I added another function to spiro_plotlib to randomly select one of the four basic types. In plot(), None is default value for line width and line style. Basically telling plot() to use its defaults. And, I wanted to control when randomly selected line styles were used. So another default value and command line parameter.

While I was at it I added one for controlling use of random line widths.

def get_ln_st(is_fancy=False):
  ln_st = ['-', '--', '-.', ':']
  if is_fancy:
    return np.random.choice(ln_st)
  return None

Okay here’s the current code for the command line parameters and such.

# defaults
# how many of the f(t) lines to plot
r_keep = None
# number of points (t for f(t)) to generate
t_pts = 500
# use randon line styles if True, default otherwise
ls_fancy = True
# use randon line widths if True, default otherwise
lw_fancy = True
# to save or not to save
sv_plot = False

# let's add some command line parameters to specify model/plot parameters
parser = argparse.ArgumentParser(description='Generate spirograph')
parser.add_argument('-p', '--points', action='store', dest='pi_pts',
                    type=int, help="Specify number of points in plot between 0 and 2pi, Examples: -p 500, default 100")
parser.add_argument('-ls', '--lstyle', help="switch state for use of random line styles, default is on", action="store_false")
parser.add_argument('-lw', '--lwide', help="switch state for use of random line widths, default is on", action="store_false")
parser.add_argument('-sf', '--fsave', help="switch save file state, default is off", action="store_true")

args = parser.parse_args()

if args.pi_pts:
  t_pts = args.pi_pts
ls_fancy = args.lstyle
lw_fancy = args.lwide
sv_plot = args.fsave

And, the plot generation code now looks like this:

# now let's give it a try
r_pts = splt.f(t)
for ln in range(2, r_keep[0]+1):
  ln_s = splt.get_ln_st(ls_fancy)
  ln_w = splt.get_ln_wd(lw_fancy)
  ln_alpha = splt.get_alpha()
  print(f"ln: {ln}, width: {ln_w}, alpha: {ln_alpha}")
  plt.plot(np.real(r_pts[-ln:]), np.imag(r_pts[-ln:]), ls=ln_s ,lw=ln_w, alpha=ln_alpha)

I am only going to show one plot generated by the above. I didn’t like most of the first ones I saw.

(ds-3.9) PS R:\learn\py_play\spirograph> python spiro_mln.test.py -sf
get_radii(5) -> [1, 0.7232703382104975j, 0.4342644753479501j, 0.4095818279774402j, 0.37987982101449885j]
get_freqs(nbr_w=5, kf=5, mcg=3) -> [-2, 18, -17, -17, -17]
r_keep: [3]
colour map: plt.cm.hot
ln: 2, style: -, width: 3, alpha: 0.7322620785107308
ln: 3, style: :, width: 3, alpha: 0.7773812805250213
curves_me_like\spiro_4\mline\sp_5_5_3_02110905.png
plot of multiple lines from output of f(t) with random line styles

Not sure random line types really help or make that much difference, but for now I will leave that as the default.

Creating the Figure with Subplots

Now, I want to have the option of drawing the single plot or drawing the multi-plot format. So, I am going to add another command line switch. I will call it '-t', '--test', it will take an integer value. I am not going to show the code changes. But, I am sure you can figure it out. Do note, I had to wrap some previous code in a suitable if statement to prevent execution when running the multi-plot test.

This particular plot is probably of little value. But I was curious and we did do it for the single curve. So…

Let’s start out slow. First decide what’s fixed and what’s changing.

if do_test == 2:
  print(f"Test {do_test}: multi-plot crazy curve figure with fixed and changing frequencies or radii")

  # select what's fixed and changing
  fxd = np.random.choice(['freq', 'radii'])
  chgg = {'freq': 'radii', 'radii': 'freq'}[fxd]

Next we will set up the basics and generate a main title. This is pretty much a repeat of the earlier post where we did this for the basic curve. Bit of work for the title.

p_rw = 4
  p_cl = 2
  # instantiate figure and axes objects, add figure title with metadata and fixed parameter values
  fig, axs = plt.subplots(p_rw, p_cl, figsize=(8,11), squeeze=False, sharex=False, sharey=False)
  plt.axis('off')

  # build figure title
  if fxd == 'radii':
    fx_vals = [f"{rd:.3f}" for rd in sp_rds]
    fx_vals = [rd.replace('0.000+','') for rd in fx_vals]
    fx_vals[0] = fx_vals[0].replace('.000','')

    if len(fx_vals) < 7:
      f_ttl = f"{nbr_c} wheels with {k_f}-fold symmetry ({cgv} mod {k_f}) at {t_pts}, keep {r_keep}\n\n{fxd}: {fx_vals}\n"
    else:
      f_ttl = f"{nbr_c} wheels with {k_f}-fold symmetry ({cgv} mod {k_f}) at {t_pts}, keep {r_keep}\n\n{fxd}: {fx_vals}\n"
  else:
    fx_vals = [f"{fq}" for fq in sp_frq]
    f_ttl = f"{nbr_c} wheels with {k_f}-fold symmetry ({cgv} mod {k_f}) at {t_pts}, keep {r_keep}\n\n{fxd}: {fx_vals}\n"
  fig.suptitle(f_ttl)

Now you may recall I had a function in spiro_plotlib specifically for drawing the subplots of the basic curve. I am going to do the same for multi-line plots. There will be many similarities with that first function. Here’s my current code. Note the parameters to partially control line styles and widths.

def sp_subplt_mline(ax=None, sb_param={'fxd': 'radii', 'rkp': 2, 'lsf': False, 'lwf': False}, **p_kwargs):
  fxd = sb_param['fxd']
  chg_vals = sp_frq
  chgg = 'freqs'
  if fxd != 'radii':
    chgg = 'radii'
    chg_vals = sp_rds
  
  ax.axis('off')
  # set the aspect ratio to square
  ax.set_aspect('equal')
  ax.set_title(sub_ttl(chgg, chg_vals))
  c_pts = f(t)
  for ln in range(1, sb_param['rkp']+1):
    ln_w = get_ln_wd(sb_param['lwf'])
    if ln_w:
      ln_w = min(5, ln_w)
    ln_s = get_ln_st(sb_param['lsf'])
    ax.plot(np.real(c_pts[-ln:]), np.imag(c_pts[-ln:]), lw=ln_w, ls=ln_s, alpha=get_alpha(), **p_kwargs)

  return ax

And finally the code to generate the full plot.

  # generate the subplots
  rw, cl = 0, 0
  for nsb in range(p_rw * p_cl):
    if nsb == 0:
      # use existing curve parameters
      radii = sp_rds
      freqs = sp_frq
    else:
      # get new values for the changing element
      if fxd == 'freqs':
        radii = get_radii(nbr_c)
      else:
        cgv, freqs = get_freqs(nbr_w=nbr_c, kf=k_f, mcg=cgv)

    # set up data for the plotting package
    splt.set_spiro(freqs, radii, t_pts)

    # determine current column
    cl = nsb % p_cl
    # and current row
    if nsb > 0:
      if cl == 0:
        rw += 1

    # pick colour map for this subplot
    rcm, cycle = splt.set_colour_map(axs[rw, cl])

    axs[rw, cl] = splt.sp_subplt_mline(ax=axs[rw, cl], sb_param={'fxd': fxd, 'rkp': r_keep[0], 'lsf': ls_fancy, 'lwf': lw_fancy})

And a sample (again not many I liked). But do be sure to set the number of points to something around 100. Otherwise these small plots get rather crowded and the whole figure takes some time to generate.

(ds-3.9) PS R:\learn\py_play\spirograph> python spiro_mln.test.py -t 2 -p 100 -ls
get_radii(4) -> [1, 0.665456020449529, 0.5855288021060694, 0.3348340949119324j]
get_freqs(nbr_w=4, kf=2, mcg=1) -> [3, -7, 1, -3]
r_keep: [2]
Test 2: multi-plot crazy curve figure with fixed and changing frequencies or radii
8 subplots of mult-lines plots with fixed frequencies and changing radii

I did the above with -p 100 but some trials with -p 140 seemed to produce nicer images. You might want to play with that value.

Done

Think that’s it for this one. A coding exercise but not sure I like the results of the 8 subplot figures. And, still not sure about random line types. Though would definitely stay with random line widths.

For the next one, I am going to try creating a changing line width for the basic plot type. It appears matplotlib can’t do this. Not sure what the Rotae coder used to generate those plots. Maybe I should ask. So, I am going to use a loop for each point in the t sequence.

If you are interested, the code for the two packages and the test module is given in this (unlisted) post, Spirograph VII: Package Code II.