-
HP 7475A Plotter: Coordinate Pruning
The original SuperFormula equation produces points in polar coordinates, which the Chiplotle library converts to the rectilinear format more useful with Cartesian plotters. I’ve been feeding the equation with 10001 angular values (10 passes around the paper, with 1000 points per pass, plus one more point to close the pattern), which means the angle changes by 3600°/10000 = 0.36° per point. Depending on the formula’s randomly chosen parameters, each successive point can move the plotter pen by almost nothing to several inches.
On the “almost nothing” end of the scale, the plotter slows to a crawl while the serial interface struggles to feed the commands. Given that you can’t see the result, why send the commands?
Computing point-to-point distances goes more easily in rectilinear coordinates, so I un-tweaked my polar-modified
superformulafunction to return the points in rectangular coordinates. I’d originally thought a progressive scaling factor would be interesting, but it never happened.The coordinate pruning occurs in the
supershapefunction, which now contains a loop to scan through the incoming list ofpointsfrom thesuperformulafunction and add a point to the outputpathonly when it differs by enough from the most recently output point:path = [] path.append(Coordinate(width * points[0][0], height * points[0][1])) outi = 0 xp, yp = points[outi][0], points[outi][1] for i in range(len(points))[1:]: x,y = width * points[i][0], height * points[i][1] dist = sqrt(pow(x - xp,2) + pow(y - yp,2)) if dist > 60 : path.append(Coordinate(x, y)) outi = i xp, yp = x, y path.append(Coordinate(width * points[-1][0], height * points[-1][1])) print "Pruned",len(points),"to",len(path),"points"The first and last points always go into the output list; the latter might be duplicated, but that doesn’t matter.
Note that you can’t prune the list by comparing successive points, because then you’d jump directly from the start of a series of small motions to their end. The idea is to step through the small motions in larger units that, with a bit of luck, won’t be too ugly.
The
widthandheightvalues scale the XY coordinates to fill either A or B paper sheets, with units of “Plotter Units” = 40.2 PU/mm = 1021 PU/inch. You can scale those in various ways to fit various output sizes within the sheets, but I use the defaults that fill the entire sheets with a reasonable margin. As a result, the magic number60specifies 60 Plotter Units; obviously, it should have a suitable name.Pruning to 40 PU = 1.0 mm (clicky for more dots, festooned with over-compressed JPEG artifacts):

Plot pruned to 40 PU Pruning to 60 PU = 1.5 mm:

Plot pruned to 60 PU Pruning to 80 PU = 2.0 mm:

Plot pruned to 80 PU Pruning to 120 PU = 3.0 mm:

Plot pruned to 120 PU All four of those plots have the same pens in the same order, although I refilled a few of them in flight.
By and large, up through 80 PU there’s not much visual difference, although you can definitely see the 3 mm increments at 120 PU. However, the plotting time drops from just under an hour for each un-pruned plot to maybe 15 minutes with 120 PU pruning, with 60 PU producing very good results at half an hour.
Comparing the length of the input point lists to the pruned output path lists, including some pruning values not shown above:
Prune 20 1 - m: 5.3, n1: 0.15, n2=n3: 0.80 Pruned 10001 to 4856 points 2 - m: 5.3, n1: 0.23, n2=n3: 0.75 Pruned 10001 to 5545 points 3 - m: 5.3, n1: 1.15, n2=n3: 0.44 Pruned 10001 to 6218 points 4 - m: 5.3, n1: 0.41, n2=n3: 1.50 Pruned 10001 to 7669 points 5 - m: 5.3, n1: 0.29, n2=n3: 0.95 Pruned 10001 to 6636 points 6 - m: 5.3, n1: 0.95, n2=n3: 0.16 Pruned 10001 to 5076 points Prune 40 1 - m: 3.1, n1: 0.23, n2=n3: 0.26 Pruned 10001 to 2125 points 2 - m: 3.1, n1: 1.05, n2=n3: 0.44 Pruned 10001 to 5725 points 3 - m: 3.1, n1: 0.25, n2=n3: 0.32 Pruned 10001 to 2678 points 4 - m: 3.1, n1: 0.43, n2=n3: 0.34 Pruned 10001 to 4040 points 5 - m: 3.1, n1: 0.80, n2=n3: 0.40 Pruned 10001 to 5380 points 6 - m: 3.1, n1: 0.55, n2=n3: 0.56 Pruned 10001 to 5424 points Prune 60 1 - m: 1.1, n1: 0.45, n2=n3: 0.40 Pruned 10001 to 2663 points 2 - m: 1.1, n1: 0.41, n2=n3: 0.14 Pruned 10001 to 1706 points 3 - m: 1.1, n1: 1.20, n2=n3: 0.75 Pruned 10001 to 4446 points 4 - m: 1.1, n1: 0.33, n2=n3: 0.80 Pruned 10001 to 3036 points 5 - m: 1.1, n1: 0.90, n2=n3: 1.40 Pruned 10001 to 4723 points 6 - m: 1.1, n1: 0.61, n2=n3: 0.65 Pruned 10001 to 3601 points Prune 80 1 - m: 3.7, n1: 0.95, n2=n3: 0.58 Pruned 10001 to 3688 points 2 - m: 3.7, n1: 0.49, n2=n3: 0.22 Pruned 10001 to 2258 points 3 - m: 3.7, n1: 0.57, n2=n3: 0.90 Pruned 10001 to 3823 points 4 - m: 3.7, n1: 0.25, n2=n3: 0.40 Pruned 10001 to 2161 points 5 - m: 3.7, n1: 0.47, n2=n3: 0.30 Pruned 10001 to 2532 points 6 - m: 3.7, n1: 0.45, n2=n3: 0.14 Pruned 10001 to 1782 points Prune 120 1 - m: 1.9, n1: 0.33, n2=n3: 0.48 Pruned 10001 to 1561 points 2 - m: 1.9, n1: 0.51, n2=n3: 0.18 Pruned 10001 to 1328 points 3 - m: 1.9, n1: 1.80, n2=n3: 0.16 Pruned 10001 to 2328 points 4 - m: 1.9, n1: 0.21, n2=n3: 1.10 Pruned 10001 to 1981 points 5 - m: 1.9, n1: 0.63, n2=n3: 0.24 Pruned 10001 to 1664 points 6 - m: 1.9, n1: 0.45, n2=n3: 0.22 Pruned 10001 to 1290 points
Eyeballometrically, 60 PU pruning halves the number of plotted points, so the average data rate jumps from 9600 b/s to 19.2 kb/s. Zowie!
Most of the pruning occurs near the middle of the patterns, where the pen slows to a crawl. Out near the spiky rim, where the points are few & far between, there’s no pruning at all. Obviously, quantizing a generic plot to 1.5 mm would produce terrible results; in this situation, the SuperFormula produces smooth curves (apart from those spikes) that look just fine.
The Python source code as a GitHub Gist:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters# Adapted from Chiplotle plotter library: # http://cmc.music.columbia.edu/chiplotle/ from chiplotle import * from math import * from datetime import * from time import * from types import * import random def supershape(width, height, m, n1, n2, n3, point_count=10 * 1000, percentage=1.0, a=1.0, b=1.0, travel=None): '''Supershape, generated using the superformula first proposed by Johan Gielis. – `points_count` is the total number of points to compute. – `travel` is the length of the outline drawn in radians. 3.1416 * 2 is a complete cycle. modified to prune short plotter motions – Ed Nisley KE4ZNU – October 2016 ''' travel = travel or (10 * 2 * pi) # compute points… phis = [i * travel / point_count for i in range(1 + int(point_count * percentage))] points = [tools.mathtools.superformula(a, b, m, n1, n2, n3, x) for x in phis] # scale and prune short motions path = [] path.append(Coordinate(width * points[0][0], height * points[0][1])) outi = 0 xp, yp = points[outi][0], points[outi][1] for i in range(len(points))[1:]: x,y = width * points[i][0], height * points[i][1] dist = sqrt(pow(x – xp,2) + pow(y – yp,2)) if dist > 60 : path.append(Coordinate(x, y)) outi = i xp, yp = x, y path.append(Coordinate(width * points[-1][0], height * points[-1][1])) print " Pruned",len(points),"to",len(path),"points" return Path(path) # Run Superformula plots if __name__ == '__main__': override = False plt = instantiate_plotters()[0] # plt.write('IN;') if plt.margins.soft.width < 11000: # A=10365 B=16640 maxplotx = (plt.margins.soft.width / 2) – 100 maxploty = (plt.margins.soft.height / 2) – 150 legendx = maxplotx – 2900 legendy = -(maxploty – 750) tscale = 0.45 numpens = 4 # prime/10 = number of spikes m_values = [n / 10.0 for n in [11, 13, 17, 19, 23]] # ring-ness 0.1 to 2.0, higher is larger n1_values = [ n / 100.0 for n in range(55, 75, 2) + range(80, 120, 5) + range(120, 200, 10)] else: maxplotx = plt.margins.soft.width / 2 maxploty = plt.margins.soft.height / 2 legendx = maxplotx – 3000 legendy = -(maxploty – 900) tscale = 0.45 numpens = 6 m_values = [n / 10.0 for n in [11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59]] # prime/10 = number of spikes # ring-ness 0.1 to 2.0, higher is larger n1_values = [ n / 100.0 for n in range(15, 75, 2) + range(80, 120, 5) + range(120, 200, 10)] print " Max: ({},{})".format(maxplotx, maxploty) # spiky-ness 0.1 to 2.0, higher is spiky-er (mostly) n2_values = [ n / 100.0 for n in range(10, 60, 2) + range(65, 100, 5) + range(110, 200, 10)] plt.write(chr(27) + '.H200:') # set hardware handshake block size plt.set_origin_center() # scale based on B size characters plt.write(hpgl.SI(tscale * 0.285, tscale * 0.375)) # slow speed for those abrupt spikes plt.write(hpgl.VS(10)) while True: # standard loadout has pen 1 = fine black plt.write(hpgl.PA([(legendx, legendy)])) pen = 1 plt.select_pen(pen) plt.write(hpgl.PA([(legendx, legendy)])) plt.write(hpgl.LB("Started " + str(datetime.today()))) if override: m = 4.1 n1_list = [1.15, 0.90, 0.25, 0.59, 0.51, 0.23] n2_list = [0.70, 0.58, 0.32, 0.28, 0.56, 0.26] else: m = random.choice(m_values) n1_list = random.sample(n1_values, numpens) n2_list = random.sample(n2_values, numpens) pen = 1 for n1, n2 in zip(n1_list, n2_list): n3 = n2 print "{0} – m: {1:.1f}, n1: {2:.2f}, n2=n3: {3:.2f}".format(pen, m, n1, n2) plt.select_pen(pen) plt.write(hpgl.PA([(legendx, legendy – 100 * pen)])) plt.write( hpgl.LB("Pen {0}: m={1:.1f} n1={2:.2f} n2=n3={3:.2f}".format(pen, m, n1, n2))) e = supershape(maxplotx, maxploty, m, n1, n2, n3) plt.write(e) pen = pen + 1 if (pen % numpens) else 1 pen = 1 plt.select_pen(pen) plt.write(hpgl.PA([(legendx, legendy – 100 * (numpens + 1))])) plt.write(hpgl.LB("Ended " + str(datetime.today()))) plt.write(hpgl.PA([(legendx, legendy – 100 * (numpens + 2))])) plt.write(hpgl.LB("More at https://softsolder.com/?s=7475a")) plt.select_pen(0) plt.write(hpgl.PA([(-maxplotx,maxploty)])) print "Waiting for plotter… ignore timeout errors!" sleep(40) while NoneType is type(plt.status): sleep(5) print "Load more paper, then …" print " … Press ENTER on the plotter to continue" plt.clear_digitizer() plt.digitize_point() plotstatus = plt.status while (NoneType is type(plotstatus)) or (0 == int(plotstatus) & 0x04): plotstatus = plt.status print "Digitized: " + str(plt.digitized_point)