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
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
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.