Commit c8c4d501 authored by Chetan Sharma's avatar Chetan Sharma
Browse files

Fixed some sensor issues, model issues

parent 113d8b1d
......@@ -3,43 +3,9 @@
*.unfinished*
private/
.vscode/
saved_cuts/
*~$*
# C++ Files
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
# Python Files
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
......@@ -62,7 +28,6 @@ parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
......@@ -92,6 +57,7 @@ coverage.xml
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
......@@ -114,6 +80,7 @@ instance/
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
......@@ -124,7 +91,9 @@ profile_default/
ipython_config.py
# pyenv
.python-version
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
......@@ -168,4 +137,10 @@ venv.bak/
dmypy.json
# Pyre type checker
.pyre/
\ No newline at end of file
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
\ No newline at end of file
......@@ -8,6 +8,10 @@ The current goal of this project is to make a system that faces a material using
## Modeling
Models for forces experienced during the cutting process and models for tool / machine failure are in [software/models.py](software/models.py).
The linear model converges well when given test data sweeps.
![](assets/model_converge.png)
## Hardware
The hardware setup is finished. The machine is a Taig Micro Mill (kindly donated by Ted Hall).
......@@ -26,4 +30,6 @@ The machine electronics are enclosed for safety.
![](assets/cabinet.jpg)
## Software
The machine controller is in the [software](software/) folder.
\ No newline at end of file
The machine controller is in the [software](software/) folder.
......@@ -15,7 +15,7 @@ void loop() {
if (Serial.available() > 0) {
char inByte = Serial.read();
if (inByte == 'F') {
float weight = loadcell.get_value();
float weight = -loadcell.get_value();
Serial.println(weight);
} else if (inByte == 'T') {
loadcell.tare();
......
"""
Intended to be the brain box of this project; for now, it just runs a test sweep to collect initial data.
"""
import numpy as np
import time
from objects import EndMill, Conditions, MachineChar
from cut import Cut
......@@ -6,4 +10,26 @@ from ml import LinearModel
from optimize import Optimizer
import logging
logging.basicConfig(level="INFO")
\ No newline at end of file
logging.basicConfig(level="INFO")
MACHINE_PORT = '/dev/ttyS25'
SPINDLE_PORT = '/dev/ttyS33'
TFD_PORT = '/dev/ttyS35'
endmill = EndMill(3, 3.175e-3, 3.175e-3, 19.05e-3, 1e-3)
cut = Cut(MACHINE_PORT, SPINDLE_PORT, TFD_PORT, endmill, 80e-3, 50.8e-3, 5e-3, 300, save_as = "6061-sweep")
f_r_range = np.linspace(2e-3, 5e-3, 9)
W_range = np.linspace(1e-3, 3.175e-3, 10)
cut.face_layer(D = 0.3e-3)
cut.begin_layer(D = 1e-3)
for f_r in f_r_range:
for W in W_range:
conditions = Conditions(1e-3, W, f_r, 300, endmill)
cut.cut(conditions, save = True, auto_layer=True)
cut.close()
\ No newline at end of file
......@@ -16,8 +16,10 @@ class MachineCrash(Exception):
class Cut:
def __init__(self, machine_port, spindle_port, tfd_port, endmill, x_max, y_max, initial_z=0, save_as=None):
def __init__(self, machine_port, spindle_port, tfd_port, endmill, x_max, y_max, f_r_clearing, w_clearing, initial_z=0, save_as=None):
self.machine = Machine(machine_port)
self.machine.unlock()
self.machine.zero()
self.spindle = Spindle_Applied(spindle_port)
self.tfd = TFD(tfd_port)
log.info("Initialized all systems.")
......@@ -27,44 +29,92 @@ class Cut:
self.x_max = x_max
self.y_max = y_max
self.cut_x = 0
self.cut_z = initial_z
self.cut_z = -initial_z # initial z in positive units
self.D = 0
self.f_r_clearing = f_r_clearing
self.w_clearing = w_clearing
self.save_as = save_as
# deriving some constants
self.Y_START = - 2 * endmill.R
self.Y_END = y_max + 2 * endmill.R
self.X_END = x_max - endmill.R
self.Y_DATA_START = 1.5 * endmill.R
self.Y_DATA_END = y_max - 1.5 * endmill.R
self.Y_START = - 2 * endmill.r_c
self.Y_END = y_max + 2 * endmill.r_c
self.X_START = endmill.r_c
self.X_END = x_max - endmill.r_c
self.Y_DATA_START = 1.5 * endmill.r_c
self.Y_DATA_END = y_max - 1.5 * endmill.r_c
def __del__(self):
self.spindle.set_w(0)
def begin_layer(self, D, f_r_clearing, w_clearing):
def warmup_spindle(self):
log.info("Warming up spindle")
self.spindle.set_w(300)
time.sleep(30)
self.spindle.set_w(0)
log.info("Finished warming spindle")
def face_layer(self, D):
"""
Prepares to start facing with cut depth D. Successive calls will compensate for previous cut depths.
Faces a layer to ensure that all cuts afterwards are even.
Args:
D: Depth of cut for this layer.
f_r_clearing: feedrate used for this clearing pass.
w_clearing: spindle speed used for this clearing pass.
"""
log.info("Preparing to face layer to depth " + str(D) +
" at feedrate " + str(self.f_r_clearing) + " with speed " + str(self.w_clearing))
# define next cut
self.D = D
self.cut_z -= D
cuts = np.append(np.arange(self.X_START, self.X_END, 1.8 * self.endmill.r_c), self.X_END)
# perform cut
self.spindle.set_w(self.w_clearing)
self.machine.rapid({'x': self.X_START, 'y': self.Y_START})
self.machine.rapid({'z': self.cut_z})
location = 'BOTTOM'
for x in cuts:
self.machine.rapid({'x': x})
if location == 'BOTTOM':
self.machine.cut({'y': self.Y_END}, self.f_r_clearing)
location = 'TOP'
elif location == 'TOP':
self.machine.cut({'y': self.Y_START}, self.f_r_clearing)
location = 'BOTTOM'
self.machine.rapid({'z': self.cut_z + D + 1e-3})
self.machine.rapid({'x': self.X_START, 'y': self.Y_START})
self.machine.rapid({'z': self.cut_z})
self.machine.hold_until_still()
log.info("Workpiece faced")
def begin_layer(self, D):
"""
Prepares to start facing with cut depth D. Successive calls will compensate for previous cut depths.
Args:
D: Depth of cut for this layer (in positive units)
f_r_clearing: feedrate used for this clearing pass.
w_clearing: spindle speed used for this clearing pass.
Returns:
A data blob from this operation.
"""
X_START = self.endmill.R
log.info("Preparing to clear layer to depth " + str(D) +
" at feedrate " + str(f_r_clearing) + " with speed " + str(w_clearing))
" at feedrate " + str(self.f_r_clearing) + " with speed " + str(self.w_clearing))
self.D = D
self.cut_z -= D
self.spindle.set_w(w_clearing)
self.spindle.set_w(self.w_clearing)
self.machine.rapid({'x': X_START, 'y': self.Y_START})
self.machine.rapid({'x': self.X_START, 'y': self.Y_START})
self.machine.rapid({'z': self.cut_z})
self.machine.cut({'y': self.Y_END}, f_r_clearing)
self.machine.rapid({'z': self.cut_z + D + 1})
self.machine.rapid({'x': X_START, 'y': self.Y_START})
self.machine.cut({'y': self.Y_END}, self.f_r_clearing)
self.machine.rapid({'z': self.cut_z + D + 1e-3})
self.machine.rapid({'x': self.X_START, 'y': self.Y_START})
self.machine.rapid({'z': self.cut_z})
self.machine.hold_until_still()
......@@ -72,15 +122,36 @@ class Cut:
log.info("Layer prepared for clearing")
def cut(self, conditions):
def cut(self, conditions, save = True, auto_layer = True):
"""
Performs a stroke of facing. Returns a data blob.
"""
_, W, f_r, w, _ = conditions.unpack()
self.spindle.set_w(w)
X_START = self.x_cut - self.endmill.r_c + W
if X_START > self.X_END:
raise MachineCrash(
"Cutting too far in X direction: X = " + str(X_START))
if auto_layer:
log.info("Hit end of travel, polishing off remaining bits.")
# just finish off the rest of the layer
self.machine.rapid({'x': self.X_END, 'y': self.Y_START})
self.machine.rapid({'z': self.cut_z})
self.machine.cut({'y': self.Y_END}, self.f_r_clearing)
self.machine.rapid({'z': self.cut_z + self.D + 1e-3})
self.machine.hold_until_still()
# start next layer
self.begin_layer(self.D)
log.info("Actually performing cut now.")
# try again, return result
return self.cut(conditions, save = save, auto_layer=False)
else:
raise MachineCrash(
"Cutting too far in X direction: X = " + str(X_START))
outQueue = PriorityQueue()
......@@ -103,7 +174,7 @@ class Cut:
lambda state: state['wpos']['y'] > self.Y_DATA_START)
log.info("Beginning data collection")
time_start = time.perf_counter
time_start = time.perf_counter()
self.spindle.start_measurement(outQueue, time_start)
self.tfd.start_measurement(outQueue, time_start)
self.machine.hold_until_condition(
......@@ -114,22 +185,36 @@ class Cut:
self.tfd.stop_measurement()
log.info("Finished cut, collected " +
str(len(outQueue)) + " data points.")
str(outQueue.qsize()) + " data points.")
self.machine.rapid({'z': self.cut_z + self.D + 1})
self.machine.rapid({'z': self.cut_z + self.D + 1e-3})
self.machine.rapid({'y': self.Y_START})
self.machine.hold_until_still()
self.x_cut += W
Ts, Fys = self.dump_queue(outQueue)
data = Data(self.D, W, f_r, w, self.endmill, Ts, Fys)
if self.save_as:
if self.save_as and save:
with shelve.open(os.path.join("saved_cuts", "db")) as db:
if self.save_as in db:
db[self.save_as].append(data)
existing = db[self.save_as]
existing.append(data)
db[self.save_as] = existing
else:
db[self.save_as] = [data]
db.sync()
log.info("Data saved under name " + self.save_as)
return data
def close(self):
log.info("Returning home before closing")
self.machine.rapid({'x': 0, 'y': 0, 'z': 0})
time.sleep(0.1)
self.machine.hold_until_still()
def dump_queue(self, outQueue):
Ts, Fys = list(), list()
......@@ -137,14 +222,13 @@ class Cut:
t, datum = outQueue.get()
datum_type, datum_value = datum
if datum_type == 'spindle':
Ts.append(t, datum_value)
Ts.append([t, datum_value])
elif datum_type == 'tfd':
Fys.append(t, datum_value)
Fys.append([t, datum_value])
return np.array(Ts), np.array(Fys)
if __name__ == "__main__":
endmill = EndMill(3, 3.175e-3, 3.175e-3, 0.019, 0.004)
cut = Cut('/dev/ttyS28', '/dev/ttyS26', 'dev/ttyS27', endmill, 50e-3, 50e-3)
log.info("Hi")
......@@ -4,7 +4,7 @@ import numpy as np
from sklearn import linear_model
from matplotlib import pyplot as plt
from models import T_lin, F_lin, T_x_vector, Fy_x_vector
from models import T_lin, F_lin, T_x_vector, T_x_vector_padded, Fy_x_vector
from objects import Data, Conditions, EndMill, Prediction
......@@ -22,7 +22,8 @@ class Model(abc.ABC):
pass
def ingest_data(self, data):
return list(map(self.ingest_datum, data))
for datum in data:
self.ingest_datum(datum)
@abc.abstractmethod
def predict_one(self, conditions):
......@@ -32,7 +33,7 @@ class Model(abc.ABC):
pass
def predict(self, conditions):
return list(map(conditions, self.predict_one))
return list(map(self.predict_one, conditions))
class LinearModel(Model):
......@@ -49,7 +50,7 @@ class LinearModel(Model):
def ingest_datum(self, datum):
# decompose
_, _, _, _, _, Ts, Fys = datum.unpack()
T, Fy = np.median(Ts[1, :]), np.median(Fys[1, :])
T, Fy = np.median(Ts[:, 1]), np.median(Fys[:, 1])
# get linear coefficients
T_x = T_x_vector(datum.conditions())
Fy_x = Fy_x_vector(datum.conditions())
......@@ -87,3 +88,100 @@ class LinearModel(Model):
# repack and return
return Prediction(*conditions.unpack(), T, F)
class UnifiedLinearModel(Model):
def __init__(self):
self.training_Tx = list()
self.training_Ty = list()
self.training_Fyx = list()
self.training_Fyy = list()
self.regressor = linear_model.LinearRegression(fit_intercept=False)
self.params = np.array([0, 0, 0, 0], dtype='float64')
def ingest_datum(self, datum):
# decompose
_, _, _, _, _, Ts, Fys = datum.unpack()
T, Fy = np.median(Ts[:, 1]), np.median(Fys[:, 1])
# get linear coefficients
T_x = np.array(T_x_vector_padded(datum.conditions()))
Fy_x = np.array(Fy_x_vector(datum.conditions()))
# we want to artificially inflate T to be as big as F
# this is a little arbitrary, might not be the best idea lol
ratio = Fy_x[:2].mean() / T_x[:2].mean()
T_x *= ratio
T *= ratio
# add T to training set
self.training_Tx.append(T_x)
self.training_Ty.append(T)
# add Fy to training set
self.training_Fyx.append(Fy_x)
self.training_Fyy.append(Fy)
self.update()
def update(self):
# calculate best fit from data
self.regressor.fit(self.training_Tx + self.training_Fyx, self.training_Ty + self.training_Fyy)
self.params = np.array(self.regressor.coef_)
def predict_one(self, conditions):
# evaluate
T = T_lin(conditions, *self.params[:2])
F = F_lin(conditions, *self.params)
# repack and return
return Prediction(*conditions.unpack(), T, F)
class RANSACLinearModel(Model):
def __init__(self):
self.training_Tx = list()
self.training_Ty = list()
self.training_Fyx = list()
self.training_Fyy = list()
base_regressor = linear_model.LinearRegression(fit_intercept=False)
self.regressor = linear_model.RANSACRegressor(base_regressor, min_samples=5)
self.params = np.array([0, 0, 0, 0], dtype='float64')
def ingest_datum(self, datum):
# decompose
_, _, _, _, _, Ts, Fys = datum.unpack()
T, Fy = np.median(Ts[:, 1]), np.median(Fys[:, 1])
# get linear coefficients
T_x = np.array(T_x_vector_padded(datum.conditions()))
Fy_x = np.array(Fy_x_vector(datum.conditions()))
# we want to artificially inflate T to be as big as F
# this is a little arbitrary, might not be the best idea lol
ratio = Fy_x[:2].mean() / T_x[:2].mean()
T_x *= ratio
T *= ratio
# add T to training set
self.training_Tx.append(T_x)
self.training_Ty.append(T)
# add Fy to training set
self.training_Fyx.append(Fy_x)
self.training_Fyy.append(Fy)
if (len(self.training_Tx) + len(self.training_Fyx)) > 4:
self.update()
def update(self):
# calculate best fit from data
self.regressor.fit(self.training_Tx + self.training_Fyx, self.training_Ty + self.training_Fyy)
self.params = np.array(self.regressor.estimator_.coef_)
def predict_one(self, conditions):
# evaluate
T = T_lin(conditions, *self.params[:2])
F = F_lin(conditions, *self.params)
# repack and return
return Prediction(*conditions.unpack(), T, F)
......@@ -75,6 +75,18 @@ def T_x_vector(conditions: Conditions):
return[(D * N * W * f_t) / (2 * np.pi), (D * N * R * np.arccos(1 - (W / R))) / (2 * np.pi)]
def T_x_vector_padded(conditions: Conditions):
"""
Outputs vector that corresponds to coefficients in order:
K_tc, K_te, K_rc, K_re
Note that last two coefficents are 0 no matter what.
"""
D, W, f_r, w, endmill = conditions.unpack()
N, R, _, _, _ = endmill.unpack()
f_t = calc_f_t(conditions)
return[(D * N * W * f_t) / (2 * np.pi), (D * N * R * np.arccos(1 - (W / R))) / (2 * np.pi), 0, 0]
def Fy_x_vector(conditions: Conditions):
D, W, f_r, w, endmill = conditions.unpack()
......
......@@ -21,9 +21,10 @@ def test_tfd():
def test_spindle():
spindle = Spindle_Applied(SPN_port)
spindle.set_w(400)
spindle.calibrate()
spindle.set_w(300)
# spindle.calibrate()
while True: pass
print("Calibrated to ", spindle.torque_loss)
spindle.set_w(0)
test_spindle()
\ No newline at end of file
test_spindle()
......@@ -12,6 +12,9 @@ log = logging.getLogger(__name__)
class Sensor(ABC):
"""
Represents a sensor used for data collection. Implements a basic set of multithreaded data collection subroutines.
"""
def __init__(self):
self.sensorThread = None
self.measure = threading.Event()
......@@ -143,12 +146,12 @@ class Machine(Sensor):
others = msgs[1:]
reports = {other.split(":")[0]: other.split(":")[1]
for other in others}
x, y, z = [float(i * 1e-3) for i in reports["WPos"].split(",")]
x, y, z = [float(i) * 1e-3 for i in reports["WPos"].split(",")]
feed, _ = reports["FS"].split(",")
dx, dy, dz = [cur - last for cur,
last in zip((x, y, z), self.last_position)]
self.last_position = (x, y, z)
return {'state': state, 'wpos': {'x': x, 'y': y, 'z': z}, 'feed': feed / 60000, 'direction': {'dx': dx, 'dy': dy, 'dz': dz}}
return {'state': state, 'wpos': {'x': x, 'y': y, 'z': z}, 'feed': float(feed) / 60000, 'direction': {'dx': dx, 'dy': dy, 'dz': dz}}
def hold_until_still(self):
self._logger.info("Holding until Idle")
......@@ -246,17 +249,41 @@ class TFD(Sensor):
self._logger.info("TFD Ready")
def get_force(self):
self.port.write("F".encode('ascii'))
self.port.flush()
resp = self.port.readline().decode('ascii').strip()
return float(resp) / TFD.BITS_TO_N
for i in range(5):
e = "No exception..."
try:
self.port.write("F".encode('ascii'))
self.port.flush()
resp = self.port.readline().decode('ascii').strip()
return float(resp) / TFD.BITS_TO_N
except Exception as ex:
e = ex
self._logger.warn("Read #" + str(i + 1) + " failed, trying again...")
self._logger.warn("Specific error: " + str(e))
self.port.reset_input_buffer()
self.port.reset_output_buffer()
raise IOError("Failed to get force from TFD")
def calibrate(self):
self.port.write("T".encode('ascii'))
self.port.flush()
while True:
if "TARED" in self.port.readline().decode('ascii'):
return
for i in range(5):
e = "No exception"
try:
self.port.write("T".encode('ascii'))
self.port.flush()
for _ in range(5):