├── Harmonographs ├── harmonograph.jpeg ├── harmonograph-2.jpeg ├── harmonograph-3.jpeg ├── harmonograph13.jpeg ├── harmonograph15.jpeg ├── harmonograph16.jpeg ├── harmonograph17.jpeg └── harmonograph19.jpeg ├── LICENSE.txt ├── random-harmonograph.py ├── README.md └── gui-harmonograph.py /Harmonographs/harmonograph.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxar-uk/Harmonumpyplot/HEAD/Harmonographs/harmonograph.jpeg -------------------------------------------------------------------------------- /Harmonographs/harmonograph-2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxar-uk/Harmonumpyplot/HEAD/Harmonographs/harmonograph-2.jpeg -------------------------------------------------------------------------------- /Harmonographs/harmonograph-3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxar-uk/Harmonumpyplot/HEAD/Harmonographs/harmonograph-3.jpeg -------------------------------------------------------------------------------- /Harmonographs/harmonograph13.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxar-uk/Harmonumpyplot/HEAD/Harmonographs/harmonograph13.jpeg -------------------------------------------------------------------------------- /Harmonographs/harmonograph15.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxar-uk/Harmonumpyplot/HEAD/Harmonographs/harmonograph15.jpeg -------------------------------------------------------------------------------- /Harmonographs/harmonograph16.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxar-uk/Harmonumpyplot/HEAD/Harmonographs/harmonograph16.jpeg -------------------------------------------------------------------------------- /Harmonographs/harmonograph17.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxar-uk/Harmonumpyplot/HEAD/Harmonographs/harmonograph17.jpeg -------------------------------------------------------------------------------- /Harmonographs/harmonograph19.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuxar-uk/Harmonumpyplot/HEAD/Harmonographs/harmonograph19.jpeg -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alan Richmond @ AILinux.net 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /random-harmonograph.py: -------------------------------------------------------------------------------- 1 | """ Multi-pendulum random Harmonograph simulator using numpy and matplotlib 2 | 3 | You can specify any number of pendulums npend > 0; this number also sets 4 | the number of frequencies available. The sine wave parameters are 5 | a: amplitude, a random float in the range 0 to 1; 6 | f: frequency, a random near-integer in the range 1 to npend 7 | p: phase, a random float in the range 0 to 2pi 8 | 9 | Copyright 2017 Alan Richmond @ Python3.codes 10 | The MIT License https://opensource.org/licenses/MIT 11 | """ 12 | import random as r 13 | import matplotlib.pyplot as plt 14 | from numpy import arange, sin, cos, exp, pi 15 | plt.rcParams["figure.figsize"] = 8,6 # size of plot in inches 16 | 17 | mf = npend = 4 # # of pendulums & maximum frequency 18 | sigma = 0.005 # frequency spread (from integer) 19 | step = 0.01 # step size 20 | steps = 40000 # # of steps 21 | linew = 2 # line width 22 | def xprint(name, value): # convenience function to print params. 23 | print(name+' '.join(['%.4f' % x for x in value])) 24 | 25 | t = arange(steps)*step # time axis 26 | d = 1 - arange(steps)/steps # decay vector 27 | while True: 28 | n = input("Number of pendulums (%d)(0=exit): "%npend) 29 | if n != '': npend = int(n) 30 | if npend == 0: break 31 | n = input("Deviation from integer freq.(%f): "%sigma) 32 | if n != '': sigma = float(n) 33 | ax = [r.uniform(0, 1) for i in range(npend)] 34 | ay = [r.uniform(0, 1) for i in range(npend)] 35 | px = [r.uniform(0, 2*pi) for i in range(npend)] 36 | py = [r.uniform(0, 2*pi) for i in range(npend)] 37 | fx = [r.randint(1, mf) + r.gauss(0, sigma) for i in range(npend)] 38 | fy = [r.randint(1, mf) + r.gauss(0, sigma) for i in range(npend)] 39 | xprint('ax = ', ax); xprint('fx = ', fx); xprint('px = ', px) 40 | xprint('ay = ', ay); xprint('fy = ', fy); xprint('py = ', py) 41 | x = y = 0 42 | for i in range(npend): 43 | x += d * (ax[i] * sin(t * fx[i] + px[i])) 44 | y += d * (ay[i] * sin(t * fy[i] + py[i])) 45 | plt.figure(facecolor = 'white') 46 | plt.plot(x, y, 'k', linewidth=1.5) 47 | plt.axis('off') 48 | plt.subplots_adjust(left=0.0, right=1.0, top=1.0, bottom=0.0) 49 | plt.show(block=False) 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Harmonograph simulators using numpy and matplotlib 2 | 3 | There are 2 programs here. The first is a simple bare-bones program that gets a couple of parameters from the user and draws a random harmonograph. The second is a full-fledged GUI with buttons and sliders to allow the creation of user-specified harmonographs, or random ones that can be used as a starting point for user adjustments. They are both Python 3 - do NOT use Python 2. 4 | 5 | ![harmonograph](Harmonographs/harmonograph.jpeg) 6 | 7 | * * * 8 | ## Basic Principles 9 | 10 | Harmonographs are mechanical devices, frequently seen in science museums, comprising a small number of pendulums which are inter-connected in such a way as to control the movement of a pen, which rests on a sheet of paper. Their motion causes intricate patterns to be drawn on the paper - which may also be moved by its own pendulum. 11 | 12 | The movement imparted by each pendulum is basically damped simple harmonic motion x = a * sin (t * f + p) * exp(-d * t), where a, f, and p are the amplitude, frequency, and phase of the sine wave. t is time, and d is a damping factor due to friction and air resistance. An orthogonal pendulum can add motion in the y axis, with different values of a, f, and p (d could also be different but we'll ignore that). We can add more pendulums for more sophisticated drawings, but in practice, 2 or 3 pendulums works very well. 13 | 14 | This device is very easily simulated in a program, especially if we take advantage of Python's numpy and matplotlib libraries. 15 | 16 | ## random-harmonograph.py 17 | 18 | This is a simple harmonograph simulator to generate random-ish harmonographs. It asks for the number of pendulums, and exits if the number is 0\. It also asks for the frequency spread, which means roughly, how far from integer may the frequencies go. The nearer to integer they are, the 'cleaner' looking the harmonographs are, but they tend to be perhaps less interesting. The further from integer they are, the more they're likely to look messy. 19 | 20 | If you've seen harmonograph programs before, you might expect there to be an outer loop for stepping through the 'time' variable. That's not necessary here because numpy handles it for us, making the program simpler and much faster. The outer loop here is gets user input and generates random parameters for each pendulum's oscillation (i.e. sine wave): amplitude, frequency, and phase. The one inner loop computes, for each pendulum, the whole x and y vectors, at once. That is, numpy notices that 't' is a vector and so knows to compute x and y using faster code than Python (C). 21 | 22 | Each picture is drawn in its own window, which remains on screen until the program exits, or you dismiss it. This way, you can compare several pictures and select the best if you want to keep some. To save a picture, click on the floppy disc icon on the window's menu bar. 23 | 24 | ## gui-harmonograph.py 25 | 26 | Based on the previous one, but now with GUI radio buttons for selecting one of the pendulums, sliders for setting pendulum sine wave properties, i.e. amplitude, frequency, and phase. There are also sliders for setting the decay rate, and number of steps. You can also specify thin/thick line width, and create a random harmonograph, which you can then modify with the sliders. 27 | 28 | Again, you can save pictures using the floppy-disc save icon. 29 | 30 | ### Some Tips for Getting Good Pictures 31 | 32 | You probably started by clicking the Random button a few times, and maybe moving the sliders around. Good! I hope you figured out that the 'radio buttons' on the left select which pendulum is controlled by the sliders. When you have done that, exit the program and restart it. 33 | 34 | The drawing you now see is the simplest harmonograph: the same frequencies in x and y on one pendulum, with phases selected to cause circular or elliptical motion when the damping is zero; otherwise the motion slowly decays, and the pen spirals inwards. If the damping is maximal, the pen spirals completely in to a central point. The decay isn't the usual exponential, I find that a linear damping gives better results, e.g. avoiding a black muddle in the middle. 35 | 36 | Now use the arrow keys to vary the x or y frequencies very slowly (if nothing happes, click on the Harmonograph Controls box). These have the greatest effect on the character of the picture. Normally, you would set amplitude to 0 or 1. If the amplitude is 0, the other controls for that pendulum, in that direction (x or y) will have no effect. The most pleasing pictures occur near small integer multiple ratios, e.g. 1:1, 1:2, 1:3, 2:3, 3:4. This doesn't necessarily mean the frequencies themselves have to be integers, but their ratio's numerator and denominator do. However, the easiest way to do that is when at least one of them is (near) an integer. 37 | 38 | The picture below was obtained by starting with the x and y frequencies the same for pendulum 1, and then hitting the up arrow key several times. You should increase the number of Steps to fill it out a bit. 39 | 40 | ![harmonograph](Harmonographs/harmonograph-3.jpeg) 41 | 42 | When you have found something interesting by varying the frequencies, you can then try adjusting their phases. The effect is a bit like rotating something, and indeed, if you reach one end of the phase range, and then quickly move the slider to the opposite end, you'll find that the harmonograph is the same - as if you had rotated it 360 degrees! So this is a good way to get the best view of the harmonograph. 43 | 44 | When you have played around with just 1 pendulum, I suggest you set the frequencies back to 1:1, so that you have again the circle or ellipse. Now click on pendulum 2, and press the up arrow several times. Eventually you'll see a very nice harmonograph. And again, try varying phase, damping, and steps... 45 | 46 | * * * 47 | Copyright 2017 Alan Richmond @ Python3.codes https://opensource.org/licenses/MIT 48 | -------------------------------------------------------------------------------- /gui-harmonograph.py: -------------------------------------------------------------------------------- 1 | """ Multi-pendulum Harmonograph simulator with GUI, using numpy and matplotlib 2 | 3 | You can specify any number of pendulums npend > 0; this number also sets 4 | the number of frequencies available. The sine wave parameters are 5 | a: amplitude, a random float in the range 0 to 1; 6 | f: frequency, a random near-integer in the range 1 to npend 7 | p: phase, a random float in the range 0 to 2pi 8 | 9 | Copyright 2017 Alan Richmond @ Python3.codes 10 | The MIT License https://opensource.org/licenses/MIT 11 | """ 12 | from numpy import arange, sin, pi 13 | import random as r 14 | import matplotlib.pyplot as plt 15 | from matplotlib.widgets import Slider, RadioButtons, Button 16 | 17 | plt.rcParams["figure.figsize"] = 12.8, 9.6 # size of plot in inches 18 | mf = npend = 4 # # of pendulums & maximum frequency 19 | sigma = 0.005 # frequency spread (from integer) 20 | step = 0.01 # step size 21 | steps = 50000 # # of steps 22 | linew = 1 # line width 23 | delta = 0.001 # frequency fine tuning with arrows 24 | print("Arrow keys for frequency fine tuning: x = left/right, y = up/down") 25 | 26 | t = arange(steps)*step # time vector 27 | d = 1 - 0.5* arange(steps)/steps # decay vector 28 | ax = [0.0 for i in range(npend)] # x amplitude 29 | fx = [i+1 for i in range(npend)] # x frequency 30 | px = [0.0 for i in range(npend)] # x phase 31 | ay = [0.0 for i in range(npend)] # y amplitude 32 | fy = [i+1 for i in range(npend)] # y frequency 33 | py = [pi/2 for i in range(npend)] # y phase 34 | ax[0] = 1.0 35 | ay[0] = 1.0 36 | x = y = 0 37 | for i in range(npend): # initial plot 38 | x += d * ax[i] * sin(t * fx[i] + px[i]) 39 | y += d * ay[i] * sin(t * fy[i] + py[i]) 40 | 41 | # PLOT area for harmonogram 42 | f1 = plt.figure(1, facecolor = 'white') 43 | f1.canvas.set_window_title('Harmonograph') 44 | ax1 = f1.add_subplot(111) 45 | plt.axis('off') 46 | plt.subplots_adjust(left=0.1, right=0.9, top=0.95, bottom=0.05) 47 | l, = plt.plot(x, y, 'k', lw=linew) 48 | 49 | # CONTROLS 50 | 51 | plt.rcParams["figure.figsize"] = 16,2 52 | f2 = plt.figure(2) 53 | f2.canvas.set_window_title('Harmonograph Controls') 54 | ax2 = f2.add_subplot(111) 55 | plt.axis('off') 56 | plt.subplots_adjust(left=0.0, right=1.0, top=1.0, bottom=0.0) 57 | 58 | def xprint(name, value): # convenience function to print params. 59 | print(name+' '.join(['%.3f' % x for x in value])) 60 | 61 | # Radio buttons for pendulum selector 62 | pend = plt.axes([0.0, 0.0, 0.1, 0.8]) 63 | radio = RadioButtons(pend, ('1', '2', '3', '4')) 64 | 65 | p = 0 66 | update = True 67 | def pensel(label): # Pendulum selector callback 68 | global p, update 69 | pendict = {'1': 0, '2': 1, '3': 2, '4': 3} 70 | p = pendict[label] 71 | update = False 72 | Xsa.set_val(ax[p]) 73 | Xsf.set_val(fx[p]) 74 | Xsp.set_val(px[p]) 75 | Ysa.set_val(ay[p]) 76 | Ysf.set_val(fy[p]) 77 | Ysp.set_val(py[p]) 78 | update = True 79 | 80 | radio.on_clicked(pensel) 81 | 82 | # Width toggle 83 | def Wupdate(val): # Width toggle callback 84 | global l, linew 85 | linew = 3 - linew # toggles between 1 & 2 86 | plt.setp(l, lw=linew) 87 | f1.canvas.draw_idle() 88 | 89 | # Button for Width toggle button 90 | width = plt.axes([0.0, 0.8, 0.1, 0.1]) 91 | Width = Button(width, ('Thin/Thick')) 92 | Width.on_clicked(Wupdate) 93 | 94 | # 'Random' callback 95 | def Rupdate(val): # Random button 96 | global l, update, ax, fx, px, ay, fy, py 97 | update = False 98 | for i in range(npend): 99 | ax[i] = r.uniform(0, 1) 100 | ay[i] = r.uniform(0, 1) 101 | px[i] = r.uniform(0, 2*pi) 102 | py[i] = r.uniform(0, 2*pi) 103 | fx[i] = r.randint(1, mf) + r.gauss(0, sigma) 104 | fy[i] = r.randint(1, mf) + r.gauss(0, sigma) 105 | Xsa.set_val(ax[i]) 106 | Xsf.set_val(fx[i]) 107 | Xsp.set_val(px[i]) 108 | Ysa.set_val(ay[i]) 109 | Ysf.set_val(fy[i]) 110 | Ysp.set_val(py[i]) 111 | # kludge: above has set display of sines to last one computed; 112 | # need to repeat for current pendulum to get correct values... 113 | Xsa.set_val(ax[p]) 114 | Xsf.set_val(fx[p]) 115 | Xsp.set_val(px[p]) 116 | Ysa.set_val(ay[p]) 117 | Ysf.set_val(fy[p]) 118 | Ysp.set_val(py[p]) 119 | update = True 120 | # print('Rupdate') 121 | # xprint('ax = ', ax); xprint('fx = ', fx); xprint('px = ', px) 122 | # xprint('ay = ', ay); xprint('fy = ', fy); xprint('py = ', py) 123 | x = y = 0 124 | for i in range(npend): 125 | x += d * (ax[i] * sin(t * fx[i] + px[i])) 126 | y += d * (ay[i] * sin(t * fy[i] + py[i])) 127 | l.set_xdata(x) 128 | l.set_ydata(y) 129 | ax1.relim() 130 | ax1.autoscale_view(True,True,True) 131 | f1.canvas.draw_idle() 132 | 133 | # Random button 134 | rand = plt.axes([0.0, 0.9, 0.1, 0.1]) 135 | Rand = Button(rand, ('Random')) 136 | Rand.on_clicked(Rupdate) 137 | 138 | # Steps controller 139 | def Supdate(val): # Steps slider callback 140 | global l, d, steps, t 141 | steps = Stps.val 142 | d = 1 - arange(steps)*Dec.val/steps # decay vector 143 | t = arange(steps)*step # time vector 144 | x = y = 0 145 | for i in range(npend): 146 | x += d * ax[i] * sin(t * fx[i] + px[i]) 147 | y += d * ay[i] * sin(t * fy[i] + py[i]) 148 | l.set_xdata(x) 149 | l.set_ydata(y) 150 | ax1.relim() 151 | ax1.autoscale_view(True,True,True) 152 | f1.canvas.draw_idle() 153 | 154 | # Slider for line length (steps) 155 | stps = plt.axes([0.2, 0.02, 0.73, 0.1]) 156 | Stps = Slider(stps, 'Steps (all)', 0.0, 200000, valinit=steps, valfmt="%d") 157 | Stps.on_changed(Supdate) 158 | 159 | # Decay controller 160 | def Dupdate(val): # Decay slider callback 161 | global l, d 162 | d = 1 - arange(steps)*Dec.val/steps # decay vector 163 | x = 0 164 | y = 0 165 | for i in range(npend): 166 | x += d * ax[i] * sin(t * fx[i] + px[i]) 167 | y += d * ay[i] * sin(t * fy[i] + py[i]) 168 | l.set_xdata(x) 169 | l.set_ydata(y) 170 | ax1.relim() 171 | ax1.autoscale_view(True,True,True) 172 | f1.canvas.draw_idle() 173 | 174 | # Slider for decay rate 175 | dec = plt.axes([0.2, 1/8+.02, 0.73, 0.1]) 176 | Dec = Slider(dec, 'Decay (all)', 0.0, 1.0, valinit=0.5) 177 | Dec.on_changed(Dupdate) 178 | 179 | # X parameters amplitude, frequency, phase 180 | def Xupdate(val): # X sliders 181 | global l, ax, fx, px 182 | if not update: return 183 | ax[p] = Xsa.val 184 | fx[p] = Xsf.val 185 | px[p] = Xsp.val 186 | x = 0 187 | for i in range(npend): 188 | x += d * ax[i] * sin(t * fx[i] + px[i]) 189 | l.set_xdata(x) 190 | ax1.relim() 191 | ax1.autoscale_view(True,True,True) 192 | f1.canvas.draw_idle() 193 | 194 | def Yupdate(val): # Y sliders 195 | global l, ay, fy, py 196 | if not update: return 197 | ay[p] = Ysa.val 198 | fy[p] = Ysf.val 199 | py[p] = Ysp.val 200 | y = 0 201 | for i in range(npend): 202 | y += d * ay[i] * sin(t * fy[i] + py[i]) 203 | l.set_ydata(y) 204 | ax1.relim() 205 | ax1.autoscale_view(True,True,True) 206 | f1.canvas.draw_idle() 207 | 208 | def getkey(event): 209 | global fx, fy 210 | if event.key == 'left': 211 | fx[p] -= delta 212 | elif event.key == 'right': 213 | fx[p] += delta 214 | elif event.key == 'up': 215 | fy[p] += delta 216 | elif event.key == 'down': 217 | fy[p] -= delta 218 | else: return 219 | xprint('fx = ', fx); xprint('fy = ', fy) 220 | update = False 221 | Xsf.set_val(fx[p]) 222 | Ysf.set_val(fy[p]) 223 | update = True 224 | x = 0 225 | y = 0 226 | for i in range(npend): 227 | x += d * ax[i] * sin(t * fx[i] + px[i]) 228 | y += d * ay[i] * sin(t * fy[i] + py[i]) 229 | l.set_xdata(x) 230 | l.set_ydata(y) 231 | ax1.relim() 232 | ax1.autoscale_view(True,True,True) 233 | f1.canvas.draw_idle() 234 | 235 | plt.connect('key_press_event', getkey) 236 | 237 | # Sliders for sine wave parameters (amplitude etc), for x & y 238 | axa = plt.axes([0.2, 2/8+.02, 0.73, 0.1]) 239 | axf = plt.axes([0.2, 3/8+.02, 0.73, 0.1]) 240 | axp = plt.axes([0.2, 4/8+.02, 0.73, 0.1]) 241 | Xsa = Slider(axa, 'X Amplitude', 0.0, 1.0, valinit=ax[p], valfmt='%1.3f') 242 | Xsf = Slider(axf, 'X Frequency', 0.0, mf, valinit=fx[p], valfmt='%1.3f') 243 | Xsp = Slider(axp, 'X Phase ', 0.0, 2*pi, valinit=px[p], valfmt='%1.3f') 244 | 245 | aya = plt.axes([0.2, 5/8+.02, 0.73, 0.1]) 246 | ayf = plt.axes([0.2, 6/8+.02, 0.73, 0.1]) 247 | ayp = plt.axes([0.2, 7/8+.02, 0.73, 0.1]) 248 | Ysa = Slider(aya, 'Y Amplitude', 0.0, 1.0, valinit=ay[p], valfmt='%1.3f') 249 | Ysf = Slider(ayf, 'Y Frequency', 0.0, mf, valinit=fy[p], valfmt='%1.3f') 250 | Ysp = Slider(ayp, 'Y Phase ', 0.0, 2*pi, valinit=py[p], valfmt='%1.3f') 251 | 252 | Xsf.on_changed(Xupdate) 253 | Xsa.on_changed(Xupdate) 254 | Xsp.on_changed(Xupdate) 255 | 256 | Ysf.on_changed(Yupdate) 257 | Ysa.on_changed(Yupdate) 258 | Ysp.on_changed(Yupdate) 259 | 260 | plt.show() 261 | --------------------------------------------------------------------------------