Foam Cutter Part III: Synchronization Software

In my blog post about cutting a turbine, I manually extracted the coordinates from the GCODE file, deleted extra coordinates, and moved rows around in Excel in order to synchronize the coordinates. This was laborious time consuming work, and it would be great if there was a program that automated this. This post is a work in progress hoping to develop an algorithm to do just that. Here it is on Github: https://github.com/agb253/3DHotwire

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.

 
 
 
 
 

 

Leave a Reply

Your email address will not be published. Required fields are marked *