Right.
Right - it should be simpler. That said, I do think it would be worthwhile to look into a similar “non-linear least squares” system for finding the contact point.
I made a change to the upstream code to move the linear squares code out of the probe_eddy_current EddyTap() class and into a new TapBestFit() class (commit ccdb7717). Hopefully this will make that code a little easier to understand in the future.
For what it is worth, I’d guess a “best fit” mechanism for load-cell would look something like the code below. This is totally untested, but maybe it gives some ideas. This sample code also does not implement least squares caching, but that could be added later if desired. (Caching improves the algorithm from O(n*n) complexity to O(n) complexity.)
Cheers,
-Kevin
# Given a list of (grams, z) pairs, find the coefficients z_contact,
# grams_contact, depress_slope, slope that best fit the data to the
# formulas `grams = grams_contact + depress_slope*(z-z_contact)` when
# z<=z_contact and `grams = grams_contact + slope*(z-z_contact)` when
# z>=z_contact. This implements a form of non-linear least squares.
class LCBestFit:
def _calc_least_squares(self, samples, est_z_contact):
len_samples = len(samples)
eqs = [[0.] * 3 for i in range(len_samples)]
ans = [[0.] for i in range(len_samples)]
for i, (step_z, sensor_grams) in enumerate(samples):
a = ans[i]
eq = eqs[i]
if step_z <= est_z_contact:
# 1*c0 + (z-ezc)*c1 = grams
eq[0] = 1.
eq[1] = step_z - est_z_contact
a[0] = sensor_grams
else:
# 1*c0 + (z-ezc)*c2 = grams
eq[0] = 1.
eq[1] = step_z - est_z_contact
a[0] = sensor_grams
eqst = mathutil.mat_transp(eqs)
eqst_eqs = mathutil.mat_mat_mul(eqst, eqs)
eqst_ans = mathutil.mat_mat_mul(eqst, ans)
coeffs = mathutil.gaussian_solve(eqst_eqs, eqst_ans)
if coeffs is None:
return sys.float_info.max, [[0.]] * 3
rel_err = -sum([c[0]*a[0] for c, a in zip(coeffs, eqst_ans)])
return rel_err, coeffs
def find_best_fit(self, data):
#for d in data:
# logging.info("sample: grams=%.3f z=%.6f", d[0], d[1][2])
# Change base of grams/z measurements to improve numerical stability
base_z = .5 * (data[0][1][2] + data[-1][1][2])
base_grams = .5 * (data[0][0] + data[-1][0])
samples = [(d[1][2] - base_z, d[0] - base_grams) for d in data]
# Run least squares with various z values to reduce residual error
min_z = best_z = samples[0][0]
max_z = samples[-1][0]
best_err = sys.float_info.max
best_coeffs = [0., 0., 0.]
while max_z - min_z > 0.000050:
# Select z value to check
mid_z = (min_z + max_z) * .5
if best_z < mid_z:
guess_z = (best_z + max_z) * .5
else:
guess_z = (min_z + best_z) * .5
# Calculate least squares error for given z
guess_err, coeffs = self._calc_least_squares(samples, guess_z)
# Update search bounds
if guess_err < best_err:
if guess_z > best_z:
min_z = best_z
else:
max_z = best_z
best_z = guess_z
best_err = guess_err
best_coeffs = coeffs
else:
if guess_z > best_z:
max_z = guess_z
else:
min_z = guess_z
# Return to original grams/z measurement base
bc = [v[0] for v in best_coeffs]
z_contact = base_z + best_z
grams_contact = base_grams + bc[0]
depress_slope = bc[1]
slope = bc[2]
#logging.info("probe_analysis: coeffs=%s",
# (z_contact, grams_contact, depress_slope, slope))
return z_contact, grams_contact, depress_slope, slope
