diff options
Diffstat (limited to 'klippy/extras/shaper_calibrate.py')
-rw-r--r-- | klippy/extras/shaper_calibrate.py | 288 |
1 files changed, 182 insertions, 106 deletions
diff --git a/klippy/extras/shaper_calibrate.py b/klippy/extras/shaper_calibrate.py index f497171f..117409aa 100644 --- a/klippy/extras/shaper_calibrate.py +++ b/klippy/extras/shaper_calibrate.py @@ -4,21 +4,23 @@ # # This file may be distributed under the terms of the GNU GPLv3 license. import collections, importlib, logging, math, multiprocessing, traceback -shaper_defs = importlib.import_module('.shaper_defs', 'extras') -MIN_FREQ = 5. -MAX_FREQ = 200. +shaper_defs = importlib.import_module(".shaper_defs", "extras") + +MIN_FREQ = 5.0 +MAX_FREQ = 200.0 WINDOW_T_SEC = 0.5 -MAX_SHAPER_FREQ = 150. +MAX_SHAPER_FREQ = 150.0 -TEST_DAMPING_RATIOS=[0.075, 0.1, 0.15] +TEST_DAMPING_RATIOS = [0.075, 0.1, 0.15] -AUTOTUNE_SHAPERS = ['zv', 'mzv', 'ei', '2hump_ei', '3hump_ei'] +AUTOTUNE_SHAPERS = ["zv", "mzv", "ei", "2hump_ei", "3hump_ei"] ###################################################################### # Frequency response calculation and shaper auto-tuning ###################################################################### + class CalibrationData: def __init__(self, freq_bins, psd_sum, psd_x, psd_y, psd_z): self.freq_bins = freq_bins @@ -27,9 +29,14 @@ class CalibrationData: self.psd_y = psd_y self.psd_z = psd_z self._psd_list = [self.psd_sum, self.psd_x, self.psd_y, self.psd_z] - self._psd_map = {'x': self.psd_x, 'y': self.psd_y, 'z': self.psd_z, - 'all': self.psd_sum} + self._psd_map = { + "x": self.psd_x, + "y": self.psd_y, + "z": self.psd_z, + "all": self.psd_sum, + } self.data_sets = 1 + def add_data(self, other): np = self.numpy joined_data_sets = self.data_sets + other.data_sets @@ -37,45 +44,55 @@ class CalibrationData: # `other` data may be defined at different frequency bins, # interpolating to fix that. other_normalized = other.data_sets * np.interp( - self.freq_bins, other.freq_bins, other_psd) + self.freq_bins, other.freq_bins, other_psd + ) psd *= self.data_sets - psd[:] = (psd + other_normalized) * (1. / joined_data_sets) + psd[:] = (psd + other_normalized) * (1.0 / joined_data_sets) self.data_sets = joined_data_sets + def set_numpy(self, numpy): self.numpy = numpy + def normalize_to_frequencies(self): for psd in self._psd_list: # Avoid division by zero errors - psd /= self.freq_bins + .1 + psd /= self.freq_bins + 0.1 # Remove low-frequency noise - low_freqs = self.freq_bins < 2. * MIN_FREQ + low_freqs = self.freq_bins < 2.0 * MIN_FREQ psd[low_freqs] *= self.numpy.exp( - -(2. * MIN_FREQ / (self.freq_bins[low_freqs] + .1))**2 + 1.) - def get_psd(self, axis='all'): + -((2.0 * MIN_FREQ / (self.freq_bins[low_freqs] + 0.1)) ** 2) + 1.0 + ) + + def get_psd(self, axis="all"): return self._psd_map[axis] CalibrationResult = collections.namedtuple( - 'CalibrationResult', - ('name', 'freq', 'vals', 'vibrs', 'smoothing', 'score', 'max_accel')) + "CalibrationResult", + ("name", "freq", "vals", "vibrs", "smoothing", "score", "max_accel"), +) + class ShaperCalibrate: def __init__(self, printer): self.printer = printer self.error = printer.command_error if printer else Exception try: - self.numpy = importlib.import_module('numpy') + self.numpy = importlib.import_module("numpy") except ImportError: raise self.error( - "Failed to import `numpy` module, make sure it was " - "installed via `~/klippy-env/bin/pip install` (refer to " - "docs/Measuring_Resonances.md for more details).") + "Failed to import `numpy` module, make sure it was " + "installed via `~/klippy-env/bin/pip install` (refer to " + "docs/Measuring_Resonances.md for more details)." + ) def background_process_exec(self, method, args): if self.printer is None: return method(*args) import queuelogger + parent_conn, child_conn = multiprocessing.Pipe() + def wrapper(): queuelogger.clear_bg_logging() try: @@ -86,6 +103,7 @@ class ShaperCalibrate: return child_conn.send((False, res)) child_conn.close() + # Start a process to perform the calculation calc_proc = multiprocessing.Process(target=wrapper) calc_proc.daemon = True @@ -95,10 +113,10 @@ class ShaperCalibrate: gcode = self.printer.lookup_object("gcode") eventtime = last_report_time = reactor.monotonic() while calc_proc.is_alive(): - if eventtime > last_report_time + 5.: + if eventtime > last_report_time + 5.0: last_report_time = eventtime gcode.respond_info("Wait for calculations..", log=False) - eventtime = reactor.pause(eventtime + .1) + eventtime = reactor.pause(eventtime + 0.1) # Return results is_err, res = parent_conn.recv() if is_err: @@ -115,12 +133,13 @@ class ShaperCalibrate: shape = (window_size, n_windows) strides = (x.strides[-1], step_between_windows * x.strides[-1]) return self.numpy.lib.stride_tricks.as_strided( - x, shape=shape, strides=strides, writeable=False) + x, shape=shape, strides=strides, writeable=False + ) def _psd(self, x, fs, nfft): # Calculate power spectral density (PSD) using Welch's algorithm np = self.numpy - window = np.kaiser(nfft, 6.) + window = np.kaiser(nfft, 6.0) # Compensation for windowing loss scale = 1.0 / (window**2).sum() @@ -138,13 +157,13 @@ class ShaperCalibrate: # For one-sided FFT output the response must be doubled, except # the last point for unpaired Nyquist frequency (assuming even nfft) # and the 'DC' term (0 Hz) - result[1:-1,:] *= 2. + result[1:-1, :] *= 2.0 # Welch's algorithm: average response over windows psd = result.real.mean(axis=-1) # Calculate the frequency bins - freqs = np.fft.rfftfreq(nfft, 1. / fs) + freqs = np.fft.rfftfreq(nfft, 1.0 / fs) return freqs, psd def calc_freq_response(self, raw_values): @@ -160,7 +179,7 @@ class ShaperCalibrate: data = np.array(samples) N = data.shape[0] - T = data[-1,0] - data[0,0] + T = data[-1, 0] - data[0, 0] SAMPLING_FREQ = N / T # Round up to the nearest power of 2 for faster FFT M = 1 << int(SAMPLING_FREQ * WINDOW_T_SEC - 1).bit_length() @@ -169,17 +188,19 @@ class ShaperCalibrate: # Calculate PSD (power spectral density) of vibrations per # frequency bins (the same bins for X, Y, and Z) - fx, px = self._psd(data[:,1], SAMPLING_FREQ, M) - fy, py = self._psd(data[:,2], SAMPLING_FREQ, M) - fz, pz = self._psd(data[:,3], SAMPLING_FREQ, M) - return CalibrationData(fx, px+py+pz, px, py, pz) + fx, px = self._psd(data[:, 1], SAMPLING_FREQ, M) + fy, py = self._psd(data[:, 2], SAMPLING_FREQ, M) + fz, pz = self._psd(data[:, 3], SAMPLING_FREQ, M) + return CalibrationData(fx, px + py + pz, px, py, pz) def process_accelerometer_data(self, data): calibration_data = self.background_process_exec( - self.calc_freq_response, (data,)) + self.calc_freq_response, (data,) + ) if calibration_data is None: raise self.error( - "Internal error processing accelerometer data %s" % (data,)) + "Internal error processing accelerometer data %s" % (data,) + ) calibration_data.set_numpy(self.numpy) return calibration_data @@ -187,51 +208,59 @@ class ShaperCalibrate: np = self.numpy A, T = np.array(shaper[0]), np.array(shaper[1]) - inv_D = 1. / A.sum() + inv_D = 1.0 / A.sum() - omega = 2. * math.pi * test_freqs + omega = 2.0 * math.pi * test_freqs damping = test_damping_ratio * omega - omega_d = omega * math.sqrt(1. - test_damping_ratio**2) + omega_d = omega * math.sqrt(1.0 - test_damping_ratio**2) W = A * np.exp(np.outer(-damping, (T[-1] - T))) S = W * np.sin(np.outer(omega_d, T)) C = W * np.cos(np.outer(omega_d, T)) - return np.sqrt(S.sum(axis=1)**2 + C.sum(axis=1)**2) * inv_D + return np.sqrt(S.sum(axis=1) ** 2 + C.sum(axis=1) ** 2) * inv_D - def _estimate_remaining_vibrations(self, shaper, test_damping_ratio, - freq_bins, psd): + def _estimate_remaining_vibrations( + self, shaper, test_damping_ratio, freq_bins, psd + ): vals = self._estimate_shaper(shaper, test_damping_ratio, freq_bins) # The input shaper can only reduce the amplitude of vibrations by # SHAPER_VIBRATION_REDUCTION times, so all vibrations below that # threshold can be igonred vibr_threshold = psd.max() / shaper_defs.SHAPER_VIBRATION_REDUCTION - remaining_vibrations = self.numpy.maximum( - vals * psd - vibr_threshold, 0).sum() + remaining_vibrations = self.numpy.maximum(vals * psd - vibr_threshold, 0).sum() all_vibrations = self.numpy.maximum(psd - vibr_threshold, 0).sum() return (remaining_vibrations / all_vibrations, vals) - def _get_shaper_smoothing(self, shaper, accel=5000, scv=5.): - half_accel = accel * .5 + def _get_shaper_smoothing(self, shaper, accel=5000, scv=5.0): + half_accel = accel * 0.5 A, T = shaper - inv_D = 1. / sum(A) + inv_D = 1.0 / sum(A) n = len(T) # Calculate input shaper shift ts = sum([A[i] * T[i] for i in range(n)]) * inv_D # Calculate offset for 90 and 180 degrees turn - offset_90 = offset_180 = 0. + offset_90 = offset_180 = 0.0 for i in range(n): if T[i] >= ts: # Calculate offset for one of the axes - offset_90 += A[i] * (scv + half_accel * (T[i]-ts)) * (T[i]-ts) - offset_180 += A[i] * half_accel * (T[i]-ts)**2 - offset_90 *= inv_D * math.sqrt(2.) + offset_90 += A[i] * (scv + half_accel * (T[i] - ts)) * (T[i] - ts) + offset_180 += A[i] * half_accel * (T[i] - ts) ** 2 + offset_90 *= inv_D * math.sqrt(2.0) offset_180 *= inv_D return max(offset_90, offset_180) - def fit_shaper(self, shaper_cfg, calibration_data, shaper_freqs, - damping_ratio, scv, max_smoothing, test_damping_ratios, - max_freq): + def fit_shaper( + self, + shaper_cfg, + calibration_data, + shaper_freqs, + damping_ratio, + scv, + max_smoothing, + test_damping_ratios, + max_freq, + ): np = self.numpy damping_ratio = damping_ratio or shaper_defs.DEFAULT_DAMPING_RATIO @@ -241,9 +270,8 @@ class ShaperCalibrate: shaper_freqs = (None, None, None) if isinstance(shaper_freqs, tuple): freq_end = shaper_freqs[1] or MAX_SHAPER_FREQ - freq_start = min(shaper_freqs[0] or shaper_cfg.min_freq, - freq_end - 1e-7) - freq_step = shaper_freqs[2] or .2 + freq_start = min(shaper_freqs[0] or shaper_cfg.min_freq, freq_end - 1e-7) + freq_step = shaper_freqs[2] or 0.2 test_freqs = np.arange(freq_start, freq_end, freq_step) else: test_freqs = np.array(shaper_freqs) @@ -257,7 +285,7 @@ class ShaperCalibrate: best_res = None results = [] for test_freq in test_freqs[::-1]: - shaper_vibrations = 0. + shaper_vibrations = 0.0 shaper_vals = np.zeros(shape=freq_bins.shape) shaper = shaper_cfg.init_func(test_freq, damping_ratio) shaper_smoothing = self._get_shaper_smoothing(shaper, scv=scv) @@ -267,7 +295,8 @@ class ShaperCalibrate: # remaining vibrations over possible damping values for dr in test_damping_ratios: vibrations, vals = self._estimate_remaining_vibrations( - shaper, dr, freq_bins, psd) + shaper, dr, freq_bins, psd + ) shaper_vals = np.maximum(shaper_vals, vals) if vibrations > shaper_vibrations: shaper_vibrations = vibrations @@ -275,13 +304,20 @@ class ShaperCalibrate: # The score trying to minimize vibrations, but also accounting # the growth of smoothing. The formula itself does not have any # special meaning, it simply shows good results on real user data - shaper_score = shaper_smoothing * (shaper_vibrations**1.5 + - shaper_vibrations * .2 + .01) + shaper_score = shaper_smoothing * ( + shaper_vibrations**1.5 + shaper_vibrations * 0.2 + 0.01 + ) results.append( - CalibrationResult( - name=shaper_cfg.name, freq=test_freq, vals=shaper_vals, - vibrs=shaper_vibrations, smoothing=shaper_smoothing, - score=shaper_score, max_accel=max_accel)) + CalibrationResult( + name=shaper_cfg.name, + freq=test_freq, + vals=shaper_vals, + vibrs=shaper_vibrations, + smoothing=shaper_smoothing, + score=shaper_score, + max_accel=max_accel, + ) + ) if best_res is None or best_res.vibrs > results[-1].vibrs: # The current frequency is better for the shaper. best_res = results[-1] @@ -294,17 +330,17 @@ class ShaperCalibrate: return selected def _bisect(self, func): - left = right = 1. + left = right = 1.0 if not func(1e-9): - return 0. + return 0.0 while not func(left): right = left - left *= .5 + left *= 0.5 if right == left: while func(right): - right *= 2. + right *= 2.0 while right - left > 1e-8: - middle = (left + right) * .5 + middle = (left + right) * 0.5 if func(middle): left = middle else: @@ -315,63 +351,99 @@ class ShaperCalibrate: # Just some empirically chosen value which produces good projections # for max_accel without much smoothing TARGET_SMOOTHING = 0.12 - max_accel = self._bisect(lambda test_accel: self._get_shaper_smoothing( - shaper, test_accel, scv) <= TARGET_SMOOTHING) + max_accel = self._bisect( + lambda test_accel: self._get_shaper_smoothing(shaper, test_accel, scv) + <= TARGET_SMOOTHING + ) return max_accel - def find_best_shaper(self, calibration_data, shapers=None, - damping_ratio=None, scv=None, shaper_freqs=None, - max_smoothing=None, test_damping_ratios=None, - max_freq=None, logger=None): + def find_best_shaper( + self, + calibration_data, + shapers=None, + damping_ratio=None, + scv=None, + shaper_freqs=None, + max_smoothing=None, + test_damping_ratios=None, + max_freq=None, + logger=None, + ): best_shaper = None all_shapers = [] shapers = shapers or AUTOTUNE_SHAPERS for shaper_cfg in shaper_defs.INPUT_SHAPERS: if shaper_cfg.name not in shapers: continue - shaper = self.background_process_exec(self.fit_shaper, ( - shaper_cfg, calibration_data, shaper_freqs, damping_ratio, - scv, max_smoothing, test_damping_ratios, max_freq)) + shaper = self.background_process_exec( + self.fit_shaper, + ( + shaper_cfg, + calibration_data, + shaper_freqs, + damping_ratio, + scv, + max_smoothing, + test_damping_ratios, + max_freq, + ), + ) if logger is not None: - logger("Fitted shaper '%s' frequency = %.1f Hz " - "(vibrations = %.1f%%, smoothing ~= %.3f)" % ( - shaper.name, shaper.freq, shaper.vibrs * 100., - shaper.smoothing)) - logger("To avoid too much smoothing with '%s', suggested " - "max_accel <= %.0f mm/sec^2" % ( - shaper.name, round(shaper.max_accel / 100.) * 100.)) + logger( + "Fitted shaper '%s' frequency = %.1f Hz " + "(vibrations = %.1f%%, smoothing ~= %.3f)" + % (shaper.name, shaper.freq, shaper.vibrs * 100.0, shaper.smoothing) + ) + logger( + "To avoid too much smoothing with '%s', suggested " + "max_accel <= %.0f mm/sec^2" + % (shaper.name, round(shaper.max_accel / 100.0) * 100.0) + ) all_shapers.append(shaper) - if (best_shaper is None or shaper.score * 1.2 < best_shaper.score or - (shaper.score * 1.05 < best_shaper.score and - shaper.smoothing * 1.1 < best_shaper.smoothing)): + if ( + best_shaper is None + or shaper.score * 1.2 < best_shaper.score + or ( + shaper.score * 1.05 < best_shaper.score + and shaper.smoothing * 1.1 < best_shaper.smoothing + ) + ): # Either the shaper significantly improves the score (by 20%), # or it improves the score and smoothing (by 5% and 10% resp.) best_shaper = shaper return best_shaper, all_shapers def save_params(self, configfile, axis, shaper_name, shaper_freq): - if axis == 'xy': - self.save_params(configfile, 'x', shaper_name, shaper_freq) - self.save_params(configfile, 'y', shaper_name, shaper_freq) + if axis == "xy": + self.save_params(configfile, "x", shaper_name, shaper_freq) + self.save_params(configfile, "y", shaper_name, shaper_freq) else: - configfile.set('input_shaper', 'shaper_type_'+axis, shaper_name) - configfile.set('input_shaper', 'shaper_freq_'+axis, - '%.1f' % (shaper_freq,)) + configfile.set("input_shaper", "shaper_type_" + axis, shaper_name) + configfile.set( + "input_shaper", "shaper_freq_" + axis, "%.1f" % (shaper_freq,) + ) def apply_params(self, input_shaper, axis, shaper_name, shaper_freq): - if axis == 'xy': - self.apply_params(input_shaper, 'x', shaper_name, shaper_freq) - self.apply_params(input_shaper, 'y', shaper_name, shaper_freq) + if axis == "xy": + self.apply_params(input_shaper, "x", shaper_name, shaper_freq) + self.apply_params(input_shaper, "y", shaper_name, shaper_freq) return gcode = self.printer.lookup_object("gcode") axis = axis.upper() - input_shaper.cmd_SET_INPUT_SHAPER(gcode.create_gcode_command( - "SET_INPUT_SHAPER", "SET_INPUT_SHAPER", { + input_shaper.cmd_SET_INPUT_SHAPER( + gcode.create_gcode_command( + "SET_INPUT_SHAPER", + "SET_INPUT_SHAPER", + { "SHAPER_TYPE_" + axis: shaper_name, - "SHAPER_FREQ_" + axis: shaper_freq})) + "SHAPER_FREQ_" + axis: shaper_freq, + }, + ) + ) - def save_calibration_data(self, output, calibration_data, shapers=None, - max_freq=None): + def save_calibration_data( + self, output, calibration_data, shapers=None, max_freq=None + ): try: max_freq = max_freq or MAX_FREQ with open(output, "w") as csvfile: @@ -384,12 +456,16 @@ class ShaperCalibrate: for i in range(num_freqs): if calibration_data.freq_bins[i] >= max_freq: break - csvfile.write("%.1f,%.3e,%.3e,%.3e,%.3e" % ( - calibration_data.freq_bins[i], - calibration_data.psd_x[i], - calibration_data.psd_y[i], - calibration_data.psd_z[i], - calibration_data.psd_sum[i])) + csvfile.write( + "%.1f,%.3e,%.3e,%.3e,%.3e" + % ( + calibration_data.freq_bins[i], + calibration_data.psd_x[i], + calibration_data.psd_y[i], + calibration_data.psd_z[i], + calibration_data.psd_sum[i], + ) + ) if shapers: for shaper in shapers: csvfile.write(",%.3f" % (shaper.vals[i],)) |