From 7dc91de3c3baf2539aa7c0a6a367d97cb0b61b7a Mon Sep 17 00:00:00 2001 From: Nick Weedon Date: Sat, 4 Jan 2025 13:28:38 -0500 Subject: [PATCH] probe: Added truncated mean averaging capability Added a new 'samples_trunc_count' configuration attribute to the '[probe]' section that will cause the 'samples_trunc_count' number of highest and lowest samples to be removed and then the average of the remaining samples is returned. Signed-off-by: Nick Weedon --- docs/Config_Reference.md | 14 ++++++++++++ docs/G-Codes.md | 2 +- klippy/extras/probe.py | 48 ++++++++++++++++++++++++++++++++++++---- 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index f797d2b06905..3e971942cf0b 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -1929,6 +1929,17 @@ z_offset: # not obtained in the given number of retries then an error is # reported. The default is zero which causes an error to be reported # on the first sample that exceeds samples_tolerance. +#samples_trunc_count: 0 +# Setting this to a non-zero value effectively enables a 'truncated mean' algorithm +# which removes 'samples_trunc_count' number of highest and lowest probe values +# before taking an average. The number which are removed is specified by this configruation value. +# This averaging option gives the best of both worlds between the median and the mean (average) +# since it is resistant to outliers but still averages the remaining results. +# This option is useful when you are taking 5 or more samples (using a +# samples_trunc_count of 2 for example). +# Note that if this option is configured such that there is less than 3 samples remaining +# after removing the truncated samples (i.e. samples_trunc_count) then this is considered a +# configuration error since this is the same as simply taking a median. #activate_gcode: # A list of G-Code commands to execute prior to each probe attempt. # See docs/Command_Templates.md for G-Code format. This may be @@ -1997,6 +2008,7 @@ control_pin: #samples_result: #samples_tolerance: #samples_tolerance_retries: +#samples_trunc_count: # See the "probe" section for information on these parameters. ``` @@ -2048,6 +2060,7 @@ z_offset: #samples_result: #samples_tolerance: #samples_tolerance_retries: +#samples_trunc_count: #activate_gcode: #deactivate_gcode: #deactivate_on_each_sample: @@ -2088,6 +2101,7 @@ sensor_type: ldc1612 #samples_result: #samples_tolerance: #samples_tolerance_retries: +#samples_trunc_count: # See the "probe" section for information on these parameters. ``` diff --git a/docs/G-Codes.md b/docs/G-Codes.md index d44017deac9c..d24681b44711 100644 --- a/docs/G-Codes.md +++ b/docs/G-Codes.md @@ -999,7 +999,7 @@ see the [probe calibrate guide](Probe_Calibrate.md)). #### PROBE `PROBE [PROBE_SPEED=] [LIFT_SPEED=] [SAMPLES=] [SAMPLE_RETRACT_DIST=] [SAMPLES_TOLERANCE=] -[SAMPLES_TOLERANCE_RETRIES=] [SAMPLES_RESULT=median|average]`: +[SAMPLES_TOLERANCE_RETRIES=] [SAMPLES_TRUNC_COUNT=] [SAMPLES_RESULT=median|average]`: Move the nozzle downwards until the probe triggers. If any of the optional parameters are provided they override their equivalent setting in the [probe config section](Config_Reference.md#probe). diff --git a/klippy/extras/probe.py b/klippy/extras/probe.py index c467e181e4d5..38f47db5e964 100644 --- a/klippy/extras/probe.py +++ b/klippy/extras/probe.py @@ -4,6 +4,7 @@ # # This file may be distributed under the terms of the GNU GPLv3 license. import logging +import math import pins from . import manual_probe @@ -14,12 +15,23 @@ """ # Calculate the average Z from a set of positions -def calc_probe_z_average(positions, method='average'): +def calc_probe_z_average(positions, method='average', trunc_count=0): + if method != 'median': # Use mean average - count = float(len(positions)) - return [sum([pos[i] for pos in positions]) / count + count = len(positions) + + # Perform truncated mean if required + if trunc_count > 0: + z_sorted = sorted(positions, key=(lambda p: p[2])) + start_index = trunc_count // 2 + end_index = count - math.ceil(trunc_count / 2.0) + return calc_probe_z_average( + z_sorted[start_index:end_index], 'average', 0) + + return [sum([pos[i] for pos in positions]) / float(count) for i in range(3)] + # Use median z_sorted = sorted(positions, key=(lambda p: p[2])) middle = len(positions) // 2 @@ -259,6 +271,19 @@ def __init__(self, config, mcu_probe): minval=0.) self.samples_retries = config.getint('samples_tolerance_retries', 0, minval=0) + self.samples_trunc_count = config.getint('samples_trunc_count', 0, + minval=0) + + if self.samples_trunc_count > 0: + if self.samples_result != 'average': + raise config.error("samples_trunc_count is only " + "valid with samples_result=average") + + if not self.sample_count - self.samples_trunc_count > 2: + raise config.error( + "samples_trunc_count must be either zero or " + "such that: sample_count - samples_trunc_count > 2") + # Session state self.multi_probe_pending = False self.results = [] @@ -299,13 +324,27 @@ def get_probe_params(self, gcmd=None): self.samples_tolerance, minval=0.) samples_retries = gcmd.get_int("SAMPLES_TOLERANCE_RETRIES", self.samples_retries, minval=0) + samples_trunc_count = gcmd.get_int("SAMPLES_TRUNC_COUNT", + self.samples_trunc_count, minval=0) samples_result = gcmd.get("SAMPLES_RESULT", self.samples_result) + + if samples_trunc_count > 0: + if samples_result != 'average': + raise gcmd.error("samples_trunc_count is only " + "valid with samples_result=average") + + if not samples - samples_trunc_count > 2: + raise gcmd.error( + "samples_trunc_count must be either zero or " + "such that: sample_count - samples_trunc_count > 2") + return {'probe_speed': probe_speed, 'lift_speed': lift_speed, 'samples': samples, 'sample_retract_dist': sample_retract_dist, 'samples_tolerance': samples_tolerance, 'samples_tolerance_retries': samples_retries, + 'samples_trunc_count': samples_trunc_count, 'samples_result': samples_result} def _probe(self, speed): toolhead = self.printer.lookup_object('toolhead') @@ -355,7 +394,8 @@ def run_probe(self, gcmd): probexy + [pos[2] + params['sample_retract_dist']], params['lift_speed']) # Calculate result - epos = calc_probe_z_average(positions, params['samples_result']) + epos = calc_probe_z_average(positions, params['samples_result'], + params['samples_trunc_count']) self.results.append(epos) def pull_probed_results(self): res = self.results