diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index ecac5a7aafcc..a73a0c37f087 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,14 @@ All dates in this document are approximate. ## Changes +20241203: The resonance test has been changed to include slow sweeping +moves. This change requires that testing point(s) have some clearance +in X/Y plane (+/- 30 mm from the test point should suffice when using +the default settings). The new test should generally produce more +accurate and reliable test results. However, if required, the previous +test behavior can be restored by adding options `sweeping_period: 0` and +`accel_per_hz: 75` to the `[resonance_tester]` config section. + 20241201: In some cases Klipper may have ignored leading characters or spaces in a traditional G-Code command. For example, "99M123" may have been interpreted as "M123" and "M 321" may have been interpreted as diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 6a5a7582397d..5d85e4835c07 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -1790,11 +1790,14 @@ section of the measuring resonances guide for more information on # auto-calibration (with 'SHAPER_CALIBRATE' command). By default no # maximum smoothing is specified. Refer to Measuring_Resonances guide # for more details on using this feature. +#move_speed: 50 +# The speed (in mm/s) to move the toolhead to and between test points +# during the calibration. The default is 50. #min_freq: 5 # Minimum frequency to test for resonances. The default is 5 Hz. #max_freq: 133.33 # Maximum frequency to test for resonances. The default is 133.33 Hz. -#accel_per_hz: 75 +#accel_per_hz: 60 # This parameter is used to determine which acceleration to use to # test a specific frequency: accel = accel_per_hz * freq. Higher the # value, the higher is the energy of the oscillations. Can be set to @@ -1808,6 +1811,13 @@ section of the measuring resonances guide for more information on # hz_per_sec. Small values make the test slow, and the large values # will decrease the precision of the test. The default value is 1.0 # (Hz/sec == sec^-2). +#sweeping_accel: 400 +# An acceleration of slow sweeping moves. The default is 400 mm/sec^2. +#sweeping_period: 1.2 +# A period of slow sweeping moves. Setting this parameter to 0 +# disables slow sweeping moves. Avoid setting it to a too small +# non-zero value in order to not poison the measurements. +# The default is 1.2 sec which is a good all-round choice. ``` ## Config file helpers diff --git a/docs/Measuring_Resonances.md b/docs/Measuring_Resonances.md index 6c58409bf139..d6aaf5671a31 100644 --- a/docs/Measuring_Resonances.md +++ b/docs/Measuring_Resonances.md @@ -694,6 +694,24 @@ If you are doing a shaper re-calibration and the reported smoothing for the suggested shaper configuration is almost the same as what you got during the previous calibration, this step can be skipped. +### Unreliable measurements of resonance frequencies + +Sometimes the resonance measurements can produce bogus results, leading to +the incorrect suggestions for the input shapers. This can be caused by a +variety of reasons, including running fans on the toolhead, incorrect +position or non-rigid mounting of the accelerometer, or mechanical problems +such as loose belts or binding or bumpy axis. Keep in mind that all fans +should be disabled for resonance testing, especially the noisy ones, and +that the accelerometer should be rigidly mounted on the corresponding +moving part (e.g. on the bed itself for the bed slinger, or on the extruder +of the printer itself and not the carriage, and some people get better +results by mounting the accelerometer on the nozzle itself). As for +mechanical problems, the user should inspect if there is any fault that +can be fixed with a moving axis (e.g. linear guide rails cleaned up and +lubricated and V-slot wheels tension adjusted correctly). If none of that +helps, a user may try the other shapers from the produced list besides the +one recommended by default. + ### Testing custom axes `TEST_RESONANCES` command supports custom axes. While this is not really diff --git a/klippy/extras/resonance_tester.py b/klippy/extras/resonance_tester.py index e9d4e9d92c53..76e56f536b9a 100644 --- a/klippy/extras/resonance_tester.py +++ b/klippy/extras/resonance_tester.py @@ -45,42 +45,96 @@ def _parse_axis(gcmd, raw_axis): "Unable to parse axis direction '%s'" % (raw_axis,)) return TestAxis(vib_dir=(dir_x, dir_y)) -class VibrationPulseTest: +class VibrationPulseTestGenerator: def __init__(self, config): - self.printer = config.get_printer() - self.gcode = self.printer.lookup_object('gcode') self.min_freq = config.getfloat('min_freq', 5., minval=1.) - # Defaults are such that max_freq * accel_per_hz == 10000 (max_accel) - self.max_freq = config.getfloat('max_freq', 10000. / 75., + self.max_freq = config.getfloat('max_freq', 135., minval=self.min_freq, maxval=300.) - self.accel_per_hz = config.getfloat('accel_per_hz', 75., above=0.) + self.accel_per_hz = config.getfloat('accel_per_hz', 60., above=0.) self.hz_per_sec = config.getfloat('hz_per_sec', 1., minval=0.1, maxval=2.) - - self.probe_points = config.getlists('probe_points', seps=(',', '\n'), - parser=float, count=3) - def get_start_test_points(self): - return self.probe_points def prepare_test(self, gcmd): self.freq_start = gcmd.get_float("FREQ_START", self.min_freq, minval=1.) self.freq_end = gcmd.get_float("FREQ_END", self.max_freq, minval=self.freq_start, maxval=300.) - self.accel_per_hz = gcmd.get_float("ACCEL_PER_HZ", - self.accel_per_hz, above=0.) - self.hz_per_sec = gcmd.get_float("HZ_PER_SEC", self.hz_per_sec, - above=0., maxval=2.) - def run_test(self, axis, gcmd): + self.test_accel_per_hz = gcmd.get_float("ACCEL_PER_HZ", + self.accel_per_hz, above=0.) + self.test_hz_per_sec = gcmd.get_float("HZ_PER_SEC", self.hz_per_sec, + above=0., maxval=2.) + def gen_test(self): + freq = self.freq_start + res = [] + sign = 1. + time = 0. + while freq <= self.freq_end + 0.000001: + t_seg = .25 / freq + accel = self.test_accel_per_hz * freq + time += t_seg + res.append((time, sign * accel, freq)) + time += t_seg + res.append((time, -sign * accel, freq)) + freq += 2. * t_seg * self.test_hz_per_sec + sign = -sign + return res + def get_max_freq(self): + return self.freq_end + +class SweepingVibrationsTestGenerator: + def __init__(self, config): + self.vibration_generator = VibrationPulseTestGenerator(config) + self.sweeping_accel = config.getfloat('sweeping_accel', 400., above=0.) + self.sweeping_period = config.getfloat('sweeping_period', 1.2, + minval=0.) + def prepare_test(self, gcmd): + self.vibration_generator.prepare_test(gcmd) + self.test_sweeping_accel = gcmd.get_float( + "SWEEPING_ACCEL", self.sweeping_accel, above=0.) + self.test_sweeping_period = gcmd.get_float( + "SWEEPING_PERIOD", self.sweeping_period, minval=0.) + def gen_test(self): + test_seq = self.vibration_generator.gen_test() + accel_fraction = math.sqrt(2.0) * 0.125 + if self.test_sweeping_period: + t_rem = self.test_sweeping_period * accel_fraction + sweeping_accel = self.test_sweeping_accel + else: + t_rem = float('inf') + sweeping_accel = 0. + res = [] + last_t = 0. + sig = 1. + accel_fraction += 0.25 + for next_t, accel, freq in test_seq: + t_seg = next_t - last_t + while t_rem <= t_seg: + last_t += t_rem + res.append((last_t, accel + sweeping_accel * sig, freq)) + t_seg -= t_rem + t_rem = self.test_sweeping_period * accel_fraction + accel_fraction = 0.5 + sig = -sig + t_rem -= t_seg + res.append((next_t, accel + sweeping_accel * sig, freq)) + last_t = next_t + return res + def get_max_freq(self): + return self.vibration_generator.get_max_freq() + +class ResonanceTestExecutor: + def __init__(self, config): + self.printer = config.get_printer() + self.gcode = self.printer.lookup_object('gcode') + def run_test(self, test_seq, axis, gcmd): + reactor = self.printer.get_reactor() toolhead = self.printer.lookup_object('toolhead') X, Y, Z, E = toolhead.get_position() - sign = 1. - freq = self.freq_start # Override maximum acceleration and acceleration to # deceleration based on the maximum test frequency - systime = self.printer.get_reactor().monotonic() + systime = reactor.monotonic() toolhead_info = toolhead.get_status(systime) old_max_accel = toolhead_info['max_accel'] old_minimum_cruise_ratio = toolhead_info['minimum_cruise_ratio'] - max_accel = self.freq_end * self.accel_per_hz + max_accel = max([abs(a) for _, a, _ in test_seq]) self.gcode.run_script_from_command( "SET_VELOCITY_LIMIT ACCEL=%.3f MINIMUM_CRUISE_RATIO=0" % (max_accel,)) @@ -90,24 +144,46 @@ def run_test(self, axis, gcmd): gcmd.respond_info("Disabled [input_shaper] for resonance testing") else: input_shaper = None - gcmd.respond_info("Testing frequency %.0f Hz" % (freq,)) - while freq <= self.freq_end + 0.000001: - t_seg = .25 / freq - accel = self.accel_per_hz * freq - max_v = accel * t_seg + last_v = last_t = last_accel = last_freq = 0. + for next_t, accel, freq in test_seq: + t_seg = next_t - last_t toolhead.cmd_M204(self.gcode.create_gcode_command( - "M204", "M204", {"S": accel})) - L = .5 * accel * t_seg**2 - dX, dY = axis.get_point(L) - nX = X + sign * dX - nY = Y + sign * dY - toolhead.move([nX, nY, Z, E], max_v) - toolhead.move([X, Y, Z, E], max_v) - sign = -sign - old_freq = freq - freq += 2. * t_seg * self.hz_per_sec - if math.floor(freq) > math.floor(old_freq): + "M204", "M204", {"S": abs(accel)})) + v = last_v + accel * t_seg + abs_v = abs(v) + if abs_v < 0.000001: + v = abs_v = 0. + abs_last_v = abs(last_v) + v2 = v * v + last_v2 = last_v * last_v + half_inv_accel = .5 / accel + d = (v2 - last_v2) * half_inv_accel + dX, dY = axis.get_point(d) + nX = X + dX + nY = Y + dY + toolhead.limit_next_junction_speed(abs_last_v) + if v * last_v < 0: + # The move first goes to a complete stop, then changes direction + d_decel = -last_v2 * half_inv_accel + decel_X, decel_Y = axis.get_point(d_decel) + toolhead.move([X + decel_X, Y + decel_Y, Z, E], abs_last_v) + toolhead.move([nX, nY, Z, E], abs_v) + else: + toolhead.move([nX, nY, Z, E], max(abs_v, abs_last_v)) + if math.floor(freq) > math.floor(last_freq): gcmd.respond_info("Testing frequency %.0f Hz" % (freq,)) + reactor.pause(reactor.monotonic() + 0.01) + X, Y = nX, nY + last_t = next_t + last_v = v + last_accel = accel + last_freq = freq + if last_v: + d_decel = -.5 * last_v2 / old_max_accel + decel_X, decel_Y = axis.get_point(d_decel) + toolhead.cmd_M204(self.gcode.create_gcode_command( + "M204", "M204", {"S": old_max_accel})) + toolhead.move([X + decel_X, Y + decel_Y, Z, E], abs(last_v)) # Restore the original acceleration values self.gcode.run_script_from_command( "SET_VELOCITY_LIMIT ACCEL=%.3f MINIMUM_CRUISE_RATIO=%.3f" @@ -116,14 +192,13 @@ def run_test(self, axis, gcmd): if input_shaper is not None: input_shaper.enable_shaping() gcmd.respond_info("Re-enabled [input_shaper]") - def get_max_freq(self): - return self.freq_end class ResonanceTester: def __init__(self, config): self.printer = config.get_printer() self.move_speed = config.getfloat('move_speed', 50., above=0.) - self.test = VibrationPulseTest(config) + self.generator = SweepingVibrationsTestGenerator(config) + self.executor = ResonanceTestExecutor(config) if not config.get('accel_chip_x', None): self.accel_chip_names = [('xy', config.get('accel_chip').strip())] else: @@ -133,6 +208,8 @@ def __init__(self, config): if self.accel_chip_names[0][1] == self.accel_chip_names[1][1]: self.accel_chip_names = [('xy', self.accel_chip_names[0][1])] self.max_smoothing = config.getfloat('max_smoothing', None, minval=0.05) + self.probe_points = config.getlists('probe_points', seps=(',', '\n'), + parser=float, count=3) self.gcode = self.printer.lookup_object('gcode') self.gcode.register_command("MEASURE_AXES_NOISE", @@ -156,12 +233,9 @@ def _run_test(self, gcmd, axes, helper, raw_name_suffix=None, toolhead = self.printer.lookup_object('toolhead') calibration_data = {axis: None for axis in axes} - self.test.prepare_test(gcmd) + self.generator.prepare_test(gcmd) - if test_point is not None: - test_points = [test_point] - else: - test_points = self.test.get_start_test_points() + test_points = [test_point] if test_point else self.probe_points for point in test_points: toolhead.manual_move(point, self.move_speed) @@ -186,7 +260,8 @@ def _run_test(self, gcmd, axes, helper, raw_name_suffix=None, raw_values.append((axis, aclient, chip.name)) # Generate moves - self.test.run_test(axis, gcmd) + test_seq = self.generator.gen_test() + self.executor.run_test(test_seq, axis, gcmd) for chip_axis, aclient, chip_name in raw_values: aclient.finish_measurements() if raw_name_suffix is not None: @@ -218,7 +293,7 @@ def _parse_chips(self, accel_chips): parsed_chips.append(chip) return parsed_chips def _get_max_calibration_freq(self): - return 1.5 * self.test.get_max_freq() + return 1.5 * self.generator.get_max_freq() cmd_TEST_RESONANCES_help = ("Runs the resonance test for a specifed axis") def cmd_TEST_RESONANCES(self, gcmd): # Parse parameters diff --git a/klippy/extras/shaper_calibrate.py b/klippy/extras/shaper_calibrate.py index 6891fefb3bf9..f497171f67c0 100644 --- a/klippy/extras/shaper_calibrate.py +++ b/klippy/extras/shaper_calibrate.py @@ -48,7 +48,9 @@ def normalize_to_frequencies(self): # Avoid division by zero errors psd /= self.freq_bins + .1 # Remove low-frequency noise - psd[self.freq_bins < MIN_FREQ] = 0. + low_freqs = self.freq_bins < 2. * 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'): return self._psd_map[axis] diff --git a/klippy/toolhead.py b/klippy/toolhead.py index ed26092f651d..256915daabf9 100644 --- a/klippy/toolhead.py +++ b/klippy/toolhead.py @@ -47,6 +47,7 @@ def __init__(self, toolhead, start_pos, end_pos, speed): self.delta_v2 = 2.0 * move_d * self.accel self.max_smoothed_v2 = 0. self.smooth_delta_v2 = 2.0 * move_d * toolhead.max_accel_to_decel + self.next_junction_v2 = 999999999.9 def limit_speed(self, speed, accel): speed2 = speed**2 if speed2 < self.max_cruise_v2: @@ -55,6 +56,8 @@ def limit_speed(self, speed, accel): self.accel = min(self.accel, accel) self.delta_v2 = 2.0 * self.move_d * self.accel self.smooth_delta_v2 = min(self.smooth_delta_v2, self.delta_v2) + def limit_next_junction_speed(self, speed): + self.next_junction_v2 = min(self.next_junction_v2, speed**2) def move_error(self, msg="Move out of range"): ep = self.end_pos m = "%s: %.3f %.3f %.3f [%.3f]" % (msg, ep[0], ep[1], ep[2], ep[3]) @@ -64,9 +67,9 @@ def calc_junction(self, prev_move): return # Allow extruder to calculate its maximum junction extruder_v2 = self.toolhead.extruder.calc_junction(prev_move, self) - max_start_v2 = min( - extruder_v2, self.max_cruise_v2, prev_move.max_cruise_v2, - prev_move.max_start_v2 + prev_move.delta_v2) + max_start_v2 = min(extruder_v2, self.max_cruise_v2, + prev_move.max_cruise_v2, prev_move.next_junction_v2, + prev_move.max_start_v2 + prev_move.delta_v2) # Find max velocity using "approximated centripetal velocity" axes_r = self.axes_r prev_axes_r = prev_move.axes_r @@ -462,6 +465,10 @@ def set_position(self, newpos, homing_axes=()): self.commanded_pos[:] = newpos self.kin.set_position(newpos, homing_axes) self.printer.send_event("toolhead:set_position") + def limit_next_junction_speed(self, speed): + last_move = self.lookahead.get_last() + if last_move is not None: + last_move.limit_next_junction_speed(speed) def move(self, newpos, speed): move = Move(self, self.commanded_pos, newpos, speed) if not move.move_d: