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 `superformula`

function 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 `supershape`

function, which now contains a loop to scan through the incoming list of `points`

from the `superformula`

function and add a point to the output `path`

only 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 `width`

and `height`

values 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 number `60`

specifies 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):

Pruning to 60 PU = 1.5 mm:

Pruning to 80 PU = 2.0 mm:

Pruning to 120 PU = 3.0 mm:

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:

# 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) |

It seems to me (and I may well be wrong) that if you compared segment angle as well as distance you could prune more aggressively with less visible artifacts.

Perhaps discarding successive points until one sits “far enough” from the straight line joining the two previous points? The closest point spacing occurs where the trace passes close to the origin and has the tightest curvature, so that might degenerate into what I’m doing now: don’t move until the distance is about one pen diameter. I should dump the point list and run some numbers, rather than guessing.

I tweaked the code while building a stack of plots to hand out during the next Mini Makerfaire: the plots now emerge faster than I can task-switch away from whatever else I’m doing in the Basement Lab, so it’s probably fast enough … [grin]

The most visible artifacts are where it cuts the corner on a spike, so forcing an inclusion of any point where the angle between previous segment and next segment is, say, >90 degrees might work.

Those cut-corner spikes come from the original point list: they show up in unpruned plots, too!

At a 160 mm radius, the 0.36° angular samples fall 1 mm apart along the tangent. A spike that lies between two of the points means the drawn line doesn’t go “all the way out” to the true tip of the spike: there’s a stumpy tip with least one short and weirdly angled line to an adjacent point. The space between the two points around the spike seems larger than it really is.

I just sat down with a ruler and measured a plot, because I didn’t quite believe it, either …

Oh, I that case don’t bother… I was looking at the spikes :)

1021 PU/inch? Your plotter must be printing too small. The officially sanctioned value is 1016, so you get ease of conversion between mm and antediluvian.

All I know is what I read in the ©1983, 1984 HP 7475A Interfacing and Programming Manual:

Perhaps they took advantage of “approximately” to shift the goal post after the metric thing caught on with techies and they stopped spelling “millimetre” that way?

Glad to see the plotter is still working well. They were a workhorse.

Once you have a great pruning algorithm you can increase the number of computed steps to get rid of the sometimes-rounded points, say from 10,000 to 100,000.

I wonder if some of of “thinning” algorithm might make more sense? Take a pass through the data, and assign each point an “importance” based on it’s distance from the midpoint of its two neighbors (note, first and last have no neighbors so are max important). Then remove the one with the lowest value and recalculate its remaining neighbors, and then repeat that until some constant minimum importance threshold has been met (note: you can not just remove anything below the threshold in one pass; the neighbor recalculation is needed). This will keep the points at direction changes. I posit that with a reasonable importance threshold this will thin the data more than the “distance from previous” algorithm yet keep more of the important points like direction changes. Given the massive ratio of your compute power versus communication speed, it doesn’t seem unreasonable to optimize this job more than would have been done when the plotter was made.

Regarding the official PU/inch value: I hazily recall that HP had one standard for most of their plotters but then one of them broke from that tradition, and I seem to recall that was the 7475 (something about it’s lower cost high volume design required the change). So expect a different value from what you might find in other HP documentation. I might have the official manual somewhere. If I run across it during any excavations I will get in touch to see if you need any info from it.

I went through a bunch of HP plotter manuals for another project a couple of years ago (somewhat futile link: https://bugs.launchpad.net/inkscape/+bug/548918/comments/9) and the 7475 manual was the only one that strayed from 40/mm. All output is approximate anyway: ask me about the fun we had making press-accurate laser proofs when the first near-typeset quality (~ 50/mm) Postscript laser printers came out …

Another potential cause of slow plotting speeds is needless precision in the HP/GL sent to the plotter. Plot command arguments are truncated to integers by the plotter’s internal parser. hp2xx — which chiplotle

mayuse as an output filter — adds 5 or 6 pointless decimal places (see what I did there?) to numerical output. This can make your plot files roughly twice the size they need to be.It. Just. Works.

Which is what one expects of classic HP gear, like my scope and spectrum analyzer. Somewhere along the timeline from then until now, HP lost that recipe.

The Python code pre-calculates the whole path, then dribbles the results point-by-point to the plotter: AFAICT, the entire calculation takes less time than the pen change between paths!

I’ve been using copies of the HP manuals from the unofficial HP Museum, which are Good Enough for my simple needs despite a few blurred pages.