Close

Synchronization of coordinates

A project log for Polargraph type hot wire foam cutter

Two polargraph type drawing robots with a hot wire instead of a pen to cut 3D shapes out of foam

adamAdam 06/01/2021 at 20:300 Comments

My previous turbine 3D model was quite large. Let’s only take the first four layers of the GCODE to help understand the coordinate synchronization algorithm.

Here is an algorithm to find the line going from a point on layer one to a point on layer 4 that minimizes the distance from the line to the surface of the model. It does this by finding the minimum mean square distance between between the line and coordinates on layer 2 and layer 3.

Here are the first four layers in our GCODE. The red points are examples of coordinates that should be synchronized.

The synchronization program will:

    1. Read in the GCODE file.
    2. Remove everything except the coordinates.
    3. Separate each layer into its own list. P1 and P2 are the top and bottom perimeters. The interior perimeters will be called iP1 through iPN from top to bottom
    4. Starting with the first location in the gcode for both P1[1] and P2[1] (in this example, let’s suppose we only have one part), find the distance of each point on iP1 to the line.
    5. Store the closest point to the line as C-iPN (closest interior perimeter point).
    6. Repeat step 4 and step 5 for every iPN (interior layers: iP1, iP2,…,iPN), storing the closest point on each iP to the line created in step 4.
    7. Calculate the sum of the distances of C-iP’s to the line. Store this number as the objective function.
    8. Remain on the same point on P1 or P1[1], but now draw a line to the next coordinate on P2 or P2[2].  (OBJ[1,1]= the objective function for P1[1] to P2[1], OBJ[1,2]=P1[1] to P2[2])
    9. Repeat step 4 through 8 for every coordinate in P2[1 through n].
    10. Find the lowest OBJ[1,j]; that is the P2[j] coordinate that should be synchronized with P1[1].
    11. For every point on P1[1 through N], repeat step 4-10.

You can download the GCODE file for the turbine blade at this link: 3DHotwire/Turbine_blade-all_GCODE.txt.gcode

Here is the synchronization code written in Python. I’m not a professional programmer, so excuse the mess. It works!!   …kinda… and it is very slow since it is checking every line created by the combination of coordinates from P1[1-N] to P2[1-N], and finding the distance from every coordinate to the every line on every interior layer. To speed things up, I only used one out of every 20 interior layers.

import re
from tkinter import Tk
from tkinter.filedialog import askopenfilename
import time

Tk().withdraw() filename = askopenfilename()#prompt user to select the gcode file


i=1
coords= []
coordinates = []
with open(filename) as gcode:
    for line in gcode:        line = line.strip()        coord = re.findall(r'[XY].?\d+.\d+', line)        if coord:            a = coord[0]            b = coord[1]            aa = float(a[1:])            bb = float(b[1:])            c = [aa,bb,i]            coords.append(c)                    elif "layer_num" in line:            coordinates.append(coords)            coords = []            i=i+1

####calculate the shortest distance between a point and line(defined by two points)
import numpy as np
P1 = coordinates[1] #first layer is not coordinates[0] as that is blank
P2 = coordinates[len(coordinates)-1]#last layer
j = 0 #top profile coordinate iterator
k = 0 #bottom profile iterator
l = 2 #interior layer iterator. Starts at 2 because first layer is empty [], and the first layer is P1
m = 0 #interior coordinate iterator
min_dist = 99999999.9 #some large distance for each point in a layer
sum_min_dist = 0
lowest_dist = 9999999.9#some large distance for total of layer's minimum distance

start = time.time()

def distance(p, q, rs):    x = p-q    return np.linalg.norm(        np.outer(np.dot(rs-q, x)/np.dot(x, x), x)+q-rs,        axis=1)

for sets in P1:    for points in P2:        while l < len(coordinates)-1: #don't count the first layer as it is blank [] and don't use P1, start at coordinates[2] to last coordinate -1            while m < len(coordinates[l]):                p = np.array(P1[j]) #coord of top profile                q = np.array(P2[k]) #coord of bottom profile                rs = np.array(coordinates[l][m])#interior layer at the m-th point                d = distance(p, q, rs)
                if min_dist > d: #first time around min_dist should be large number, so this will be true                    min_dist = d #distance to the closest coordinate [m] on a layer [l]                    #print("min_dist " +str(min_dist))                m = m + 1 #next coordinate on the same layer [l]                #end result is min_dist is equal to the distance to the closest coordinate            sum_min_dist = sum_min_dist + min_dist #sum of the closest coordinate distances on each layer             l = l +20 #next layer. You can skip layers by making l+ some number of skipped layers            min_dist = 9999999.9 #initialize at some large distance            m = 0            #which coordinate on P2 has the lowest sum_min_dist?        #print(sum_min_dist)        if sum_min_dist < lowest_dist: #if the sum of the closest distances for all layers is less than            lowest_dist = sum_min_dist #the previous lowest sum of closest distance, then it may be a synch point            synch_coord = P2[k] #so keep that new lowest distance and coordinate in memory
        sum_min_dist = 0 #reset the sum of the closest coordinates and try the next coordinate on P2
        k = k + 1 #try the next coordinate on P2        l = 2    print(str(P1[j]) +" and " +str(synch_coord))    end = time.time()    print(end - start)    j = j + 1 #next coordinate on P1    lowest_dist = 9999999.9 #some large number    k = 0

I was given the advice to use Anaconda3 for python compiler, write the code in a series of separate functions for easier parallelization, and try the “Numba” just in time compiler. It uses the “@jit” decorator. Whatever that means… Anyway, here is that code. Unfortunately, it runs slower than it did without Numba (68.15 sec per synchronized coordinate without Numba, and 84.65 sec with Numba).

import re
from tkinter import Tk
from tkinter.filedialog import askopenfilename
import time
import numpy as np
from numba import jit

Tk().withdraw() filename = askopenfilename()#prompt user to select the gcode file


i=1
coords= []
coordinates = []
with open(filename) as gcode:
    for line in gcode:        line = line.strip()        coord = re.findall(r'[XY].?\d+.\d+', line)        if coord:            a = coord[0]            b = coord[1]            aa = float(a[1:])            bb = float(b[1:])            c = [aa,bb,i]            coords.append(c)                    elif "layer_num" in line:            coordinates.append(coords)            coords = []            i=i+1

####calculate the shortest distance between a point and line(defined by two points)

P1 = coordinates[1] #first layer is not coordinates[0] as that is blank
P2 = coordinates[len(coordinates)-1]#last layer


start = time.time()

@jit
def distance(p, q, rs):    x = p-q    return np.linalg.norm(        np.outer(np.dot(rs-q, x)/np.dot(x, x), x)+q-rs,        axis=1)
@jit   def loops3(l,j,k):    m = 0 #interior coordinate iterator    min_dist = 99999999.9 #some large distance for each point in a layer
    while m < len(coordinates[l]):            p = np.array(P1[j]) #coord of top profile            q = np.array(P2[k]) #coord of bottom profile            rs = np.array(coordinates[l][m])#interior layer at the m-th point            d = distance(p, q, rs)
            if min_dist > d: #first time around min_dist should be large number, so this will be true                min_dist = d #distance to the closest coordinate [m] on a layer [l]                #print("min_dist " +str(min_dist))            m = m + 1 #next coordinate on the same layer [l]    return(min_dist)    @jit
def loops2(j,k):    l = 2 #interior layer iterator. Starts at 2 because first layer is empty [], and the first layer is P1    sum_min_dist = 0    while l < len(coordinates)-1: #don't count the first layer as it is blank [] and don't use P1, start at coordinates[2] to last coordinate -1        #end result is min_dist is equal to the distance to the closest coordinate        min_dist = loops3(l,j,k)        sum_min_dist= sum_min_dist + min_dist #sum of the closest coordinate distances on each layer        l = l +20 #next layer. You can skip layers by making l+ some number of skipped layers    return(sum_min_dist)

@jit    def loops(j):    k = 0 #bottom profile iterator    lowest_dist = 9999999.9#some large distance for total of layer's minimum distance        while k < len(P2):        synchro_dist=loops2(j,k)        if synchro_dist < lowest_dist: #if the sum of the closest distances for all layers is less than            lowest_dist = synchro_dist #the previous lowest sum of closest distance, then it may be a synch point            synch_coord = P2[k] #current lowest coordinate on P2 through k coordinates on P2        k = k + 1 #try the next coordinate on P2    return(synch_coord)        j = 0 #top profile coordinate iterator    while j < len(P1):    synchro_coords = loops(j)    print(str(P1[j]) +str(synchro_coords))    j = j + 1        end = time.time()    print(end - start)

I came up with a new algorithm that runs faster! It moves along both P1 and P2 depending on which movement has a better minimum synchronization distance.

  1. I updated the code to use the long algorithm for the first coordinate only, i.e., check P1[1] versus all P2[1 through n], to find the first set of synchronized coordinates.
  2. The P2 list is reordered starting from the synchronized coordinate.
  3. We check the synchronization distance for three cases: the next point on P2 to the next point on P1, the next point on P1 to the current point on P2, and the next point on P2 to the current point on P1. Whichever has the best synchronization distance, use that combination as a synchronization coordinate.

I had a problem with the algorithm getting stuck in a local minimum, and the same coordinate would keep repeating. In an attempt to fix this, I added code to check if the last three coordinates on either P1 or P2 are repeats. If repeats on P1, then step to the next one or two on P1 (depending on which gives you the better synchronization distance), and vice versa for repeats on P2.

#Read in P2 to a new P2* starting at location k
n=0
PP2=[]

lP = len(P2)-kopt #
#print(lP)

while n < lP:    PP2.append(P2[kopt+n])    n=n+1
n=0
while n < kopt:    PP2.append(P2[n])    n=n+1
P2=PP2    Q=0
R=1
w=[1,2,3] #w tells you how many times you've stepped the P1 side in a row
x=[1,2,3] #x tells you how many times you've stepped the P2 side in a row

U=[]

while Q<len(P1)-2:    while R<len(P2)-1:
        T=loops2(Q,R+1) #iterate along P2        U=loops2(Q+1,R) #iterate along P1        V=loops2(Q+1,R+1)#iterate along both P2 and P1        W=loops2(Q,R+2)        X=loops2(Q+2,R)                if x[0]==x[1]==x[2]: #check if your last four points on P2 are the same            T=loops2(Q,R+1)            W=loops2(Q,R+2)            if T < W:                x.pop(0)                x.append(P2[R+1])                w.pop(0)                w.append(P1[Q])                file.write((str(P1[Q]) +" ; " +str(P2[R+1]) +'\n'))                R = R+1

            else:                x.pop(0)                x.append(P2[R+2])                w.pop(0)                w.append(P1[Q])                file.write((str(P1[Q]) +" ; " +str(P2[R+2]) +'\n'))                R = R+2
        elif w[0]==w[1]==w[2]:#check if your last four points on P1 are the same            X=loops2(Q+2,R)            U=loops2(Q+1,R)            if U<X:                x.pop(0)                x.append(P2[R])                w.pop(0)                w.append(P1[Q+1])                file.write((str(P1[Q+1]) +" ; " +str(P2[R])+'\n'))                Q = Q+1
            else:                x.pop(0)                x.append(P2[R])                w.pop(0)                w.append(P1[Q+2])                file.write((str(P1[Q+2]) +" ; " +str(P2[R])+'\n'))                Q = Q+2                                else:            if U<V and U<T:                x.pop(0)                x.append(P2[R])                w.pop(0)                w.append(P1[Q+1])                file.write((str(P1[Q+1]) +" ; " +str(P2[R])+'\n'))                Q=Q+1
                            elif T<U and T<V:                x.pop(0)                x.append(P2[R+1])                w.pop(0)                w.append(P1[Q])                file.write((str(P1[Q]) +" ; " +str(P2[R+1])+'\n'))                R=R+1
            else:                x.pop(0)                x.append(P2[R+1])                w.pop(0)                w.append(P1[Q+1])                file.write((str(P1[Q+1]) +" ; " +str(P2[R+1])+'\n'))                Q = Q+1                R = R+1

So how did it do? Here is a slice of the back end of a Lamborghini Gallardo, and the GCODE used to find synchronization coordinates.

You can see the coordinates that have been synchronized in the picture below. Left (P1) and Right (P2) start at slightly different locations. At some points, the coordinates look fairly well synchronized, but further on, they are way out of synch. Sometimes P1 is too far along its path, and sometimes it’s P2.

It would be much faster to do a local search of coordinates since each added coordinate results in an exponential increase in calculations. In the end, I leave this up to the community in case someone wants to improve the program. Please share if you do. 

I ended up with a lot of cut pieces of foam that was taking up my garage because recycling doesn't take them and garbage cost a lot in Beaverton Oregon (where I lived at the time). It was too much to handle and got expensive, so I stopped the project. Recycling (or better yet repairing the cut foam) is probably a better project than this.

Some other notes if you embark on making your own foam cutter... since the wire would sometimes cut at a steep angle, the amount of wire contacting the foam would increase until it would be cooled by the foam so much it would get stuck. If you increase the temperature, you end up with a large kerf. The solution was to use a DC-DC power supply that maintained the same voltage (which would drop as it cooled). Seemed to do the trick.

Also, I ended up buying 11' x 8' poster hanging frames and mounted some unistrut channels and mounted the stepper motors on slides, so it could cut the full 8' long sheets, and I could vary the thickness to several inches by adjusting a few bolts. I can post that information if requested, but it's been taken apart, so I'll wait.

Discussions