The imprecise nature of 3D printing limits this technology’s use beyond prototyping. In order to reliably use this technology for aerospace applications and aid certification of printed parts, the quality and repeatability of the parts must be improved. Much of the difficulty in obtaining high quality printed parts lies in finding optimum printing parameters. Currently, this requires much trial and error and expertise in the 3D printing process and materials engineering. Finding the optimum printing parameters for a print are also obfuscated by different optimum parameters throughout the part due in part to part geometry and within printer effects. In order to ease the determination of localized optimum printing parameters, one can envision a machine learning algorithm that could take in the object, predict the best printing parameters throughout the print, and communicate these parameters to the printer.
This is the goal of this work. In this internship we aim to: collect the data, train models, and use the predicted optimum local printing parameters to print a part with high fidelity and repeatability. We need to print parts varying the parameters of interest, create a point cloud for which the parameters will be chosen, identify errors at these points using an image classification network, obtain geometric information of these points, use this dataset to train a machine learning model, and use this model for prediction.
This document explains the work done and decisions made by Kevin Hunt and Austin Ebel in fulfillment of the Summer 2018 NIFS Internship. This work is a continuation of the work by John Gardner and much of the image classification work was done by John Gardner and the Governer’s School High School students Sean Zylich and Evan Rose.
This section covers the work to create the final dataset used to train the Error Prediction Machine Learning Model. This is printing parts varying the parameters of interest, creating a point cloud for which the parameters will be chosen, obtaining geometric information of these points, identifying errors at these points using an image classification network, and merging it all together.
The first step in collecting the data for this project requires printing the parts with various printing parameter combinations. This involves (1) choosing the part to print, (2) the material, (3) the printing parameters to vary and (4) printing the object with the various parameter combinations.
The part we have chosen to print is “3 sides of a cube”. This is a very simple 3D geometry that can easily have errors classified through an image classification neural network. Only 3 sides for speed, less material, and stackability.
From OpenSCAD, the object was exported as an .STL file, which was then sliced into gcode by CURA Lulzbot Edition for the Lulzbot Mini.
Lulzbot
Natural ABS 3mm Filament 1 kg reel
SKU: (3) 1775201312 (3)
Aleph Objects Incorporated
Reel 1: Prints 1-26, 167, 3b, 7b-13b, 314(Need to drop this print see comments section at end!)
Reel 2: Prints 27-112
Reel 3: Prints 113-127, 146, 161, 173, 186, 201, 202, 211, 239, 241, 259, 270, 272, 274
The parameters of interest are the Nozzle Temperature, Bed Temperature, Print Speed, Extrusion Multiplier and Fan Speed.
Nozzle temperature ranges were chosen to be 225 C to 245 C. In Rao 2015, it was recommended not to go below 225 to prevent clogging. We have also noticed material burning above 245.
Note: fan speeds are not possible between 0 to ~27%.
The R code for creating the parameter combinations and randomly ordering them:
nozzle.temp <- seq(225,245,10)
bed.temp <- seq(100,120,10)
print.speed <- seq(20,80,20)
extrusion.multiplier <- seq(90,110,10)
fan.speed.percent <- c(30,45,80)
# Combine them into the possible combinations
parameter.list <- list(Nozzle.Temp=nozzle.temp, Bed.Temp=bed.temp,
Print.Speed=print.speed, Extrusion.Multiplier =extrusion.multiplier,
Fan.Speed.Percent=fan.speed.percent)
all.options <- expand.grid(parameter.list)
# randomly order the rows
set.seed(42)
randomized.all.options <- all.options[sample(nrow(all.options),nrow(all.options)),]
# reset row numbers
rownames(randomized.all.options) <- NULL
# save these combinations as a .csv
#write.table("varied_parameters_print_order.txt", quote=F, x = randomized.all.options, col.names=FALSE)
There are 324 possible combinations:
Nozzle.Temp | Bed.Temp | Print.Speed | Extrusion.Multiplier | Fan.Speed.Percent | |
---|---|---|---|---|---|
1 | 245 | 120 | 20 | 110 | 80 |
2 | 245 | 110 | 40 | 110 | 80 |
3 | 245 | 100 | 60 | 110 | 30 |
4 | 245 | 110 | 40 | 100 | 80 |
5 | 235 | 120 | 60 | 110 | 45 |
6 | 225 | 110 | 60 | 100 | 45 |
We chose to randomly order the prints. If you look at the 3D representation of a problem surface, we could imagine the red to indicate an error while the dark blue indicates the highest fidelity. Imagine we do not randomly select the printing parameters and our problem surface is built in a straight line up the hill leading to the worst print conditions. By randomly sampling, we are hoping to approximate the surface of the problem prior to completing all 324 possible prints.
Pronterface.Temperature.Setting | Thermocouple.Reading | Difference | |
---|---|---|---|
1 | 260 | 257 | 3.0 |
2 | 255 | 252 | 3.0 |
3 | 250 | 247 | 3.0 |
4 | 245 | 243 | 2.0 |
5 | 240 | 238 | 2.0 |
6 | 235 | 234 | 1.0 |
7 | 230 | 229 | 1.0 |
8 | 225 | 223 | 2.0 |
9 | 220 | 219 | 1.0 |
10 | 215 | 214 | 1.0 |
11 | 210 | 209 | 1.0 |
12 | 205 | 204 | 1.0 |
13 | 200 | 200 | 0.0 |
14 | 195 | 195.1 | -0.1 |
15 | 190 | 190.2 | -0.2 |
16 | 185 | 185.3 | -0.3 |
17 | 180 | 180.5 | -0.5 |
18 | 175 | 175.3 | -0.3 |
These readings indicate we can rely on the pronterface reading of nozzle temperature.
As seen above, there are 324 possible combinations, requiring 324 gcodes. A python script was written to parse a base.gcode into the 324 required gcodes.
with open("/path/to/parameter combinations") as print_order_file:
for line in print_order_file:
parameter_list = line.split()
print_number = parameter_list[0]
nozzle_temperature = parameter_list[1]
bed_temperature = parameter_list[2]
print_speed = parameter_list[3]
extrusion_multiplier = parameter_list[4]
fan_speed_percent = parameter_list[5]
with open("/path/to/base gcode file") as base_gcode:
# load in the base gcode
gcode_data = base_gcode.readlines()
# Edit the gcode based on the new parameters
# Set Nozzle Temp
gcode_data[62] = "M109 R" + nozzle_temperature + " ; wait for extruder to reach printing temp\n"
gcode_data[570] = "M104 S" + nozzle_temperature + "\n"
# Set Bed temp
gcode_data[15] = "M140 S" + bed_temperature + " ; start bed heating up\n"
gcode_data[63] = "M190 S" + bed_temperature + " ; wait for bed to reach printing temp\n"
gcode_data[576] = "M140 S" + bed_temperature + "\n"
# Set Print Speed and Extrusion Multiplier
make_changes = False
print_finished = False
line_counter = 0
previous_extrusion_multiplier = float(gcode_data[67][-4:-1])
for code_line in gcode_data:
# make sure outside of skirt
if code_line == ";TYPE:WALL-INNER\n":
make_changes = True
# make sure not at end
if "M104 S0" in code_line:
make_changes = False
# begin changing the line if it's not the skirt or after the print
if make_changes:
# if the line is an extrusion and movement command
if all(x in code_line for x in ['G1', 'X', 'Y', 'E']):
split_line = code_line.split()
# loop through the elements of the line
for i in range(0,len(split_line)):
# if the element is setting speed
if "F" in split_line[i]:
# change the speed
split_line[i] = 'F' + str(int(print_speed)*60)
# if the element is setting the extrusion amt
elif "E" in split_line[i]:
# grab the original extrusion amount
extruded_amount = split_line[i][1:]
# calculate the new extrusion amount
# original*new flow/previous flow
new_extrude_amount = round(float(extruded_amount)*float(extrusion_multiplier)/previous_extrusion_multiplier,5)
# TO DO: round to 5 decimals
split_line[i] = 'E' + str(new_extrude_amount)
# rewrite the line
gcode_data[line_counter] = " ".join(split_line) + '\n'
elif " E" in code_line:
split_up = code_line.split()
for i in range(0, len(split_up)):
if "E" in split_up[i]:
extrusion = float(split_up[i][1:])
new_extrude_amount = round(extrusion*float(extrusion_multiplier)/previous_extrusion_multiplier,5)
split_up[i] = "E" + str(new_extrude_amount)
gcode_data[line_counter] = " ".join(split_up) + '\n'
line_counter += 1
# Set Extrusion Multiplier
gcode_data[67] = "M221 S100\n"
# Set Fan Speed Percent
gcode_data[70] = "M106 S" + str(int(255*int(fan_speed_percent)/100)) + '\n'
# Add the parameter settings to the file as comments
print_parameters = [None] * 8
print_parameters[0] = "; GCode auto generated from a base CURA file\n"
print_parameters[1] = "; Kevin Hunt\n"
print_parameters[2] = "; Print Number " + print_number + ' of 324\n'
print_parameters[3] = "; Nozzle Temperature " + nozzle_temperature + ' C\n'
print_parameters[4] = "; Bed Temperature " + bed_temperature + ' C\n'
print_parameters[5] = "; Print Speed " + print_speed + ' mm/sec\n'
print_parameters[6] = "; Extrusion Multiplier " + extrusion_multiplier + ' %\n'
print_parameters[7] = "; Fan Speed " + fan_speed_percent + " %\n"
gcode_data = print_parameters + gcode_data
# write the new gcode to a file
new_filename = "gcode folder\\3_sides_of_a_cube_print_" + print_number + ".gcode"
with open(new_filename, 'w') as new_gcode:
new_gcode.writelines(gcode_data)
At the time of printing, the ambient room temperature and ambient humidity was recorded. Prints are sent to the Lulzbot Mini printer using Pronterface. Approximately 141 prints varying the printing parameters were completed in this internship.
Thus far, we have made prints varying the printing parameters overall. However, part of our goal is to find local optimum parameters along the toolpath using a machine learning model. A machine learning model will need data about the individual areas of a part, not the entire part.
Our tactic to creating a point cloud was to split up the gcode along the toolpath and use those XYZ points. In our ‘3 sides of a cube’, some Gcode segments spanned the entire 50mm wall, looking like this:
We decided it was best to break up these Gcode segments into roughly 2mm each. A python script was written to pull the extrusion commands out of a base gcode (skipping the skirt), chop those up into evenly divided section of ~2 mm, and rewrite the gcode as this chopped up version.
# Similar code will be used later taking into account our predictions from the model
import math
nozzle_temperature = "245"
bed_temperature = "100"
print_speed = "60"
extrusion_multiplier = "110"
fan_speed_percent = "10"
segment_size = 2 #mm
offset = 0 # lines
with open("/path/to/base gcode") as base_gcode:
# load in the base gcode
gcode_data = base_gcode.readlines()
gcode_to_write = []
# Set the overall bed temperature
gcode_data[15+offset] = "M140 S" + bed_temperature + " ; start bed heating up\n"
gcode_data[63+offset] = "M190 S" + bed_temperature + " ; wait for bed to reach printing temp\n"
gcode_data[576+offset] = "M140 S" + bed_temperature + "\n"
# Set First Extrusion Multiplier
gcode_data[67+offset] = "M221 S100\n"
# Set First Fan Speed Percent
gcode_data[71+offset] = "M106 S" + str(round(255*int(fan_speed_percent)/100,1)) + '\n'
# Set Print Speed and Extrusion Multiplier
make_changes = False
print_finished = False
line_counter = 0
previous_extrusion_multiplier = float(gcode_data[67+offset][-4:-1])
X_prev = 0
Y_prev = 0
current_X = 0
current_Y = 0
for code_line in gcode_data:
line_to_write = code_line
# make sure outside of skirt
# TO DO: change this to instead check type not skirt
if code_line == ";TYPE:WALL-INNER\n":
make_changes = True
# make sure not at end
if "M104 S0" in code_line:
make_changes = False
# store the current X and Y
if "X" in code_line:
current_X = float([x[1:] for x in code_line.split() if "X" in x][0])
if " Y" in code_line:
current_Y = float([y[1:] for y in code_line.split() if "Y" in y][0])
#TO DO: prevent infill split up
# begin changing the line if it's not the skirt or after the print
if make_changes:
# if the line is an extrusion and movement command
if all(x in code_line for x in ['G1', 'X', 'Y', 'E']):
# grab the amount of extrudate
extrusion = float([e[1:] for e in code_line.split() if "E" in e][0])
# start segmenting the line up
# a^2 + b^2 = c^2
extrusion_distance = math.sqrt((current_X-X_prev)**2+(current_Y-Y_prev)**2)
# based on the segment sizes, how many whole segments will be needed
number_of_new_segments = max(1,round(extrusion_distance/segment_size))
# based on that many segments, how far do we have to step in the X and Y direction each time
X_step_distance = (current_X-X_prev)/number_of_new_segments
Y_step_distance = (current_Y-Y_prev)/number_of_new_segments
# store the new coordinates for the segments
new_X_coords = []
new_Y_coords = []
for i in range(0,number_of_new_segments-1):
new_X_coords.append(X_prev + X_step_distance*(i+1))
new_Y_coords.append(Y_prev + Y_step_distance*(i+1))
new_X_coords.append(current_X)
new_Y_coords.append(current_Y)
# segment the extrusion amount up
new_extrusion_amount = round(extrusion/number_of_new_segments,5)
# write these new instructions out
line_to_write = []
for i in range(0,number_of_new_segments):
# calculate the new extrusion amount
# original*new flow/previous flow
# new_extrude_amount = round(float(extruded_amount)*float(extrusion_multiplier)/previous_extrusion_multiplier,5)
# Set Fan Speed Percent
fan_speed_set = "M106 S" + str(int(255*float(fan_speed_percent)/100)) + ' ; Set Fan Speed Percent\n'
# Set Hot end temp
wait_hot_end = "M104 S" + nozzle_temperature + " ; Set Nozzle Temp and continue without waiting\n"
gcode_command = "G1 F" + str(int(print_speed)*60) + " X" + str(new_X_coords[i]) \
+ " Y" + str(new_Y_coords[i]) + " E" + str(new_extrusion_amount) + "\n"
line_to_write.extend([fan_speed_set, wait_hot_end, gcode_command])
# store the previous X and Y
X_prev = current_X
Y_prev = current_Y
gcode_to_write.extend(line_to_write)
# write the new gcode to a file
new_filename = "segmented gcode.gcode"
with open(new_filename, 'w') as new_gcode:
new_gcode.writelines(gcode_to_write)
The motivation for rewriting the gcode in this way was three-fold: (1) these segments may have different optimum parameters picked by the model and thus, the gcode will need to be segmented in this way in the future; (2) we wanted to test that the segmenting alone (without changing any of the parameters from segment to segment) did not negatively impact the print; (3) we wanted to test the impact of varying the parameters from segment to segment on part quality. These will be discussed below in the sections on communicating predicted parameters to the gcode and parameter smoothing.
Now we have our XYZ points of interest. Next, we will combine these XYZ points with the other information needed to train the model.
Many features of a printed part contribute to the quality of the print (printer XYZ, part XYZ, material, etc). Another feature we know contributes to printing errors and thus, optimum printing parameters, is the part geometry around a point. We were able to find a package that can calculate the geometric properties of an object from faces and vertices contained within the .STL file. Getting the geometric properties for each point of the gcode required (1) refining the .STL file to have a fine enough resolution, (2-3) using the LibIGL package and some custom code to calculate the geometric properties, and (4-6) matching these .STL geometries with gcode points.
Our original 3 sides of a cube .STL file has very few vertices. However, our gcode is segmented every 2 mm, thus we want the geometric properties of that exact location in the printed part. Thus, we need more vertices than OpenSCAD created for this part. More vertices can be obtained by refining the model using NetFabb.
NetFabb has an option to import a mesh, and, for our purposes, define a maximum edge length to refine the model. We used a student version of this software for this project.
A step-by-step process goes as follows:
Under the new menu select:
Setting the first value, Max. Edge Length, to 0.1mm (or the desired max).
It’s important afterwards to click “Apply Repair” in the bottom left and remove the old part.
Now, go to:
The reason we have chosen 0.1mm for the maximum edge length is to ensure that every gcode segment maps almost exactly to an STL vertex. Once refined to a max edge length of 0.1mm, there were around ~3,000,000 vertices in our STL file (Hence the need for code optimizations later on).
In order to obtain geometric information for each vertex of the .STL, a software package known as Libigl was used; it was designed specifically to obtain geometric data from meshes. They provide an in-depth tutorial and installation instructions and an official GitHub page.
Within Libigl, we looked specifically at the vertex normals, Gaussian Curvature, and Discrete Mean Curvature (along with X, Y, Z coordinates). While Libigl is written in C++, it has bindings in Python which made working these values into the training data much easier later on.
Libigl has done a fantastic job at minimizing the amount of code you need to write. In addition, there are Python tutorials on their Github for each of the three types of geometric data we’ve used.
For example, code to calculate and view the vertex normals looks as follows:
import sys, os
# Add the igl library to the module's search path
sys.path.insert(0, '/Users/person/.../libigl/python/') # Add own path
import pyigl as igl # Libigl's python bindings
stl_path = '/path/to/STL file'
# Libigl uses the eigen class:
# More info found here: https://eigen.tuxfamily.org/dox/group__TutorialMatrixClass.html
OV = igl.eigen.MatrixXd()
OF = igl.eigen.MatrixXi()
def compute_vertex_normals():
# Syntax for computing normals
igl.per_vertex_normals(V, F, igl.PER_VERTEX_NORMALS_WEIGHTING_TYPE_AREA, N_vertices)
return N_vertices
if __name__ == "__main__":
V = igl.eigen.MatrixXd()
F = igl.eigen.MatrixXi()
SVI = igl.eigen.MatrixXi()
SVJ = igl.eigen.MatrixXi()
N_vertices = igl.eigen.MatrixXd()
# Read in mesh and remove duplicate vertices, since multiple triangles
# can contain the same vertex - reducing redundant training data
igl.read_triangle_mesh(stl_path, OV, OF)
igl.remove_duplicate_vertices(OV, OF, 0.0, V, SVI, SVJ, F)
N_vertices = compute_vertex_normals()
# Vertex normals will be stored in N_vertices
print(N_vertices)
Code for all the geometric data is also below:
import sys, os
# Add the igl library to the modules search path
sys.path.insert(0, '/Users/person/.../libigl/python/') # Add own path
import pyigl as igl
stl_path = '/path/to/STL file'
OV = igl.eigen.MatrixXd()
OF = igl.eigen.MatrixXi()
# Compute vertex normals
def compute_vertex_normals():
igl.per_vertex_normals(V, F, igl.PER_VERTEX_NORMALS_WEIGHTING_TYPE_AREA, N_vertices)
return N_vertices
# Compute Gaussian Curvature
def compute_gaussian():
K = igl.eigen.MatrixXd()
igl.gaussian_curvature(V, F, K)
return K
# Compute Discrete Mean Curvature - in Libigl's tutorial
def compute_dmc():
HN = igl.eigen.MatrixXd()
L = igl.eigen.SparseMatrixd()
M = igl.eigen.SparseMatrixd()
Minv = igl.eigen.SparseMatrixd()
igl.cotmatrix(V, F, L)
igl.massmatrix(V, F, igl.MASSMATRIX_TYPE_VORONOI, M)
igl.invert_diag(M, Minv)
# Laplace-Beltrami of position
HN = -Minv * (L * V)
# Extract magnitude as mean curvature
H = HN.rowwiseNorm()
# Compute curvature directions via quadric fitting
PD1 = igl.eigen.MatrixXd()
PD2 = igl.eigen.MatrixXd()
PV1 = igl.eigen.MatrixXd()
PV2 = igl.eigen.MatrixXd()
igl.principal_curvature(V, F, PD1, PD2, PV1, PV2)
# Mean curvature
H = 0.5 * (PV1 + PV2)
return H
if __name__ == "__main__":
V = igl.eigen.MatrixXd()
F = igl.eigen.MatrixXi()
SVI = igl.eigen.MatrixXi()
SVJ = igl.eigen.MatrixXi()
N_vertices = igl.eigen.MatrixXd()
igl.read_triangle_mesh(stl_path, OV, OF)
igl.remove_duplicate_vertices(OV, OF, 0.0, V, SVI, SVJ, F)
N_vertices = compute_vertex_normals()
K = compute_gaussian() # K stores Gaussian Curvature for each vertex
H = compute_dmc() # H stores DMC values for each vertex
C = igl.eigen.MatrixXd()
igl.jet(H, True, C) # Convert into RGB values
# Libigl's viewer
viewer = igl.glfw.Viewer()
viewer.data().set_mesh(V, F)
viewer.data().set_colors(C)
viewer.launch()
Set up to view the Discrete Mean Curvature values, what appears should look something like this:
In addition to the geometric data obtained from Libigl, we felt angles between the Z axis, as well as print direction might also be important to include. Angles between the Z axis might be able to better tell us about overhangs, and print direction (defined as a vector facing out of the front of the printer) might give us information about how orientation affects quality.
The angles found between the Z axis can be seen in the following picture:
We can use the vertex normals found through Libigl to find both \(\theta\) and \(\phi\) and write functions that return these angles for every vertex in the STL file.
The print angle was found just by using the standard angle between two vectors equation:
\(\alpha = cos^{-1}(\frac{\textbf{u} \cdot \textbf{v}}{|\textbf{u}| |\textbf{v}|})\)
Where \(v\) is the vector \((0, 1, 0)\) - facing out of the print bed. In the code, compute_theta_vert() computes \(\theta\) for each vertex, and compute_phi_vert() computes \(\phi\) for each vertex.
The code for this looks as follows, and can also be visualized using Libigl’s viewer:
import numpy as np
import math
# Add the igl library to the modules search path
sys.path.insert(0, '/Users/person/.../libigl/python/') # Add own path
import pyigl as igl
from iglhelpers import e2p
# Compute theta from vertical (shown in picture)
def compute_theta_vert():
theta_vert = igl.eigen.MatrixXd(len(numpy_vertices), 1)
# Iterate over entire array calculating theta for each vertex
for i in range(len(numpy_vertices)):
if numpy_vertices[i][1] == 0:
theta_vert[i] = 0
else:
theta_vert[i] = math.atan2(numpy_vertices[i][1], numpy_vertices[i][0])
return theta_vert
# Compute phi from vertical (shown in picture)
def compute_phi_vert():
phi_vert = igl.eigen.MatrixXd(len(numpy_vertices), 1)
for i in range(len(numpy_vertices)):
phi_vert[i] = math.acos(numpy_vertices[i][2] / np.linalg.norm(numpy_vertices[i]))
return phi_vert
# Compute print angle according to above equation
def compute_angle_print():
print_angle = np.array([0, 1, 0])
angle_print = igl.eigen.MatrixXd(len(numpy_vertices), 1)
for i in range(len(numpy_vertices)):
angle_print[i] = math.acos(np.dot(print_angle, numpy_vertices[i]) / np.linalg.norm(numpy_vertices[i]))
return angle_print
if __name__ == "__main__":
numpy_vertices = e2p(N_vertices) # Libigl's helper function to convert eigen matrices into numpy arrays
tv = compute_theta_vert()
pv = compute_phi_vert()
ap = compute_angle_print()
C = igl.eigen.MatrixXd()
igl.jet(tv, True, C) # Convert into RGB values
viewer = igl.glfw.Viewer()
viewer.data().set_mesh(V, F)
viewer.data().set_colors(C)
viewer.launch()
Obtaining geometric data for all of the vertices took around 15 minutes. However, here we have gathered geometric data about the vertices of the .STL file and what we need to train our model is the geometric information of the gcode points. Thus, we need an efficient way to convert STL vertex information into Gcode segment information. This involved several steps talked about below.
Our first challenge can be perfectly summed up in the picture below.
Here the gcode toolpath is in light blue and 2 mm refined STL vertices are the black dots. The STL vertices on the top and right side go beyond that of the toolpath. This is an issue as Gcode segments that lie on the corner might not receive the correct ‘corner-like’ information from Libigl.
In CURA, the toolpath is adjusted to take the nozzle width of 0.5 mm and material deposition into account. So the dimensions of the toolpath are 49.5mm x 49.5mm x 49.4mm. In addition, the walls of our original STL file were 1mm wide, however Cura creates a toolpath along the walls of only 0.5mm.
We expect this will be less of an issue for less sharp and thin models. While our ‘3 sides of a cube’ is simple and easy to print, it’s somewhat of a double-edged sword as it also means in order to perfectly capture the geometries, we need to exactly align the STL file and Gcode segments.
Initially, we scaled all of the X, Y, Z coordinates to meet the Gcode toolpath, however this did not appropriately scale the wall widths. This approach should be fine for most models as the wall width will not be exposed. We remedied this issue for our part by simply adjusting the CAD model to reflect the Gcode path. We realize this isn’t the most generalizable option, however, again, we feel this issue is exacerbated in our model. In the future, it might be worth looking into better ways to match the toolpath with the STL file.
Now the dimensions of .STL and gcode points line up, but they do not have the same values for X,Y,Z. This was corrected so we could directly associate geometric information from the STL file with Gcode segments. This was done by finding the minimum X, Y, Z coordinates of both the STL and Gcode files and finding the difference between each of them. Then, we added that difference to the STL file to obtain the same coordinate system.
Now we have the gcode and stl points so they correctly overlap, now we need to map the gcode points to the closest STL point and grab the geometric information of that STL point. Here is a visual representation of what we are doing. We have colored the “gcode” by it’s associated vertex. This indicates that our mapping algorithm works as expected.
There’s no guarantee that every STL point will directly correspond to every other Gcode point. This is the case because we’re segmenting the STL file only every 0.1mm, so there will always be that slight misalignment. In fact, only a few STL points directly corresponded to Gcode points, so we needed a way to find the closest vertex to each Gcode segment.
This was done through what’s known as a KD Tree. Scikit Learn - a fantastic package for all things Machine Learning in Python - uses a KD Tree for their K-Nearest Neighbors algorithm. We were able to take the functionality they provide and use it for our purposes(implementation/examples)
The code below uses the KD Tree to associate STL vertices with Gcode segments. The remove_duplicates() function, well, removes the duplicate vertices in the STL file (as a result of multiple trianges containing the same vertex). The reorient() function adds the offset described in the ‘Correctly Align the Gcode and STL Files’ section above. Finally the KD Tree is created in the get_KDTree() function.
# Given an STL file and spliced Gcode segments, this program matches each of
# the given GCode segments to the closest vertex in the STL file.
import sys, os
sys.path.insert(0, '/Users/person/.../libigl/python/') # Add own path
import pandas as pd
from sklearn.neighbors import KDTree
import _pickle as cPickle
import pyigl as igl
from iglhelpers import e2p
import time
IMPORT_TREE = True
stl_path = '/path/to/STL file'
gcode_path = '/path/to/segmented gcode file'
tree_path = '/path/to/pre-built tree' # Only used after running once
def remove_duplicates():
# Use libigl's function to read in STL file
OV = igl.eigen.MatrixXd()
OF = igl.eigen.MatrixXi()
igl.read_triangle_mesh(stl_path, OV, OF)
V = igl.eigen.MatrixXd()
F = igl.eigen.MatrixXi()
SVI = igl.eigen.MatrixXi()
SVJ = igl.eigen.MatrixXi()
igl.remove_duplicate_vertices(OV, OF, 0.0, V, SVI, SVJ, F);
print("\nFinished removing duplicates.")
print("The mesh now has " + str(V.rows()) + " triangles.")
return V
def reorient(V):
vertices = e2p(V) # Libigl helper function to create numpy array from MatrixXd
df_vertices = pd.DataFrame(data = vertices, columns=['X', 'Y', 'Z'])
df_vertices['X'] = df_vertices['X'].astype(float)
df_vertices['Y'] = df_vertices['Y'].astype(float)
df_vertices['Z'] = df_vertices['Z'].astype(float)
min_stl_x = df_vertices['X'].min()
min_stl_y = df_vertices['Y'].min()
min_stl_z = df_vertices['Z'].min()
df_gcode_arr = pd.read_csv(gcode_path, names=['X', 'Y', 'Z'])
min_gcode_x = df_gcode_arr['X'].min()
min_gcode_y = df_gcode_arr['Y'].min()
min_gcode_z = df_gcode_arr['Z'].min()
# Calculate distance between min gcode values and min stl values
dist_x = min_gcode_x - min_stl_x
dist_y = min_gcode_y - min_stl_y
dist_z = min_gcode_z - min_stl_z
# Add that distance (above) as offset to each STL to have the STL file
# and G-code be in the same coordinate system
df_vertices['X'] += dist_x
df_vertices['Y'] += dist_y
df_vertices['Z'] += dist_z
print("Finished reorienting.")
return df_vertices, df_gcode_arr
def get_KDTree(df_vertices, df_gcode_arr):
if IMPORT_TREE == True:
tree = pd.read_pickle(tree_path)
else:
tree = KDTree(df_vertices)
print("Saving tree as pickle file...")
with open(tree_path, "wb") as output_file:
cPickle.dump(tree, output_file)
print(" done.")
df_gcode = pd.DataFrame(columns=['index', 'gx', 'gy', 'gz', 'vx', 'vy', 'vz', 'distance'])
# Query KD Tree to find nearest vertex for every gcode segment
total_dist = 0
for i in range(len(df_gcode_arr.index)):
dist, ind = tree.query([[df_gcode_arr['X'][i].astype(float),
df_gcode_arr['Y'][i].astype(float),
df_gcode_arr['Z'][i].astype(float)]], k = 1)
df_gcode.loc[i] = [i+1,
df_gcode_arr['X'][i],
df_gcode_arr['Y'][i],
df_gcode_arr['Z'][i],
df_vertices.ix[ind[0][0], 'X'],
df_vertices.ix[ind[0][0], 'Y'],
df_vertices.ix[ind[0][0], 'Z'],
dist[0][0]
]
total_dist += dist[0][0]
average_dist = round(total_dist / len(df_gcode_arr.index), 5)
print("Average distance from vertex: ", average_dist)
return df_gcode
if __name__ == '__main__':
t0 = time.clock()
V = remove_duplicates()
df_vertices, df_gcode_arr = reorient(V)
df_gcode = get_KDTree(df_vertices, df_gcode_arr)
df_gcode.to_pickle("/Users/.../df_gcode.pickle")
df_gcode.to_csv("/Users/.../df_gcode.csv")
t1 = time.clock()
print("Run time: " + str(round(t1 - t0, 4)) + "\n")
Including all segments, the average distance to the STL vertex is 0.04mm, which lines up with the fact that we segmented the STL file so that no edge is longer than 0.1mm.
Also, we can plot the geometric data of each gcode point to ensure we have correctly aligned/scaled properly:
Now we have a data set where the first 10 elements look like:
Segment | Gcode X | Gcode Y | Gcode Z | Vertex X | Vertex Y | Vertex Z | Distance | Vert Norm X | Vert Norm Y | Vert Norm Z | Theta Vert | Phi Vert | Print Angle | Gaussian Curvature | Discrete Mean Curve |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 99.22917 | 101.25 | 0.424 | 99.23633 | 101.2666 | 0.424 | 0.0180807 | 0 | 0 | -1 | 0 | 3.141593 | 1.570796 | 0 | 0 |
2505 | 99.22917 | 101.25 | 0.674 | 99.23633 | 101.2666 | 0.424 | 0.2506530 | 0 | 0 | -1 | 0 | 3.141593 | 1.570796 | 0 | 0 |
2 | 97.20833 | 101.25 | 0.424 | 97.20606 | 101.2666 | 0.424 | 0.0167576 | 0 | 0 | -1 | 0 | 3.141593 | 1.570796 | 0 | 0 |
2506 | 97.20833 | 101.25 | 0.674 | 97.20606 | 101.2666 | 0.424 | 0.2505610 | 0 | 0 | -1 | 0 | 3.141593 | 1.570796 | 0 | 0 |
3 | 95.18750 | 101.25 | 0.424 | 95.17578 | 101.2666 | 0.424 | 0.0203215 | 0 | 0 | -1 | 0 | 3.141593 | 1.570796 | 0 | 0 |
2507 | 95.18750 | 101.25 | 0.674 | 95.17578 | 101.2666 | 0.424 | 0.2508246 | 0 | 0 | -1 | 0 | 3.141593 | 1.570796 | 0 | 0 |
4 | 93.16667 | 101.25 | 0.424 | 93.14551 | 101.2666 | 0.424 | 0.0268945 | 0 | 0 | -1 | 0 | 3.141593 | 1.570796 | 0 | 0 |
2508 | 93.16667 | 101.25 | 0.674 | 93.14551 | 101.2666 | 0.424 | 0.2514425 | 0 | 0 | -1 | 0 | 3.141593 | 1.570796 | 0 | 0 |
5 | 91.14583 | 101.25 | 0.424 | 91.11523 | 101.2666 | 0.424 | 0.0348130 | 0 | 0 | -1 | 0 | 3.141593 | 1.570796 | 0 | 0 |
6 | 89.12500 | 101.25 | 0.424 | 89.13330 | 101.2183 | 0.424 | 0.0328056 | 0 | 0 | -1 | 0 | 3.141593 | 1.570796 | 0 | 0 |
Quick aside: Originally, we were segmenting the STL file every 2mm, however realized that we were often associating vertex information multiple layers above/below that gcode segment with that Gcode segment. This is obviously not ideal, as we really wanted each Gcode segment to be mapped to a vertex on the same level as the previous segment.
This is where we decided to further refine our model to a 0.1mm max edge length resulting in the ~3,000,000 vertices (from ~14,000). Dealing with this much data meant rethinking how we were storing the data; moving away from arrays to a more database-like approach with the Pandas DataFrame
This gave us a massive performance boost in creating our training data as now we were using data structure made for incredibly large amounts of data like our own. You can see the database structure in action in the code above.
So far we have our gcode segments and their geometric properties. Next we need to combine information about the printing parameters and errors with the gcode segments. The workflow for this section is training an image classification model to classify error types and then using the model to scan images of the printed part and identify the error type at each gcode point.
We were lucky enough to have two highschool interns from the Governor’s School Program to continue John Gardner’s work in this part of the project. John, Sean, and Evan created an image classification algorithm using transfer learning with AlexNet in MATLAB to automatically determine the likelihood of an error type based on pictures of the walls. Code can be found in the folder alongside this file. By giving Sean and Evan our segmented Gcode points, they could then translate the pixel coordinates of each picture into the necessary Gcode segments for our model.
Image Classification is an approach that lends itself very well to transfer learning, a technique that modifies a neural network pre-trained to classify a much larger data set. The early layers of the neural network have already learned how to pull out basic features from an image (e.g. lines and textures) and the last few layers do the work of classification. This allows us to replace the last few layers with new starting layers that will be trained to classify our images with our classes. Here, the MATLAB tutorial for using AlexNet for transfer learning was implemented to classify images with Blob, Delamination, None or Warp.
To begin, training data for the neural network to learn from must be created. Because we want high refinement of the location of errors in our print, images of printed walls were segmented into > 600 smaller squares. These smaller images were hand classified as Blob, Delamination, None or Warp by placing the image into a folder of that name. Not all of the error types occur at the same rate: ~95% of the images were None. When the network was trained on an unbalanced dataset, the network learned to classify every image as None because it was acheiving 95% accuracy. Hence, the classes needed to be balanced. The dataset was limited by delamination, starting out there were ~200 training images of delamination, so only ~1000 images total could be used to train the network. However, even with this limitation the AlexNet trained by Sean and Evan could achieve 93.8 % training accuracy and 88% test accuracy.
Sean and Evan came up with what we are calling “pseudo-semi-supervised learning”. Semi-supervised learning is the approach of using a model to classify and then using the results of that as new training data. What we are calling pseudo-semi-supervised is manually checking the results and reclassifying where the model was wrong. We were able to scour all of the prints so far and come up with more than twice as many delaminations. As we did this, we also reclassified many of the training images. There were many dark images, blurry images, blue printing material, and prints with white instead of black backgrounds. This training data was not representative of the images coming from our dataset. This is due to the fact that the training data used to train AlexNet was created before most of the pictures were taken so the technique of picture taking changed. Thus, despite the model trained having a decent test/training accuracy, it’s accuracy was poor when the model was used on our dataset. This is an important point; despite the high test accuracy, if the training data was not representative of the real world dataset, the model will have less accuracy in use. This came up a few times after our first round of modifying the training data. The network trained with >98% test accuracy, but when used to classify our full dataset, it did not catch a lot of blobs, or it put a lot of nones in delamination. Adding some more smaller blobs to the training set of Blobs and replacing too washed out or too dark Nones allowed us to achieve a model with 98.26% test accuracy that also showed high accuracy when manually checking the classifications of our dataset.
Other optimizations were added to the training done by us. We were able to see that delaminations mostly occured on slow prints with high nozzle temperature so we started printing these print conditions to try and come up with more delamination images. Training of AlexNet was very slow (~4 hrs) when we adopted it from Sean and Evan, so we added parallelization to the training which dropped the training time down to ~ 1 hour. We also tried various techniques: using ADAM optimizer instead of SGD, when using SGD increasing momentum from 0 to the default of 0.9, and others. The best results (by a hair) were seen with ADAM optimizer and ADAM optimizer trained very quickly ~20 minutes. The final model is created by the code in “transferLearning.m” and saved as “98.26_after_modifications.mat”:
There are some remaining concerns with this model. First, k-fold cross-validation was not used when training and testing this model. K-fold cross-validation adds more confidence in the generalizability of the test accuracy to a new dataset. Secondly, classification depends on what we deemed the errors were. Areas that we deemed as warping, might not be warping. Finally, there are other error types that could be classified (e.g. stringiness, missing material, burning). The images could be manualy moved to new folders of these error types and the MATLAB AlexNet Model slightly modified and retrained to pick up on these errors. Currently, some of these error types are possibly captured by the current approach (i.e stringness and missing material are possibly caught by blobs). This depends on how the human classification treated these errors when manually assigning an error type.
The Image classifier will need to use an edge detection method to find the printed part within the image. To aid the edge detection, a black background a chosen for maximum contrast against the natural ABS. Pictures of the printed part were taken in the order of the bottom with label, then front (XZ) then left (YZ). This order is very important for using a script to automatically prepare the data for classification. Great care was taken to ensure the images were in focus and bright.
In order for the MATLAB script to know which print number and which face it is working on, the images need to be organized in a specific way. Each Print Number needs it’s own folder named with the Print Number. Inside the folder should be the two faces named PrintNumber_XZ, PrintNumber_YZ.
We wrote a python script that could loop over one folder of images direct from the camera and rename and organize the images in this way. The first step is to ensure that the images from the camera are in a single folder and in the order of label, front (XZ), left (YZ), next label, etc. At the beginning of the python script, manually create a list of the print numbers in the order they are in the folder. Upon running the script, it will loop through folder of images and use the list and the order to rename the images as PrintNumber_Label, PrintNumber_XZ, PrintNumber_YZ. All the label images are moved into a single folder together. The two face images are placed into their own folder named by the PrintNumber. With this organization, these images are ready to feed into the Image Classification Network.
"""
Renames images from the camera with their appropriate print number and face plane and moves them to a new file structure in a target folder.
First: Place images in a root folder. Make sure they are in the order of "Label image, XZ face, YZ face"
Second: Create a list below with the print numbers in the appropriate order
Third: Set the holding folder and the target folder.
"""
import os, shutil
root = "path/to/folder of images in order of label, front, left, label"
new_location = "path/to/outfolder"
first_print = 79
last_print = 85
printList = list(range(first_print,last_print+1))
print(printList)
pictureNames = []
for printNumber in printList:
pictureNames.append(str(printNumber)+"_Label.jpg")
pictureNames.append(str(printNumber)+"_XZ.jpg")
pictureNames.append(str(printNumber)+"_YZ.jpg")
print(pictureNames)
for prints in printList:
folderName = os.path.join(new_location, str(prints))
if not os.path.exists(folderName):
os.mkdir(folderName)
if not os.path.exists(os.path.join(new_location, "Labels")):
os.mkdir(os.path.join(new_location, "Labels"))
for dirs,subdirs,images in os.walk(root):
for i in range(len(images)):
if ("Label" in pictureNames[i]):
new_loc = os.path.join(new_location, "Labels")
else:
new_loc = os.path.join(new_location, pictureNames[i].split("_")[0])
os.rename(os.path.join(root, images[i]), os.path.join(new_loc, pictureNames[i]))
So far we have discussed how the Image Classifier was trained and how to organize the images for classification. Next, a MATLAB program, “examplescript2.m” will prompt for the input folder of images organized as discussed above and an output folder to save the segment images for future training data. The program will take in images of printed part faces and:
gx | gy | gz | None | Blob | Delamination | Warp | Print Number |
---|---|---|---|---|---|---|---|
52.25 | 52.25 | 7.424 | 0.9984384 | 4.4e-06 | 0.0010829 | 0.0004744 | 1 |
52.25 | 52.25 | 7.674 | 0.9984384 | 4.4e-06 | 0.0010829 | 0.0004744 | 1 |
52.25 | 52.25 | 7.924 | 0.9984384 | 4.4e-06 | 0.0010829 | 0.0004744 | 1 |
52.25 | 52.25 | 8.174 | 0.9984384 | 4.4e-06 | 0.0010829 | 0.0004744 | 1 |
52.25 | 52.25 | 8.424 | 0.9984384 | 4.4e-06 | 0.0010829 | 0.0004744 | 1 |
52.25 | 52.25 | 8.674 | 0.9984384 | 4.4e-06 | 0.0010829 | 0.0004744 | 1 |
52.25 | 52.25 | 8.924 | 0.9984384 | 4.4e-06 | 0.0010829 | 0.0004744 | 1 |
52.25 | 52.25 | 9.174 | 0.9984384 | 4.4e-06 | 0.0010829 | 0.0004744 | 1 |
52.25 | 52.25 | 9.424 | 0.9995300 | 5.0e-07 | 0.0000442 | 0.0004253 | 1 |
52.25 | 52.25 | 9.674 | 0.9995300 | 5.0e-07 | 0.0000442 | 0.0004253 | 1 |
The results of running this image classifier on print 11 can be see here:
The pictures above are visual confirmations that the classification of and mapping to the gcode points works. These error classifications are then taken to be the true error at each segment when we train our prediction model next, but one should note that there is possibility of error in this classification data.
We tasked Sean and Evan, as an end goal, to output a file that contained each gcode segment of the outer wall, their respective error probabilities, as well as the print number associated with those segments. This could then be merged with our current training data to complete the data collection process.
Merging was done with the code below:
import pandas as pd
if __name__ == "__main__":
df_without_errors = pd.read_csv('/path/to/training_without_errors.csv') # Included with our code, but we created one file
# for all this, so look to the attached "add_errors.py" code to see how we included this as a function
errors = pd.read_csv('/path/to/defects.txt',
names = ['gx', 'gy', 'gz', 'None', 'Blob', 'Delamination', 'Warp', 'Print Number']) # Output from Sean/Evan's code
df_without_errors = df_without_errors.round({'gx': 3, 'gy': 3, 'gz': 3})
df_without_errors['Print Number'] = df_without_errors['Print Number'].astype(str)
df_without_errors['gx'] = df_without_errors['gx'].astype(str)
df_without_errors['gy'] = df_without_errors['gy'].astype(str)
df_without_errors['gz'] = df_without_errors['gz'].astype(str)
errors = errors.round({'gx': 3, 'gy': 3, 'gz': 3})
errors['Print Number'] = errors['Print Number'].astype(str)
errors['gx'] = errors['gx'].astype(str)
errors['gy'] = errors['gy'].astype(str)
errors['gz'] = errors['gz'].astype(str)
df_final = pd.merge(errors, df_without_errors, how = 'inner',
on = ['Print Number', 'gx', 'gy', 'gz'])
del df_final['vx']
del df_final['vy']
del df_final['vz']
df_final.to_csv('/Users/.../training_data.csv')
df_final.to_pickle('/Users/.../training_data.pickle') # Pickle files are good!
By doing what’s known as an ‘inner merge’ in Pandas, we were able to exclude information about the bottom/inner walls where errors were not classified by the Image Classifier.
After running the above code, our training data was complete.
So our final training data set now looks as follows:
Print Number | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
Segment Number | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 |
gx | 52.25 | 52.25 | 52.25 | 52.25 | 52.25 | 52.25 | 52.25 | 52.25 |
gy | 101.75 | 99.77 | 97.79 | 95.81 | 93.83 | 91.85 | 89.87 | 87.89 |
gz | 0.424 | 0.424 | 0.424 | 0.424 | 0.424 | 0.424 | 0.424 | 0.424 |
Vert Norm X | -0.5213484 | -0.7063889 | -0.7063889 | -0.7063889 | -0.7063889 | -0.7063889 | -0.7063889 | -0.7063889 |
Vert Norm Y | 0.6747491 | 0.0000000 | 0.0000000 | 0.0000000 | 0.0000000 | 0.0000000 | 0.0000000 | 0.0000000 |
Vert Norm Z | -0.5224075 | -0.7078239 | -0.7078239 | -0.7078239 | -0.7078239 | -0.7078239 | -0.7078239 | -0.7078239 |
Theta Vert | 2.22864 | 0.00000 | 0.00000 | 0.00000 | 0.00000 | 0.00000 | 0.00000 | 0.00000 |
Phi Vert | 2.120468 | 2.357209 | 2.357209 | 2.357209 | 2.357209 | 2.357209 | 2.357209 | 2.357209 |
Print Angle | 0.8301717 | 1.5707963 | 1.5707963 | 1.5707963 | 1.5707963 | 1.5707963 | 1.5707963 | 1.5707963 |
Gaussian Curvature | 1.570796e+00 | 5.550000e-16 | -3.330000e-16 | -4.440000e-16 | -4.440000e-16 | -4.440000e-16 | -4.440000e-16 | 5.550000e-16 |
Discrete Mean Curve | 6.271776 | 4.244462 | 4.244461 | 4.244461 | 4.244462 | 4.244461 | 4.244462 | 4.244462 |
Nozzle Temperature | 245 | 245 | 245 | 245 | 245 | 245 | 245 | 245 |
Bed Temperature | 120 | 120 | 120 | 120 | 120 | 120 | 120 | 120 |
Print Speed | 20 | 20 | 20 | 20 | 20 | 20 | 20 | 20 |
Extrusion Ratio | 121 | 121 | 121 | 121 | 121 | 121 | 121 | 121 |
Fan Speed | 80 | 80 | 80 | 80 | 80 | 80 | 80 | 80 |
None | 0.239282700 | 0.038243070 | 0.030320730 | 0.007509449 | 0.007509449 | 0.007382136 | 0.003673186 | 0.002521003 |
Blob | 0.019021520 | 0.008032763 | 0.004906438 | 0.002815095 | 0.002815095 | 0.008144435 | 0.014871720 | 0.009163341 |
Delamination | 0.000466769 | 0.001625355 | 0.000847524 | 0.000372028 | 0.000372028 | 0.000630621 | 0.000721761 | 0.000202111 |
Warp | 0.7412291 | 0.9520988 | 0.9639253 | 0.9893034 | 0.9893034 | 0.9838428 | 0.9807333 | 0.9881136 |
This concludes all of the information about building our training data. We have printed parts varying the parameters, segmented the gcode to obtain a point cloud, calculated the geometric properties of the gcode points (through mapping to stl information), trained an image classification neural network, used the image classification model on our prints, mapped the errors to the gcode points, and combined all of this data together into one dataframe that can be used in the next step: training an error prediction model.
Using the above dataset, we wish to train a machine learning model to aid in the prediction of the optimum printing parameters for each point on the part. This model could be designed in various ways to predict various parameters. This problem could be a multi-output or single-output or a regression or classification model.
Below is a discussion of the various approaches, our attempts at all of the options, and the best model we found.
The approach of using the model to predict multiple values simultaneously is called Multi-Output, Multi-Target, Multi-Variate, or Multi-Response regression (there are different names and techniques when the problem is a classification problem). This approach becomes more complicated than fitting a separate model to each output, because there are expected relationships between the outputs. The machine learning techniques best used for this approach are support vector regression, regression trees and neural networks. A good review by Borchani et al (2015) outlines this problem and recent attempts to improve the techniques.
Scikit-learn has a few models that natively support mutli-output regression:
One can imagine a machine learning model that would take in part XYZ and geometric properties and predict the best printing parameters. This would be a multi-output model. In addition to the difficulties in training a multi-output model, there is another reason not to pose the problem in this way: this approach would provide one good parameter combination per point. The predicted parameter changes from segment to segment might not be possible with the physical limitations of the printer. We will discuss physical limitations of parameter changes below.
If we instead use part XYZ, geometric properties and include printing parameters as input and predict the error instead, the training simplifies to single-output model. There are various packages available for this type of prediction.
There is also the added advantage of using this approach to create a list of possible parameters for a particular point. For example, let’s say we are looking at the model output for a given point (X=1, Y=1, Z=1) and various printing parameter combinations. The model could yield different probabilites of the none type error:Nozzle.Temp | Bed.Temp | Print.Speed | Extrusion.Multiplier | Fan.Speed.Percent | Prob.None.Error |
---|---|---|---|---|---|
245 | 120 | 20 | 110 | 80 | 0.98 |
245 | 110 | 40 | 110 | 80 | 0.90 |
235 | 120 | 60 | 110 | 45 | 0.65 |
225 | 110 | 60 | 100 | 45 | 0.54 |
245 | 100 | 60 | 110 | 30 | 0.12 |
245 | 110 | 40 | 100 | 80 | 0.00 |
Here we can see the best predicted printing parameters is the first combination, but the second combination might also do well in printing. This option of being able to pick between parameter combinations that fall above some threshold prediction, might enable better smoothing logic when it’s time to print using the optimized local parameters.
This would mean each gcode point at each parameter combination (3,240,000 rows) would need to be run through our model to obtain the error probabilities. A timing test using a pre-trained sklearn MLPRegressor model to predict over 2 million values completed the task in ~3 seconds.
Because we have set the image classification up to provide us with the image classification probabilites, not just the error type, we can pose this problem in various ways: as a multi-output regression (predict all of the error type probabilities as an output), single-output regression (predict the probability of a none type error), or a type of classification (binary being error or no error or multi-class to predict which of all the errors).
We tried all of these models and various parameters of these models:
We also tried these various approaches with the models:
All of this was performed with scikit-learn packages. The resulting best model is GradientBoostingClassifier().
This model was trained by down-sampling the data so each class (Blob, Delamination, None and Warp) was represented equally in the data. This had to be done with care because we learned that if information of a single print was in both the training and testing data, the prediction accuracy was artificially inflated. After balancing the classes, the final model used was trained with 12,291 data points of each error type. Keeping in mind there are ~10,000 data points per print, the amount of data used to train the model was significantly smaller than the amount of data available. This is due again to the limiting factor of delaminations in the data set.
The final model used dropped prints that contained points with 121% extrusion multiplier (discussed below): prints 1, 2, 3, and 167.
All the default parameters for the model were used, in the future the parameters can be tweaked to potentially obtain a higher accuracy model. We did not do this because this default model was good enough and safe. Also, one should be cautious while fine tuning a model to get the best prediction out of the specific test set; these parameters might not be best when the model is used in the real world.
Code for running our model can be seen below:
from sklearn.neural_network import MLPRegressor
from sklearn.neural_network import MLPClassifier
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import cross_val_predict
from sklearn.preprocessing import normalize
from sklearn.metrics import r2_score
from sklearn import preprocessing
import numpy as np
import pickle
import pandas
import time
import itertools
from sklearn.metrics import confusion_matrix
import matplotlib.pyplot as plt
if __name__ == '__main__':
# load the data from pickle
data = pandas.read_pickle("/path/to/training data.pickle") # Will be a pickle file
# delam_prints = data[data[['None', 'Blob', 'Delamination', 'Warp']].idxmax(axis=1) == 'Delamination']
# delam_prints = delam_prints['Print Number'].unique()
# ['1' '10' '10b' '14' '15' '16' '17' '20' '21' '27' '31' '314' '39' '45' '7']
# drop prints 1 2 3 from the dataframe because they have em 121%
data = data[~data['Print Number'].isin(["1", "2", "3", "167"])].reset_index()
# split the data into the predictors and the output
X = data[['gx', 'gy', 'gz', 'Vert Norm X', 'Vert Norm Y', 'Vert Norm Z', 'Theta Vert', 'Phi Vert', 'Print Angle', 'Discrete Mean Curve', 'Gaussian Curvature', 'Nozzle Temperature', 'Bed Temperature', 'Print Speed', 'Extrusion Ratio', 'Fan Speed']]
Y = data[['None', 'Blob', 'Delamination', 'Warp']]
# Normalize the input
min_max_scaler = preprocessing.MinMaxScaler()
np_scaled = min_max_scaler.fit_transform(X)
X = pandas.DataFrame(np_scaled)
# create a Y column of just the highest class
y_class = Y.idxmax(axis=1)
# Split into training and test while making sure not to split the Prints apart
test_size = 0.05
prints_numbers = data['Print Number'].unique()
total_prints = len(prints_numbers)
updated_test_size = round(test_size * total_prints) / total_prints
from sklearn.model_selection import train_test_split
X_train, X_test, Y_train, Y_test = train_test_split(X, y_class,
test_size = updated_test_size,
random_state = 0,
shuffle = False)
# Hand equalize the number of each
grouped = Y_train.groupby(Y_train)
balanced_Y_train_rows = grouped.apply(lambda x: x.sample(grouped.size().min()))
print(balanced_Y_train_rows.groupby(balanced_Y_train_rows).size())
blob_rows_to_grab = balanced_Y_train_rows['Blob'].index.values.ravel().tolist()
warp_rows_to_grab = balanced_Y_train_rows['Warp'].index.values.ravel().tolist()
delam_rows_to_grab = balanced_Y_train_rows['Delamination'].index.values.ravel().tolist()
none_rows_to_grab = balanced_Y_train_rows['None'].index.values.ravel().tolist()
rows_to_grab = blob_rows_to_grab + warp_rows_to_grab + delam_rows_to_grab + none_rows_to_grab
balanced_Y_train = Y_train.iloc[Y_train.index.get_indexer(rows_to_grab)]
balanced_X_train = X_train.iloc[X_train.index.get_indexer(rows_to_grab)]
# shuffle them
balanced_Y_train = balanced_Y_train.reset_index(drop=True)
balanced_X_train = balanced_X_train.reset_index(drop=True)
idx = np.random.RandomState(seed=42).permutation(balanced_X_train.index)
balanced_Y_train = balanced_Y_train.reindex(idx)
balanced_X_train = balanced_X_train.reindex(idx)
from sklearn.ensemble import GradientBoostingClassifier
model = GradientBoostingClassifier(random_state=42)
# start timer
t0 = time.clock()
######## Calculating the RFE ##########
# from sklearn.feature_selection import RFE
# rfe = RFE(model)
# fit = rfe.fit(balanced_X_train, balanced_Y_train)
# print("Num Features:")
# print(fit.n_features_)
# print("Selected Features:")
# print(fit.support_)
# print("Feature Ranking:")
# print(fit.ranking_)
model.fit(balanced_X_train, balanced_Y_train)
predictions = model.predict(X_test)
cnf_matrix = confusion_matrix(Y_test, predictions)
print(cnf_matrix)
# end timer
t1 = time.clock()
print(str(t1-t0) + "seconds")
# save the model
filename = 'gradientBoostingClassifier_model_dropEM121.sav'
pickle.dump(model, open(filename, 'wb'))
def plot_confusion_matrix(cm, classes,
normalize=False,
title='Confusion matrix',
cmap=plt.cm.Blues):
"""
This function prints and plots the confusion matrix.
Normalization can be applied by setting `normalize=True`.
"""
if normalize:
cm = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
print("Normalized confusion matrix")
else:
print('Confusion matrix, without normalization')
print(cm)
plt.imshow(cm, interpolation='nearest', cmap=cmap)
plt.title(title)
plt.colorbar()
tick_marks = np.arange(len(classes))
plt.xticks(tick_marks, classes, rotation=45)
plt.yticks(tick_marks, classes)
fmt = '.2f' if normalize else 'd'
thresh = cm.max() / 2.
for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
plt.text(j, i, format(cm[i, j], fmt),
horizontalalignment="center",
color="white" if cm[i, j] > thresh else "black")
plt.tight_layout()
plt.ylabel('True label')
plt.xlabel('Predicted label')
# Compute confusion matrix
np.set_printoptions(precision=2)
class_names=Y_test.groupby(Y_test).groups.keys()
# Plot non-normalized confusion matrix
plt.figure()
plot_confusion_matrix(cnf_matrix, classes=class_names,
title='Confusion matrix, without normalization')
# Plot normalized confusion matrix
plt.figure()
plot_confusion_matrix(cnf_matrix, classes=class_names, normalize=True,
title='Normalized confusion matrix')
plt.show()
Results of the model can be visualized through the confusion matrices below:
We can also look at the False Positive and False Negatives rates that occur in our model. We define a positive to be an occurence of an error, thus a false positive is when our model predicts there to be an error when there isn’t, and a false negative to be when our model predicts no error but there actually is one. For this model, we see:
Thus our model is much more likely to assign errors where there aren’t than vice versa.
At the stage of using the model for a prediction, the predict() method will provide the name of the class most likely. However, the predict_proba() method will provide the probabilities for each of the classes. We will move forward using predict_proba() because it is helpful when it comes to smoothing later on. Additionally, these probabilities provide a dial that can be turned for classification. Perhaps it might not be best to classify the point to the highest probability. It might turn out to be more accurate if an error requires a probability above a certain threshold. By tuning the threshold required to classify an error to be greater than 45% we see a better false positive to false negative ratio.
Furthermore, we might find out that there is a different best threshold for each class. In the future, area under the curve (AUC) approaches would be helpful in fine tuning the parameter. However, current tunings should be taken with a grain of salt due to the lack of cross-validation used in training the model (this threshold might just be better for this particular test set. Another reason to do cross validation while training the model.) We have chosen to continue moving forward with the default model.
Code for this can be seen below:
THRESHOLD = 0.45
# Use our model to predict the error types as well as their likelihoods
predictions = model.predict(X_test)
proba_predictions = model.predict_proba(X_test)
proba_predictions = pandas.DataFrame(data = proba_predictions, columns = model.classes_)
pred = pandas.concat([proba_predictions, proba_predictions.idxmax(axis = 1)], axis = 1)
pred.columns = ['Blob', 'Delamination', 'None', 'Warp', 'Error']
# Modify the model's predictions based on our threshold
pred['Error'] = np.where((pred[['Blob', 'Delamination', 'Warp']].max(axis = 1) <= THRESHOLD) | (pred[['Blob', 'Delamination', 'Warp']].max(axis = 1) < pred['None']),
'None', pred[['Blob', 'Delamination', 'Warp']].idxmax(axis = 1))
predictions = pred['Error']
Adding a threshold to the model results in confusion matrices that look like this:
With False Positive and True Positive rates being:
Thus, by adding the threshold, we increased the amount of ‘None’ classifications - which decreases the number of false positives by screening out when the model is < 45% sure an error occurs. However, in turn, we’re also increasing the number of false negatives by throwing what the model could accurately predict with < 45% confidence into ‘None’.
Increasing the threshold even further means balancing out the false positive and false positive rates, however, as there are significantly more ‘Nones’ than errors, an equal rate would yield an incredibly ugly confusion matrix.
As stated above, optimizations can be done (especially regarding separate thresholds for each error type) through the use of AUC approaches.
This model has been trained on a very narrow range of XYZ and geometric properties. Due to this, the model should not be used to predict other shapes. This model is trained on this shape and there is probably not much generalizability to other structures. This is further evidenced by the results of feature ranking with recursive feature elimination (RFE).
######## Calculating the RFE ##########
from sklearn.feature_selection import RFE
rfe = RFE(model)
fit = rfe.fit(balanced_X_train, balanced_Y_train)
print("Num Features:")
print(fit.n_features_)
print("Selected Features:")
print(fit.support_)
print("Feature Ranking:")
print(fit.ranking_)
This method will assign the importance of the features used to train a model and the default is to select half of the features by assigning them an importance of 1. Performing this with the Gradient Boosting Classifer yields:
This indicates the most important feautres during model training are the XYZ and printing parameters. This is probably due to the geometric properties being intrinsic to the XYZ since we are only training with a single part. With a model trained on more shapes, we expect future predictions to rely more on geometry.
We did try removing X and Y from the data and also including the information of the 4 following segments. The results were encouraging, but unusable for our curent goal. When the model will be trained on various shapes, there are other features that may be important for the model training (e.g. time since the segment below was printed, printer XYZ)
-Note: although printer XYZ will have an effect on print quality, our printer XYZ and gcode XYZ are the same.
In order to verify the predictive power of our model, we thought of a few ideas to test it with. They include predicting errors in future prints (ones that our model hasn’t seen), forcing each of the error types, optimizing for print speed while reducing errors, and printing the highest quality print our model predicts.
For each of the following sections, we will need all of the possible 10,149 points with all of the possible 432 parameter combinations (there are more than 324 because fan speed 0 was included in the training data). A python script was used to load in the points, do a cartesian join with the possible parameter combinations, normalize the data, and send it all through the model. All of these predictions were then saved to be loaded in for the following tasks.
import numpy as np
import pickle
import pandas
from sklearn import preprocessing
import time
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import matplotlib.cm as cm
# load the image classification data from pickle
data = pandas.read_pickle("/path/to/training data") # Will be pickle file
# drop prints 1 2 3 from the dataframe because they have em 121%
data = data[~data['Print Number'].isin(["1", "2", "3", "167"])].reset_index()
# drop the print number column
data = data.loc[:, data.columns != 'Print Number']
# grab only the points information
points = data[['gx', 'gy', 'gz', 'Vert Norm X', 'Vert Norm Y', 'Vert Norm Z', 'Theta Vert', 'Phi Vert', 'Print Angle', 'Discrete Mean Curve', 'Gaussian Curvature']].drop_duplicates()
print("There are ", str(points.shape[0]), " points in this object")
# get the unique values of printings parameters
# 'Nozzle Temperature', 'Bed Temperature', 'Print Speed', 'Extrusion Ratio', 'Fan Speed'
possible_NozzleTemps = data['Nozzle Temperature'].unique()
possible_bedTemp = data['Bed Temperature'].unique()
possible_printSpeed = data['Print Speed'].unique()
possible_extrusionMultiplier = data['Extrusion Ratio'].unique()
possible_fanSpeed = data['Fan Speed'].unique()
# all possible combos of the changing parameters
potential_param_combos = pandas.DataFrame([[a,b,c,d,e] for a in possible_NozzleTemps for b in possible_bedTemp\
for c in possible_printSpeed for d in possible_extrusionMultiplier for e in possible_fanSpeed])
print("There are ", str(potential_param_combos.shape[0]), " possible printing parameter combinations")
potential_param_combos.columns = ['Nozzle Temperature', 'Bed Temperature', 'Print Speed', 'Extrusion Ratio', 'Fan Speed']
# merge the points with all the combos
print("...Merging the points with the combinations...")
points['key'] = 0
potential_param_combos['key'] = 0
points_to_check = pandas.merge(points, potential_param_combos, on='key').drop('key', axis=1)
# normalize the data
column_names = points_to_check.columns
min_max_scaler = preprocessing.MinMaxScaler()
np_scaled = min_max_scaler.fit_transform(points_to_check)
send_to_model = pandas.DataFrame(np_scaled)
send_to_model.columns = column_names
# reorder the columns to match original order
# 'Nozzle Temperature', 'Bed Temperature', 'Print Speed', 'Extrusion Ratio', 'Fan Speed'
send_to_model = send_to_model[['gx', 'gy', 'gz', 'Vert Norm X', 'Vert Norm Y', 'Vert Norm Z', 'Theta Vert', 'Phi Vert', 'Print Angle', 'Discrete Mean Curve', 'Gaussian Curvature', 'Nozzle Temperature', 'Bed Temperature', 'Print Speed', 'Extrusion Ratio', 'Fan Speed']]
# load the model from disk
print("...Loading the model...")
filename = '/path/to/gradientBoostingClassifier_model_dropEM121.sav' # Will be found in code folder
loaded_model = pickle.load(open(filename, 'rb'))
# run the points to check through the model
print("...Making point predictions...")
results = loaded_model.predict_proba(send_to_model)
print("...Done making predictions")
# add the probability predictions and a classification column to the non normalized data
results = pandas.DataFrame(results)
results.columns = ['Blob', 'Delamination', 'None', 'Warp']
results_class = results.idxmax(axis=1)
points_to_check['Predicted Error'] = results_class
points_to_check = pandas.concat([points_to_check.reset_index(drop=True), results], axis=1)
# save this dataframe as AllPossiblePredictions
points_to_check.to_pickle("AllPossiblePredictions.pickle")
gx | 52.25 | 52.25 | 52.25 | 52.25 | 52.25 | 52.25 |
gy | 52.25 | 52.25 | 52.25 | 52.25 | 52.25 | 52.25 |
gz | 0.424 | 0.424 | 0.424 | 0.424 | 0.424 | 0.424 |
Vert Norm X | -0.5769593 | -0.5769593 | -0.5769593 | -0.5769593 | -0.5769593 | -0.5769593 |
Vert Norm Y | -0.5769593 | -0.5769593 | -0.5769593 | -0.5769593 | -0.5769593 | -0.5769593 |
Vert Norm Z | -0.5781314 | -0.5781314 | -0.5781314 | -0.5781314 | -0.5781314 | -0.5781314 |
Theta Vert | -2.356194 | -2.356194 | -2.356194 | -2.356194 | -2.356194 | -2.356194 |
Phi Vert | 2.187233 | 2.187233 | 2.187233 | 2.187233 | 2.187233 | 2.187233 |
Print Angle | 2.185797 | 2.185797 | 2.185797 | 2.185797 | 2.185797 | 2.185797 |
Discrete Mean Curve | 6.432168 | 6.432168 | 6.432168 | 6.432168 | 6.432168 | 6.432168 |
Gaussian Curvature | 1.570796 | 1.570796 | 1.570796 | 1.570796 | 1.570796 | 1.570796 |
Nozzle Temperature | 225 | 225 | 225 | 225 | 225 | 225 |
Bed Temperature | 120 | 120 | 120 | 120 | 120 | 120 |
Print Speed | 20 | 20 | 20 | 20 | 20 | 20 |
Extrusion Ratio | 90 | 90 | 90 | 90 | 110 | 110 |
Fan Speed | 0 | 45 | 80 | 30 | 0 | 45 |
Predicted Error | Blob | Blob | Blob | Blob | Blob | Blob |
Blob | 0.8585363 | 0.8278785 | 0.7725166 | 0.8719606 | 0.5487145 | 0.6771166 |
Delamination | 0.006941469 | 0.037239006 | 0.112329269 | 0.026525362 | 0.008253792 | 0.031690457 |
None | 0.010334267 | 0.002244989 | 0.001789122 | 0.002087487 | 0.013658617 | 0.003893036 |
Warp | 0.12418797 | 0.13263753 | 0.11336500 | 0.09942655 | 0.42937307 | 0.28729993 |
Transposed for viewing.
In picking the prints we wanted to test, we chose 4 of the prints at random that we hadn’t yet done - as to remain as objective as possible. The Error Predictions for these prints were obtained from our model.
import pickle
import pandas
# params for print
print_number = 138
nt = 245
bt = 100
ps = 60
em = 90
fs = 80
# load in the prediction data
points_to_check = pandas.read_pickle("AllPossiblePredictions.pickle")
# points_to_check = points_to_check.loc[0:500000,:]
# grab the row indexes for points of print of interest
print_of_interest = points_to_check[(points_to_check['Fan Speed']==fs) \
& (points_to_check['Bed Temperature']==bt)\
& (points_to_check['Nozzle Temperature']==nt)\
& (points_to_check['Print Speed']==ps)\
& (points_to_check['Extrusion Ratio']==em)]
# save the predictions
print_of_interest.to_pickle('Predictions of Print ' + str(print_number) + '.pickle')
After printing, we photographed each of them, and ran them back through the Image Classifier to obtain the “true” errors at each segment. This could then be compared with our Error Prediction model’s output for accuracy.
The prints chosen at random were:
The accuracy score for the prediction was determined by calculating how many of the XYZ errors were accurately predicted compared to the Image Classification.
import glob
import pandas as pd
import numpy as np
from sklearn.metrics import accuracy_score
list_dir = glob.glob("/path/to/folder of predictions/*.pickle")
df_alex = pd.read_csv("/path/to/defects predictions.txt", header = None)
df_alex.columns = ['gx', 'gy', 'gz', 'None', 'Blob', 'Delamination', 'Warp', 'Print Number']
y_class = df_alex[['Warp', 'None', 'Blob', 'Delamination']].idxmax(axis = 1)
y_class = pd.DataFrame(data = y_class, columns = ['Classification'])
df_alex = pd.concat([df_alex, y_class], axis = 1)
avg_list = []
for element in list_dir:
df = pd.read_pickle(element).reset_index()
string = element.strip().split(".")
print_number = string[0][-3:]
df_subset = df_alex.loc[df_alex['Print Number'] == print_number].reset_index()
accuracy = accuracy_score(df['Predicted Error'], df_subset['Classification'])
print(print_number, accuracy)
avg_list.append(accuracy)
print("AVERAGE ACCURACY: ", np.mean(avg_list))
More information about these results can be found in the Results section.
With our trained model, we would like to predict the printing parameters to optimize for speed, quality or errors. In all of the optimizations below, we chose to hold Bed Temperature and Nozzle Temperature constant. Holding bed temperature constant was decided upon due to global effect of varying the bed temperature throughout the print. Holding the Nozzle Temperature constant is due to the physical constraints of the printer.
Here we see, under the worst printing conditions for nozzle heating, it takes ~42 seconds to ramp up 10 degrees Celsius. At our slowest printing conditions, 20 mm/sec, one face layer is completed in 2.5 seconds. Waiting from segment to segment for even a second drastically affects the print quality. Thus, it is outside the current printers capabilities to change the nozzle temperature within a single layer.
Holding the nozzle temperature constant throughout the print does not seem to lower the probability of None Error and in the case of the optimizing for speed, there does not seem to be a loss in the print speeds predicted.
In these optimizations, we have done a very simple approach for choosing the best parameters. In the future, a more advanced optimization approach may provide a better print; printer capabilities for changing extrusion ratio and nozzle temperature and/or desired structural properties could be included when deciding on the best local printing parameters.
In order to optimize for quality, the best overall Bed Temperature and Nozzle Temperature was found. “Best” was determined by finding the Bed Temperature and Nozzle Temperature combination that contained the most None Predictions of the 10,149 points. This Bed Temperature and Nozzle Temperature was kept constant throughout the decisions.
Points that could never be None error at this Bed Temperature and Nozzle Temperature had their Print Speed, Extrusion Ratio and Fan Speed selected for the lowest probability of Blob. Points that could be None at this Bed Temperature and Nozzle Temperature had their Print Speed, Extrusion Ratio and Fan Speed selected for the highest probability of None.
import numpy as np
import pickle
import pandas
from sklearn import preprocessing
import time
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import matplotlib.cm as cm
# load in the prediction data
points_to_check = pandas.read_pickle("AllPossiblePredictions.pickle")
# points_to_check = points_to_check.loc[0:500000,:]
# get the total number of points in the objects
points = points_to_check[['gx', 'gy', 'gz', 'Vert Norm X', 'Vert Norm Y', 'Vert Norm Z', 'Theta Vert', 'Phi Vert', 'Print Angle', 'Discrete Mean Curve', 'Gaussian Curvature']].drop_duplicates()
print("There are ", str(points.shape[0]), " points in this object")
# create a boolean for if there error is a None or not
points_to_check['isNoneError'] = points_to_check['Predicted Error'] == 'None'
# how many points are never none
bad_points = points_to_check.groupby(['gx','gy','gz']).filter(lambda s: s.isNoneError.sum()==0)[['gx', 'gy', 'gz']].drop_duplicates()
number_bad_points = bad_points.shape[0]
potential_printing_accuracy = round(float((points.shape[0] - number_bad_points)/points.shape[0]*100),2)
print("There are ", str(number_bad_points), " points in this object that are predicted to always print poorly.")
print("The potential printing accuracy is ", str(potential_printing_accuracy), "%")
# these columns aren't needed
good_points = points_to_check.drop(columns=['Vert Norm X', 'Vert Norm Y', 'Vert Norm Z', 'Theta Vert', 'Phi Vert', 'Print Angle', 'Discrete Mean Curve', 'Gaussian Curvature'])
# now pick the best bed temp and nozzle temp
best_points = good_points[good_points.groupby(['gx', 'gy', 'gz', 'Bed Temperature', 'Nozzle Temperature'])['None'].transform(max) == good_points['None']]
bed_temp_prediction_probs = best_points.groupby(['Bed Temperature', 'Nozzle Temperature'])['None'].agg(["mean", "std"])
# first get all the points at these parameters
good_points = good_points[(good_points['Bed Temperature']==bed_temp_prediction_probs['mean'].idxmax()[0]) & (good_points['Nozzle Temperature']==bed_temp_prediction_probs['mean'].idxmax()[1])]
# next grab the points that are never none
bad_points = good_points.groupby(['gx','gy','gz']).filter(lambda s: s.isNoneError.sum()==0)
# now create a new data frame of the points that can be none
remaining_points = pandas.merge(good_points, bad_points, how = 'outer', indicator=True).query('_merge=="left_only"').drop('_merge', axis=1)
# for the bad points, we need to grab the parameters that give the lowest blob probability
best_bad_points = bad_points[bad_points.groupby(['gx', 'gy', 'gz'])['Blob'].transform(min) == bad_points['Blob']]
# now grab the highest none for the good_points
best_points = remaining_points[remaining_points.groupby(['gx', 'gy', 'gz'])['None'].transform(max) == remaining_points['None']]
# now combine the decisions for the bad points and the good points into one df
best_points = best_points.append(best_bad_points, ignore_index=True)
# plot the decisions
param_list = ['Nozzle Temperature', 'Bed Temperature', 'Print Speed', 'Extrusion Ratio', 'Fan Speed', 'None']
i=1
fig = plt.figure(figsize=(8.5,10))
for parameter in param_list:
ax = fig.add_subplot(3,2,i, projection = '3d')
ax.view_init(azim=225)
line = ax.scatter(best_points['gx'].astype(float).tolist(), best_points['gy'].astype(float).tolist(), best_points['gz'].astype(float).tolist(), c = best_points[parameter].astype(float).tolist(), cmap=cm.get_cmap('viridis', len(best_points[parameter].unique())))
if parameter == 'None':
cb = plt.colorbar(line)
plt.title('Probability of None Type Error')
else:
cb = plt.colorbar(line, ticks=best_points[parameter].unique())
plt.title('Changes in ' + parameter)
i+=1
plt.show()
In order to optimize for speed, the best overall bed temperature and nozzle temperature with a print speed of 80 mm/sec was found. “Best” was determined by finding the bed temperature and nozzle temperature combination that contained the most None Predictions of the 10,149 points. This bed temperature and nozzle temperature was kept constant throughout the decisions.
First, all points that could have None error at 80 mm/sec print speed at this Bed Temperature and Nozzle Temperature were grabbed. Fan Speed and Extrusion Ratio for each point were selected to maximize the probability of None error.
Points that could not be none at 80 mm/sec then underwent the same procedure at 60 mm/sec. From there, remaining points were grabbed that could produce None error at 40 mm/sec and finally at 20 mm/sec.
Points that could not be None under any of these conditions had their Print Speed, Extrusion Ratio and Fan Speed selected for the highest probability of None at first. However, we later found that instead selecting for the lowest probability of Blob gave better results.
'''
This is very Kludgey code. It should have been functionalized for DRYness. When first getting comfortable with this code, start with optimizing for quality.
'''
import numpy as np
import pickle
import pandas
from sklearn import preprocessing
import time
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import matplotlib.cm as cm
# start timer
t0 = time.clock()
# load in the prediction data
points_to_check = pandas.read_pickle("AllPossiblePredictions.pickle")
# points_to_check = points_to_check.loc[0:500000,:]
# get the total number of points in the objects
points = points_to_check[['gx', 'gy', 'gz', 'Vert Norm X', 'Vert Norm Y', 'Vert Norm Z', 'Theta Vert', 'Phi Vert', 'Print Angle', 'Discrete Mean Curve', 'Gaussian Curvature']].drop_duplicates()
print("There are ", str(points.shape[0]), " points in this object")
# create a boolean for if there error is a None or not
points_to_check['isNoneError'] = points_to_check['Predicted Error'] == 'None'
# how many points are never none
bad_points = points_to_check.groupby(['gx','gy','gz']).filter(lambda s: s.isNoneError.sum()==0)[['gx', 'gy', 'gz']].drop_duplicates()
number_bad_points = bad_points.shape[0]
potential_printing_accuracy = round(float((points.shape[0] - number_bad_points)/points.shape[0]*100),2)
print("There are ", str(number_bad_points), " points in this object that are predicted to always print poorly.")
print("The potential printing accuracy is ", str(potential_printing_accuracy), "%")
# get the number of good points are within each subgroup that cannot easily be varied (exclude fan speed and extrusion multiplier)
# grab all the rows with "None" error, groupby bed temp, nozzle temp and print speed, but only count each point once
parameter_quality = points_to_check[points_to_check['Predicted Error']=='None'].drop_duplicates(['Bed Temperature', 'Nozzle Temperature', 'Print Speed', 'gx','gy','gz'])
parameter_quality = parameter_quality.groupby(['Bed Temperature', 'Nozzle Temperature', 'Print Speed']).size().to_frame(name='count').reset_index()
parameter_quality['percent'] = parameter_quality['count'].apply(lambda x: round(float(x/points.shape[0]*100),2))
# print(parameter_quality.sort_values(['Print Speed', 'percent'], ascending=[False, False]))
print("The best parameter combination at 80 print speed is: ")
best_combo = parameter_quality[parameter_quality['Print Speed']==80].ix[parameter_quality[parameter_quality['Print Speed']==80]['percent'].idxmax()]
print(best_combo)
# using the top parameter combo as a guide, what are the good points
good_points = points_to_check[(points_to_check['Predicted Error']=='None') \
& (points_to_check['Bed Temperature']==best_combo['Bed Temperature'])\
& (points_to_check['Nozzle Temperature']==best_combo['Nozzle Temperature'])\
& (points_to_check['Print Speed']==best_combo['Print Speed'])]
good_points = good_points.drop_duplicates(['gx','gy','gz'])[['gx', 'gy', 'gz']]
print(good_points.shape[0])
# we now have the good points and the bad points, where are the potential gain points
remaining_points = points_to_check[['gx', 'gy', 'gz']].drop_duplicates(['gx', 'gy', 'gz'])
# drop the points that are good
remaining_points = pandas.merge(remaining_points, good_points, how = 'outer', indicator=True).query('_merge=="left_only"').drop('_merge', axis=1)
# drop the points that will always be bad
remaining_points = pandas.merge(remaining_points, bad_points, how = 'outer', indicator=True).query('_merge=="left_only"').drop('_merge', axis=1)
print("By locally varying the print parameters, we can gain ", str(remaining_points.shape[0]), " more None type points")
# select the printing parameter combinations that these points are good at
def good_param_combos():
remaining_point_data = pandas.merge(points_to_check, remaining_points, on=['gx', 'gy', 'gz'], how='outer', indicator=True).query('_merge=="both"').drop('_merge', axis=1)
remaining_points_constant_bed_temp = remaining_point_data[(remaining_point_data['Bed Temperature']==best_combo['Bed Temperature']) & (remaining_point_data['Predicted Error']=='None')]
num_remaining_points_constant_bed_temp = remaining_points_constant_bed_temp.drop_duplicates(['gx','gy','gz']).shape[0]
remaining_points_quality = remaining_points_constant_bed_temp[remaining_points_constant_bed_temp['Predicted Error']=='None'].drop_duplicates(['Bed Temperature', 'Nozzle Temperature', 'Print Speed', 'gx','gy','gz'])
remaining_points_quality = remaining_points_quality.groupby(['Bed Temperature', 'Nozzle Temperature', 'Print Speed']).size().to_frame(name='count').reset_index()
remaining_points_quality['percent of remaining points'] = remaining_points_quality['count'].apply(lambda x: round(float(x/remaining_points.shape[0]*100),2))
print("However, we cannot change bed temperature. Without varying bed temperature, we can gain ", str(num_remaining_points_constant_bed_temp), " more None type points")
print("These points are good at the following print parameters:")
print(remaining_points_quality.sort_values('percent of remaining points', ascending=False).sort_values('Print Speed', ascending=False))
good_param_combos()
print("Of these we can gain a decent amount of points by not changing Nozzle Temperature and dropping speed down to 60.")
good_points_half = points_to_check[(points_to_check['Predicted Error']=='None') \
& (points_to_check['Bed Temperature']==best_combo['Bed Temperature'])\
& (points_to_check['Nozzle Temperature']==best_combo['Nozzle Temperature'])\
& (points_to_check['Print Speed']==60)]
good_points_half = good_points_half.drop_duplicates(['gx','gy','gz'])[['gx', 'gy', 'gz']]
#drop points that were already in good_points
good_points_half = pandas.merge(good_points_half, good_points, how = 'outer', indicator=True).query('_merge=="left_only"').drop('_merge', axis=1)
print("We have gained ", str(good_points_half.shape[0]), " good points")
# So now how many points do we have left to gain
# drop the points that are now good
remaining_points = pandas.merge(remaining_points, good_points_half, how = 'outer', indicator=True).query('_merge=="left_only"').drop('_merge', axis=1)
print("There are still ", str(remaining_points.shape[0]), " points that can be gained by varying parameters.")
print("Of these we can gain a decent amount of points by not changing Nozzle Temperature and dropping speed down to 40.")
good_points2 = points_to_check[(points_to_check['Predicted Error']=='None') \
& (points_to_check['Bed Temperature']==best_combo['Bed Temperature'])\
& (points_to_check['Nozzle Temperature']==best_combo['Nozzle Temperature'])\
& (points_to_check['Print Speed']==40)]
good_points2 = good_points2.drop_duplicates(['gx','gy','gz'])[['gx', 'gy', 'gz']]
#drop points that were already in good_points and good_points_half
good_points2 = pandas.merge(good_points2, good_points, how = 'outer', indicator=True).query('_merge=="left_only"').drop('_merge', axis=1)
good_points2 = pandas.merge(good_points2, good_points_half, how = 'outer', indicator=True).query('_merge=="left_only"').drop('_merge', axis=1)
print("We have gained ", str(good_points2.shape[0]), " good points")
# So now how many points do we have left to gain
# drop the points that are now good
remaining_points = pandas.merge(remaining_points, good_points2, how = 'outer', indicator=True).query('_merge=="left_only"').drop('_merge', axis=1)
print("There are still ", str(remaining_points.shape[0]), " points that can be gained by varying parameters.")
# select the printing parameter combinations that these points are good at
good_param_combos()
print("We can gain a decent amount of the remaining points while still not changing bed temperature or nozzle temperature if we drop print speed down to 20")
good_points3 = points_to_check[(points_to_check['Predicted Error']=='None') \
& (points_to_check['Bed Temperature']==best_combo['Bed Temperature'])\
& (points_to_check['Nozzle Temperature']==best_combo['Nozzle Temperature'])\
& (points_to_check['Print Speed']==20)]
good_points3 = good_points3.drop_duplicates(['gx','gy','gz'])[['gx', 'gy', 'gz']]
# drop points that were already in good_points and good_points2
good_points3 = pandas.merge(good_points3, good_points, how = 'outer', indicator=True).query('_merge=="left_only"').drop('_merge', axis=1)
good_points3 = pandas.merge(good_points3, good_points_half, how = 'outer', indicator=True).query('_merge=="left_only"').drop('_merge', axis=1)
good_points3 = pandas.merge(good_points3, good_points2, how = 'outer', indicator=True).query('_merge=="left_only"').drop('_merge', axis=1)
print("We have gained ", str(good_points3.shape[0]), " good points")
# So now how many points do we have left to gain
# drop the points that are now good
remaining_points = pandas.merge(remaining_points, good_points3, how = 'outer', indicator=True).query('_merge=="left_only"').drop('_merge', axis=1)
print("There are still ", str(remaining_points.shape[0]), " points that can be gained by varying parameters.")
# select the printing parameter combinations that these points are good at
good_param_combos()
# for the remaining points, let's get the highest probability of no error without changing the nozzle temp
points_to_fudge = bad_points.append(remaining_points)
print("Because ther are no more at the same nozzle temperature, we will not grab these points.")
print("This leaves a total of ", str(points_to_fudge.shape[0]), " points to fudge.")
print("We will use the probabilites of have a none error to pick the print conditions that give the highest likelihood of no error with a constant Nozzle and Bed Temperature.")
# for the remaining points let's grab the results of points without changing the nozzle temp/bed temp
bad_param_data = points_to_check[(points_to_check['Bed Temperature']==best_combo['Bed Temperature'])\
& (points_to_check['Nozzle Temperature']==best_combo['Nozzle Temperature'])]
points_to_fudge = points_to_fudge.merge(bad_param_data, how='left', on=['gx', 'gy', 'gz'])
# now grab the best parameters for the smallest risk of Blobs
best_bad_points = points_to_fudge[points_to_fudge.groupby(['gx', 'gy', 'gz'])['Blob'].transform(min) == points_to_fudge['Blob']]
# where are these bad points chosen
param_list = ['Print Speed', 'Extrusion Ratio', 'Fan Speed', 'None']
i=1
fig = plt.figure(figsize=(10,8))
for parameter in param_list:
ax = fig.add_subplot(2,2,i, projection = '3d')
ax.view_init(azim=225)
line = ax.scatter(best_bad_points['gx'].astype(float).tolist(), best_bad_points['gy'].astype(float).tolist(), best_bad_points['gz'].astype(float).tolist(), c = best_bad_points[parameter].astype(float).tolist(), cmap=cm.get_cmap('viridis', len(best_bad_points[parameter].unique())))
if parameter == 'None':
cb = plt.colorbar(line)
plt.title('Probability of None Type Error')
else:
cb = plt.colorbar(line, ticks=best_bad_points[parameter].unique())
plt.title('Changes in ' + parameter)
i+=1
plt.show()
# great, now we need to pick the best extrusion multipliers and fan speeds for the good points above
# so first grab the prediction data that matches the points and parameter conditions chosen above
good_param_data = points_to_check[(points_to_check['Predicted Error']=='None') \
& (points_to_check['Bed Temperature']==best_combo['Bed Temperature'])\
& (points_to_check['Nozzle Temperature']==best_combo['Nozzle Temperature'])\
& (points_to_check['Print Speed']==best_combo['Print Speed'])]
good_points = good_points.merge(good_param_data, how='left', on=['gx', 'gy', 'gz'])
good_half_param_data = points_to_check[(points_to_check['Predicted Error']=='None') \
& (points_to_check['Bed Temperature']==best_combo['Bed Temperature'])\
& (points_to_check['Nozzle Temperature']==best_combo['Nozzle Temperature'])\
& (points_to_check['Print Speed']==60)]
good_points_half = good_points_half.merge(good_half_param_data, how='left', on=['gx', 'gy', 'gz'])
good2_param_data = points_to_check[(points_to_check['Predicted Error']=='None') \
& (points_to_check['Bed Temperature']==best_combo['Bed Temperature'])\
& (points_to_check['Nozzle Temperature']==best_combo['Nozzle Temperature'])\
& (points_to_check['Print Speed']==40)]
good_points2= good_points2.merge(good2_param_data, how='left', on=['gx', 'gy', 'gz'])
good3_param_data = points_to_check[(points_to_check['Predicted Error']=='None') \
& (points_to_check['Bed Temperature']==best_combo['Bed Temperature'])\
& (points_to_check['Nozzle Temperature']==best_combo['Nozzle Temperature'])\
& (points_to_check['Print Speed']==20)]
good_points3= good_points3.merge(good3_param_data, how='left', on=['gx', 'gy', 'gz'])
# now they are ready to be combined
good_points = good_points.append(good_points_half, ignore_index=True)
good_points = good_points.append(good_points2, ignore_index=True)
good_points = good_points.append(good_points3, ignore_index=True)
# now grab the parameters that give the highest probability of None
best_points = good_points[good_points.groupby(['gx', 'gy', 'gz'])['None'].transform(max) == good_points['None']]
best_points = best_points.append(best_bad_points, ignore_index=True)
# these columns aren't needed
best_points = best_points.drop(columns=['Vert Norm X', 'Vert Norm Y', 'Vert Norm Z', 'Theta Vert', 'Phi Vert', 'Print Angle', 'Discrete Mean Curve', 'Gaussian Curvature'])
print(best_points)
# save this as a pickle
best_points.to_pickle("SmoothingForSpeed-1 temp.pickle") # This will be loaded later for smoothing. Look for the same filename two code chunks down.
# end timer
t1 = time.clock()
print(str(round(t1-t0,2)) + " seconds")
param_list = ['Nozzle Temperature', 'Bed Temperature', 'Print Speed', 'Extrusion Ratio', 'Fan Speed', 'None']
i=1
fig = plt.figure(figsize=(9,11))
for parameter in param_list:
ax = fig.add_subplot(3,2,i, projection = '3d')
ax.view_init(azim=225)
line = ax.scatter(best_points['gx'].astype(float).tolist(), best_points['gy'].astype(float).tolist(), best_points['gz'].astype(float).tolist(), c = best_points[parameter].astype(float).tolist(), cmap=cm.get_cmap('viridis', len(best_points[parameter].unique())))
if parameter == 'None':
cb = plt.colorbar(line)
plt.title('Probability of None Type Error')
else:
cb = plt.colorbar(line, ticks=best_points[parameter].unique())
plt.title('Changes in ' + parameter)
i+=1
plt.show()
Here are the locations of the points that could never be None at this specific Bed Temperature and Nozzle Temperature and parameters were chosen to minimize the probability of Blob:
Here are the parameters chosen for each point in order to optimize speed with the bad points chosen to minimize the probability of Blob:
In order to force errors, instead of selecting highest None probabilities, as is the case when optimizing for quality, we are selecting for the highest probabilites of Blobs, Warps, or Delaminations.
This is the code for forcing Blobs. To force Warps or Delaminations just change all instances of Blob to Warp or Delamination.
import numpy as np
import pickle
import pandas
from sklearn import preprocessing
import time
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import matplotlib.cm as cm
# load in the prediction data
points_to_check = pandas.read_pickle("AllPossiblePredictions.pickle")
# points_to_check = points_to_check.loc[0:500000,:]
# get the total number of points in the objects
points = points_to_check[['gx', 'gy', 'gz', 'Vert Norm X', 'Vert Norm Y', 'Vert Norm Z', 'Theta Vert', 'Phi Vert', 'Print Angle', 'Discrete Mean Curve', 'Gaussian Curvature']].drop_duplicates()
print("There are ", str(points.shape[0]), " points in this object")
# create a boolean for if there error is a Blob or not
points_to_check['isBlobError'] = points_to_check['Predicted Error'] == 'Blob'
# how many points are never Blob
bad_points = points_to_check.groupby(['gx','gy','gz']).filter(lambda s: s.isBlobError.sum()==0)[['gx', 'gy', 'gz']].drop_duplicates()
number_bad_points = bad_points.shape[0]
potential_printing_accuracy = round(float((points.shape[0] - number_bad_points)/points.shape[0]*100),2)
print("There are ", str(number_bad_points), " points in this object that are predicted to always print poorly.")
print("The potential printing accuracy is ", str(potential_printing_accuracy), "%")
# these columns aren't needed
good_points = points_to_check.drop(columns=['Vert Norm X', 'Vert Norm Y', 'Vert Norm Z', 'Theta Vert', 'Phi Vert', 'Print Angle', 'Discrete Mean Curve', 'Gaussian Curvature'])
# now grab the highest probability Blobs parameters for each grouop of Bed Temp and Nozzle Temp
best_points = good_points[good_points.groupby(['gx', 'gy', 'gz', 'Bed Temperature', 'Nozzle Temperature'])['Blob'].transform(max) == good_points['Blob']]
# now pick Bed Temp and Nozzle Temp group had the highest Blob Prob
bed_temp_prediction_probs = best_points.groupby(['Bed Temperature', 'Nozzle Temperature'])['Blob'].agg(["mean", "std"])
# grab that group of data for the parameters
best_points = best_points[(best_points['Bed Temperature']==bed_temp_prediction_probs['mean'].idxmax()[0]) & (best_points['Nozzle Temperature']==bed_temp_prediction_probs['mean'].idxmax()[1])]
# plot
param_list = ['Nozzle Temperature', 'Bed Temperature', 'Print Speed', 'Extrusion Ratio', 'Fan Speed', 'Blob']
i=1
fig = plt.figure(figsize=(8.5,10))
for parameter in param_list:
ax = fig.add_subplot(3,2,i, projection = '3d')
ax.view_init(azim=225)
line = ax.scatter(best_points['gx'].astype(float).tolist(), best_points['gy'].astype(float).tolist(), best_points['gz'].astype(float).tolist(), c = best_points[parameter].astype(float).tolist(), cmap=cm.get_cmap('viridis', len(best_points[parameter].unique())))
if parameter == 'Blob':
cb = plt.colorbar(line)
plt.title('Probability of Blob Type Error')
else:
cb = plt.colorbar(line, ticks=best_points[parameter].unique())
plt.title('Changes in ' + parameter)
i+=1
plt.show()
Randomly varying Extrusion Ratio and Print Speed every 2 mm revealed physical limitations of the system.
We see that changing the Print Speed comprimises the printing quality and we believe, due to the compressible fluid nature of ABS, changes in the extrusion multiplier are not immediately put in to effect. We also believe, this poor quality of changing the Print Speed is due in part to the same issue. Although extrusion multiplier is not changed in the varying print speed part, when the speed increases the rate of extrusion must increase as well. We think this extrusion rate cannot keep pace with the print speed due to the compressible fluid nature of the material. There may also be some mechanical jerking from the rapid change in speed affecting the print quality. We see a similar issue in one of our optimization tests:
Here we see very clear demarcations where Print Speed and Extrusion Multiplier have changed during the print. It is also interesting to note that Print Speed and Extrusion Multiplier tend to change together. The printing path is from the left to the right in the picture of the printed part above. So we see as print speed and extrusion multiplier are supposed to quickly drop, there is an overextrusion of material for roughly 1 cm (5 XYZ segments). Near the right edge of this printed face, Print Speed and Extrusion Multiplier are supposed to increase. Here we see an underextrusion of material for rougly 1 cm.
In order to overcome these issues due to a quick change in Print Speed and/or Extrusion Multiplier, we created a very rough technique for smoothing. This code will look at a window of segments for a single parameter and if the last value is different than the second to last value, the whole window takes incremental steps from the first value of the window to the last. So for a window size of 5:
There are some issues with this approach and future work could be done to improve the smoothing algorithm. For now, this algoithm does a nice job.
This smoothing is applied to Print Speed, Extrusion Multiplier and Fan Speed.
import numpy as np
import pickle
import pandas as pd
from sklearn import preprocessing
import time
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import matplotlib.cm as cm
# load the image classification data from pickle
data = pd.read_pickle("./mapped_SmoothingForSpeed-1 temp.pickle") # Output from the code 2 chunks above
# function to perform smoothing
def smooth(df):
windowsize = 5
for i in range(1,df.shape[0]):
if i < windowsize-1:
continue
else:
df_subset = df.iloc[i-windowsize+1:i+1]
if df_subset.iloc[-1].item() != df_subset.iloc[-2].item():
new_array = np.linspace(df_subset.iloc[0].item(), df_subset.iloc[-1].item(), num=len(df_subset))
df.iloc[i-windowsize+1:i+1] = new_array
return(df)
# function to plot the results of the smoothing
def plots(best_points):
param_list = ['Print Speed', 'Extrusion Ratio', 'Fan Speed']
for parameter in param_list:
fig = plt.figure()
ax = fig.add_subplot(111, projection = '3d')
line = ax.scatter(best_points['gx'].astype(float).tolist(), best_points['gy'].astype(float).tolist(), best_points['gz'].astype(float).tolist(), c = best_points[parameter].astype(float).tolist(), cmap=cm.get_cmap('viridis', len(best_points[parameter].unique())))
cb = plt.colorbar(line)
plt.title('Changes in ' + parameter)
plt.show()
# do the smoothing
data['Extrusion Ratio'] = smooth(data['Extrusion Ratio'])
data['Print Speed'] = smooth(data['Print Speed'])
data['Fan Speed'] = smooth(data['Fan Speed'])
# save the results
data.to_pickle("/.mapped_SmoothingForSpeed-1 temp_SMOOTH.pickle")
# plot the results
plots(data)
Here are the results of applying the smoothing algorithm to the predicted parameters for optimizing speed and quality:
Since we were only able to capture errors of the outer walls (due to the limitations we had in photographing), it was necessary to communicate the optimal printing parameters found throughout our model on the outer walls to the rest of the print (inner walls and bottom).
To associate the outer wall segments with the inner wall segments, we used another KD Tree to find the nearest outer wall segment to each inner wall segment. We then used Pandas to merge information about printing parameters with the associated Gcode segments.
For the entire bottom (including 3 layers), we looked at the predictions of our model for the bottom 3 layers of the outer wall and generalized the parameters that were seen most often there. Thus the bottom three layers of our print were always constant.
Code for this can be seen below:
from sklearn.neighbors import KDTree
import pandas as pd
import numpy as np
BOTTOM_COUNT = 3
def gcode_tree():
best_print = pd.read_pickle("/.mapped_SmoothingForSpeed-1 temp.pickle")
# Get the Z value of the BOTTOM_COUNT (3rd) layer
df_z_bottom = best_print['gz'].unique().astype(float)
df_z_bottom = pd.DataFrame(data = df_z_bottom, columns = ['Z'])
df_z_bottom = df_z_bottom.sort_values(['Z']).reset_index()
max_bottom = df_z_bottom['Z'].nsmallest(BOTTOM_COUNT)[BOTTOM_COUNT - 1]
all_segs = pd.read_csv("path/to/segmented_gcode_points.txt", header = None, names = ['gx', 'gy', 'gz'])
outer_segs = best_print[['gx', 'gy', 'gz']]
# Create a KD Tree of just the outer points and query every point segment of gcode
gcode_tree = KDTree(outer_segs)
dist, ind = gcode_tree.query(all_segs, k = 1)
ind = ind.ravel()
df_dist = pd.DataFrame(data = dist).reset_index().drop(['index'], axis = 1)
df_nn = pd.DataFrame(data = outer_segs.iloc[ind]).reset_index().drop(['index'], axis = 1)
df_dist.columns = ['Distance']
df_nn.columns = ['nx', 'ny', 'nz']
# Make a dataframe of each gcode segment and it's nearest outer wall point
# Eventually removing the bottom 3 layers from this
gcode_map = pd.DataFrame(data = all_segs, columns = ['gx', 'gy', 'gz'])
gcode_map = pd.concat([gcode_map, df_nn, df_dist], axis = 1)
# print(gcode_map) -- might be useful here
print_params = best_print[['gx', 'gy', 'gz',
'Nozzle Temperature', 'Bed Temperature', 'Print Speed',
'Extrusion Ratio', 'Fan Speed']].reset_index().drop(['index'], axis = 1)
print_params = print_params.rename(index = str, columns = {"gx": "nx", "gy": "ny", "gz": "nz"})
# Merge the gcode mapping with the associated print parameters of the outer wall
df_with_bottom = pd.merge(gcode_map, print_params, how = 'left', on = ['nx', 'ny', 'nz'])
# Cut out predictions for bottom and apply blanket prediction
df_without_bottom = df_with_bottom.loc[df_with_bottom['gz'] > max_bottom]
df_bottom = df_with_bottom.loc[df_with_bottom['gz'] <= max_bottom]
df_bottom['Nozzle Temperature'] = df_bottom['Nozzle Temperature'].value_counts().idxmax().astype(float)
df_bottom['Bed Temperature'] = df_bottom['Bed Temperature'].value_counts().idxmax().astype(float)
df_bottom['Print Speed'] = df_bottom['Print Speed'].value_counts().idxmax().astype(float)
df_bottom['Extrusion Ratio'] = df_bottom['Extrusion Ratio'].value_counts().idxmax().astype(float)
df_bottom['Fan Speed'] = df_bottom['Fan Speed'].value_counts().idxmax().astype(float)
df_final = pd.concat([df_bottom, df_without_bottom])
df_final = df_final.drop(['nx', 'ny', 'nz', 'Distance'], axis = 1)
return df_final
if __name__ == "__main__":
mapped_gcode = gcode_tree()
print(mapped_gcode)
mapped_gcode.to_pickle("./mapped_gcode_SmoothingForSpeed.pickle")
Before and after pictures of this code can also be seen here:
These pictures show that the algorithm did indeed work as intended, and provided a decent way to generalize the predictions of our model on the outer wall to every segment seen in the Gcode.
In merging with Pandas the way we did, we preserved the original order of Gcode segments in the print, thus communicating our parameter combinations to Gcode was made much simpler.
Next, these printing parameters for each segment need to be communicated to the printer via gcode. Here is a script that takes in the original gcode, cuts up the lines, and rewrites the segmented gcode with the new printing parameters for each gcode segment. In order to run this, place any mapped Gcode segments from the above sections into a folder and give the code the path to that folder. It will write the gcode for all of the pickles in that file. The code is very similar to the code used to section up the gcode, it just uses the new predicted parameters to rewrite the gcode instead of the original.
import math
import numpy as np
import pickle
import pandas
import glob, os
segment_size = 2 #mm
offset = 8 # lines
os.chdir("/path/to/Done Mapping Pickles") # (folder)
# load the point parameter info from pickle
for filename in glob.glob('*.pickle'):
data = pandas.read_pickle(filename)
bed_temperature = str(data['Bed Temperature'].unique()[0])
nozzle_temperature = str(data['Nozzle Temperature'].unique()[0])
with open(".\\3_sides_of_a_cube_print_24.gcode") as base_gcode:
# load in the base gcode
gcode_data = base_gcode.readlines()
gcode_to_write = []
# Set the overall bed temperature
gcode_data[15+offset] = "M140 S" + bed_temperature + " ; start bed heating up\n"
gcode_data[63+offset] = "M190 S" + bed_temperature + " ; wait for bed to reach printing temp\n"
gcode_data[576+offset] = "M140 S" + bed_temperature + "\n"
# Set the first nozzle temp
gcode_data[62+offset] = "M109 R" + nozzle_temperature + " ; wait for extruder to reach printing temp\n"
# Set First Extrusion Multiplier
gcode_data[67+offset] = "M221 S100\n"
# Set First Fan Speed Percent to be off
gcode_data[71+offset] = "M106 S0\n"
# Set Print Speed and Extrusion Multiplier
make_changes = False
print_finished = False
line_counter = 0
previous_extrusion_multiplier = 100.0
X_prev = 0
Y_prev = 0
current_X = 0
current_Y = 0
current_Z = 0
gcode_split_points = []
for code_line in gcode_data:
line_to_write = code_line
# make sure outside of skirt
# TO DO: changed this, check works
if ";TYPE:" in code_line and code_line != ";TYPE:SKIRT\n":
make_changes = True
# make sure not at end
if "M104 S0" in code_line:
make_changes = False
# store the current X and Y
if "X" in code_line:
current_X = float([x[1:] for x in code_line.split() if "X" in x][0])
if " Y" in code_line:
current_Y = float([y[1:] for y in code_line.split() if "Y" in y][0])
if " Z" in code_line:
current_Z = float([y[1:] for y in code_line.split() if "Z" in y][0])
#TO DO: prevent infill split up
# begin changing the line if it's not the skirt or after the print
if make_changes:
# if the line is an extrusion and movement command
if all(x in code_line for x in ['G1', 'X', 'Y', 'E']):
# grab the amount of extrudate
extrusion = float([e[1:] for e in code_line.split() if "E" in e][0])
# start segmenting the line up
# a^2 + b^2 = c^2
extrusion_distance = math.sqrt((current_X-X_prev)**2+(current_Y-Y_prev)**2)
# based on the segment sizes, how many whole segments will be needed
number_of_new_segments = max(1,round(extrusion_distance/segment_size))
# based on that many segments, how far do we have to step in the X and Y direction each time
X_step_distance = (current_X-X_prev)/number_of_new_segments
Y_step_distance = (current_Y-Y_prev)/number_of_new_segments
# store the new coordinates for the segments
new_X_coords = []
new_Y_coords = []
for i in range(0,number_of_new_segments-1):
new_X_coords.append(X_prev + X_step_distance*(i+1))
new_Y_coords.append(Y_prev + Y_step_distance*(i+1))
new_X_coords.append(current_X)
new_Y_coords.append(current_Y)
# segment the extrusion amount up
new_extrusion_amount = round(extrusion/number_of_new_segments,5)
# write these new instructions out
line_to_write = []
import numpy as np
for i in range(0,number_of_new_segments):
gx = new_X_coords[i]
gy = new_Y_coords[i]
gz = current_Z
# grab the row pertaining to the point from the data
current_row = data[(np.isclose(data['gx'],gx)) & (np.isclose(data['gy'],gy)) & (np.isclose(data['gz'],gz))]
# grab the parameters for this point
nozzle_temperature = str(current_row['Nozzle Temperature'].item())
print_speed = current_row['Print Speed'].item()
extrusion_multiplier = current_row['Extrusion Ratio'].item()
fan_speed_percent = current_row['Fan Speed'].item()
# Set Fan Speed Percent in the gcode
fan_speed_set = "M106 S" + str(int(255*fan_speed_percent/100)) + ' ; Set Fan Speed Percent\n'
# Set Hot end temp in the gcode
hot_end = "M104 S" + nozzle_temperature + " ; Set Nozzle Temp and continue without waiting\n"
# calculate the new extrusion amount
# MAKE SURE original extrusion amount was 100% !!! This depends on the base gcode used
new_extrude_amount = round(new_extrusion_amount*extrusion_multiplier/float(previous_extrusion_multiplier),5)
# write the gcode printing command
gcode_command = "G1 F" + str(int(print_speed)*60) + " X" + str(gx) \
+ " Y" + str(gy) + " E" + str(new_extrude_amount) + "\n"
line_to_write.extend([fan_speed_set, hot_end, gcode_command])
# if you need to get the new split points unccmment this line and the one writing the file below
# gcode_split_points.append(str(new_X_coords[i]) + "," + str(new_Y_coords[i]) + "," + str(current_Z) + "\n")
# store the previous X and Y
X_prev = current_X
Y_prev = current_Y
gcode_to_write.extend(line_to_write)
# write the new gcode to a file
new_filename = filename.split('.')[0] + ".gcode"
with open(new_filename, 'w') as new_gcode:
new_gcode.writelines(gcode_to_write)
# # use to write out the new gcode points
# with open("segmented_gcode_points.txt", 'w') as points:
# points.writelines(gcode_split_points)
And now we have come full circle, CAD –> .STL –> gcode –> segment match with geometric properites –> run these points through model with various printing parameters –> communicate optimum parameters back into gcode.
Below are the predicted errors of each print, the associated photographs, and the AlexNet predictions.
Print #161:
Accuracy: 95.22%
Nozzle Temperature: 235 °C, Bed Temperature: 100 °C, Print Speed: 60 mm/s, Extrusion Multiplier: 110%, Fan Speed: 80%
Print #173:
Accuracy: 82.56%
Nozzle Temperature: 245 °C, Bed Temperature: 120 °C, Print Speed: 60 mm/s, Extrusion Multiplier: 100%, Fan Speed: 30%
Print #239:
Accuracy: 84.46%
Nozzle Temperature: 245 °C, Bed Temperature: 100 °C, Print Speed: 40 mm/s, Extrusion Multiplier: 100%, Fan Speed: 30%
Print #274:
Accuracy: 57.09%
Nozzle Temperature: 245 °C, Bed Temperature: 100 °C, Print Speed: 20 mm/s, Extrusion Multiplier: 90%, Fan Speed: 45%
The average accuracy of just these four prints is 79.83%. With more prints, we expect this accuracy to roughly fall to the 73.69%. accuracy of the test set.
The above pictures, as well as pictures not shown, indicate that our model performs worst at predicting prints with a 20 mm/sec print speed. This is likely due to both the randomness in delaminations compared with other errors, as well as the fact that our AlexNet Image Classification model seemed to often incorrectly classify delaminations. These two, in combination, meant our model was good at predicting that delaminations can occur with low print speeds, but not very good at saying where they occur.
The results of optimizing for quality can be seen below:
We printed this part twice, and it terms of quality, they ranked 6th and 12th out of 146 prints according to our Image Classification Model. This print took 24 minutes 13 seconds to complete. Comparing this with each time for baseline prints, we get:
Quality Optimizations: 24:13
Note: As mentioned previously, the AlexNet Image Classification Model was dependent on our own interpretation errors. Outputs of these rankings should only be used to roughly get an idea of where the prints fall.
In addition, we can look at the average percentage of each print classified (by our Image Classification model) to not have an error. This can be compared with our optimized-for-quality print below:
The print time of the different Print Speeds and our Optimized for Quality print and their Image Classification Qualities.
This is fantastic news, as this means the use of machine learning in 3D printing is an extremely valid tactic!
In optimizing for speed, our goal, broadly, was to keep as high of speed as possible while also decreasing errors. Results of our speed optimizations can be seen below:
Our print with local speed optimizations took 16 minutes 56 seconds to complete. This can be compared to baseline prints - each with speed and time:
20 mm/s: 45:27 40 mm/s: 23:31 60 mm/s: 16:03 80 mm/s: 12:32 *Our print: 16:56
Reiterating the average percentages of no error at each print speed from above, and including our optimized-for-speed print, we get:
20 mm/s: 74.21% 40 mm/s: 76.04% 60 mm/s: 72.20% 80 mm/s: 69.59% *Speed optimizations: 76.61%
The print time of the different Print Speeds, Optimized for Quality print, and Optimized for Speed print and their Image Classification Qualities.
The constant print speed percentages fall in line with what we expect - that there’s a sweet spot in speed that isn’t too slow, but isn’t too fast.
More importantly, our print optimizing speed still outranks the average quality of each constant print speed.
Again, this is good news for machine learning in 3D printing!
Forcing each error type is also a great measure of our model’s predictive power. As a reminder, here is our model’s attempt at forcing blobs to occur:
In creating these localized optimizations, we’ve modified previous segment information in a way that our model can’t account for. What might work best for constant parameters is not guaranteed to work if the parameters are constantly being varied. For example, going quickly from low print speeds to high print speeds around the corners actually had a negative impact on forcing blobs due to fact that quickly speeding up had the effect of thinning out the material.
Broadly, the goal of this internship was to lay the groundwork for the use of Machine Learning to increase the quality and reliability of Additive Manufacturing (3D printing). The end goal of this project was to create code and models to take in a CAD design and optimize the printing parameters throughout the print. More specifically, in this internship we aimed to create the data, use the data to train models, and predict the optimum printing parameters at each location in a print. In this work, wepredicted future print quality with ~80% accuracy, predicted local optimum printing parameters throughout the part to optimize for Quality, obtaining None Error rates in the top 10% of our data, and optimized for Speed resulting in a 30% reduction in print time, while maintaining a high None Error rate.
We developed a suite of software that can be used to enhance 3D printing, as well as documentation to support this software. The documentation includes detailed descriptions of the decisions that were made throughout the development process and that enable future improvements to the software. The results of this project lay a groundwork for and are an encouraging step towards using Machine Learning to aid certification of printed parts, a requirement for advancing the use of AM for aerospace applications.
A special thank you to Mia Siochi, Kris Wise, and Ben Jensen, who allowed us to ramble for an hour every week and gave absolutely invaluable insight to help advance the work. Thank you to Chris Stelter for the interesting conversations and insights. Thank you to David Anderegg, our fellow intern who helped us throughout the learning process of 3D Printing. Thank you to Jae-Woo Kim, who I believe we chased out of his office. Thank you to Sean Zylich and Evan Rose for their amazing work on the Image Classification in their 1 month here. Thank you to Christine Linsinbigler, Christine Dillard and Valerie Ellis, our intern coordinators who always kept an eye out for us before and during our time here. Thank you to Forrest Baber for his interest in our project. And a thanks to everyone in the building who opened doors for us before we had badges, smiled at us in the hallways, made coffee in the morning, brought donuts anonymously, sparked up conversations and all around made us feel welcome at NASA. And finally, We would like to thank our mentors: Godfrey Sauti and John Gardner. You came up with an amazing idea and we feel so lucky to have been a part of this project. Thank you so much for chosing us for this project and all of your advice, life, career and project based. You will be missed and never forgotten.
Kevin Hunt: kevin.ann.hunt@gmail.com 260.450.8674
Austin Ebel: austinebel3@gmail.com, abe2122@columbia.edu 757.876.3312
5.2.4 Comments
Firstly, this is very intriguing as it’s getting into an aspect of 3D printing not yet explored. In addition, our quick and dirty smoothing algorithm significantly improved the performance in this regard. Looking into more advanced smoothing techniques/optimizations will absolutely improve performance. Finally, and what’s beautiful about machine learning in this regard, is that adding this print to the training data (while also adding previous segments’ parameter information) will result in a model that begins to understand these interplay between the parameters with localized optimizations.