Skip to content

Commit

Permalink
scripts: Added shaper tuning parameters to calibrate_shaper script
Browse files Browse the repository at this point in the history
The added parameters include square_corner_velocity, shaper frequencies
to optimize, input shapers to test, input shaper damping ratio and
damping ratios to test. All these options can be useful for fine-tuning
the input shapers when the default suggestions generated by the tuning
script are not optimal.

Signed-off-by: Dmitry Butyugin <dmbutyugin@google.com>
  • Loading branch information
dmbutyugin committed Feb 8, 2024
1 parent 9f41f53 commit a5cfc32
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 25 deletions.
12 changes: 10 additions & 2 deletions klippy/extras/resonance_tester.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# A utility class to test resonances of the printer
#
# Copyright (C) 2020 Dmitry Butyugin <dmbutyugin@google.com>
# Copyright (C) 2020-2024 Dmitry Butyugin <dmbutyugin@google.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import logging, math, os, time
Expand Down Expand Up @@ -114,6 +114,8 @@ 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):
Expand Down Expand Up @@ -302,8 +304,14 @@ def cmd_SHAPER_CALIBRATE(self, gcmd):
"Calculating the best input shaper parameters for %s axis"
% (axis_name,))
calibration_data[axis].normalize_to_frequencies()
systime = self.printer.get_reactor().monotonic()
toolhead = self.printer.lookup_object('toolhead')
toolhead_info = toolhead.get_status(systime)
scv = toolhead_info['square_corner_velocity']
best_shaper, all_shapers = helper.find_best_shaper(
calibration_data[axis], max_smoothing, gcmd.respond_info)
calibration_data[axis], max_smoothing=max_smoothing,
scv=scv, max_freq=1.5*self.test.get_max_freq(),
logging=gcmd.respond_info)
gcmd.respond_info(
"Recommended shaper_type_%s = %s, shaper_freq_%s = %.1f Hz"
% (axis_name, best_shaper.name,
Expand Down
51 changes: 36 additions & 15 deletions klippy/extras/shaper_calibrate.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Automatic calibration of input shapers
#
# Copyright (C) 2020 Dmitry Butyugin <dmbutyugin@google.com>
# Copyright (C) 2020-2024 Dmitry Butyugin <dmbutyugin@google.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import collections, importlib, logging, math, multiprocessing, traceback
Expand Down Expand Up @@ -227,34 +227,48 @@ def _get_shaper_smoothing(self, shaper, accel=5000, scv=5.):
offset_180 *= inv_D
return max(offset_90, offset_180)

def fit_shaper(self, shaper_cfg, calibration_data, max_smoothing):
def fit_shaper(self, shaper_cfg, calibration_data, shaper_freqs,
damping_ratio, scv, max_smoothing, test_damping_ratios,
max_freq):
np = self.numpy

test_freqs = np.arange(shaper_cfg.min_freq, MAX_SHAPER_FREQ, .2)
damping_ratio = damping_ratio or shaper_defs.DEFAULT_DAMPING_RATIO
test_damping_ratios = test_damping_ratios or TEST_DAMPING_RATIOS

if not shaper_freqs:
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
test_freqs = np.arange(freq_start, freq_end, freq_step)
else:
test_freqs = np.array(shaper_freqs)

max_freq = max(max_freq or MAX_FREQ, test_freqs.max())

freq_bins = calibration_data.freq_bins
psd = calibration_data.psd_sum[freq_bins <= MAX_FREQ]
freq_bins = freq_bins[freq_bins <= MAX_FREQ]
psd = calibration_data.psd_sum[freq_bins <= max_freq]
freq_bins = freq_bins[freq_bins <= max_freq]

best_res = None
results = []
for test_freq in test_freqs[::-1]:
shaper_vibrations = 0.
shaper_vals = np.zeros(shape=freq_bins.shape)
shaper = shaper_cfg.init_func(
test_freq, shaper_defs.DEFAULT_DAMPING_RATIO)
shaper_smoothing = self._get_shaper_smoothing(shaper)
shaper = shaper_cfg.init_func(test_freq, damping_ratio)
shaper_smoothing = self._get_shaper_smoothing(shaper, scv=scv)
if max_smoothing and shaper_smoothing > max_smoothing and best_res:
return best_res
# Exact damping ratio of the printer is unknown, pessimizing
# remaining vibrations over possible damping values
for dr in TEST_DAMPING_RATIOS:
for dr in test_damping_ratios:
vibrations, vals = self._estimate_remaining_vibrations(
shaper, dr, freq_bins, psd)
shaper_vals = np.maximum(shaper_vals, vals)
if vibrations > shaper_vibrations:
shaper_vibrations = vibrations
max_accel = self.find_shaper_max_accel(shaper)
max_accel = self.find_shaper_max_accel(shaper, scv)
# 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
Expand All @@ -278,6 +292,8 @@ def fit_shaper(self, shaper_cfg, calibration_data, max_smoothing):

def _bisect(self, func):
left = right = 1.
if not func(1e-9):
return 0.
while not func(left):
right = left
left *= .5
Expand All @@ -292,22 +308,27 @@ def _bisect(self, func):
right = middle
return left

def find_shaper_max_accel(self, shaper):
def find_shaper_max_accel(self, shaper, scv):
# 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) <= TARGET_SMOOTHING)
shaper, test_accel, scv) <= TARGET_SMOOTHING)
return max_accel

def find_best_shaper(self, calibration_data, max_smoothing, 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 AUTOTUNE_SHAPERS:
if shaper_cfg.name not in shapers:
continue
shaper = self.background_process_exec(self.fit_shaper, (
shaper_cfg, calibration_data, max_smoothing))
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)" % (
Expand Down
93 changes: 85 additions & 8 deletions scripts/calibrate_shaper.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# Shaper auto-calibration script
#
# Copyright (C) 2020 Dmitry Butyugin <dmbutyugin@google.com>
# Copyright (C) 2020-2024 Dmitry Butyugin <dmbutyugin@google.com>
# Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
Expand Down Expand Up @@ -40,7 +40,9 @@ def parse_log(logname):
######################################################################

# Find the best shaper parameters
def calibrate_shaper(datas, csv_output, max_smoothing):
def calibrate_shaper(datas, csv_output, *, shapers, damping_ratio, scv,
shaper_freqs, max_smoothing, test_damping_ratios,
max_freq):
helper = shaper_calibrate.ShaperCalibrate(printer=None)
if isinstance(datas[0], shaper_calibrate.CalibrationData):
calibration_data = datas[0]
Expand All @@ -52,8 +54,17 @@ def calibrate_shaper(datas, csv_output, max_smoothing):
for data in datas[1:]:
calibration_data.add_data(helper.process_accelerometer_data(data))
calibration_data.normalize_to_frequencies()


shaper, all_shapers = helper.find_best_shaper(
calibration_data, max_smoothing, print)
calibration_data, shapers=shapers, damping_ratio=damping_ratio,
scv=scv, shaper_freqs=shaper_freqs, max_smoothing=max_smoothing,
test_damping_ratios=test_damping_ratios, max_freq=max_freq,
logger=print)
if not shaper:
print("No recommended shaper, possibly invalid value for --shapers=%s" %
(','.join(shapers)))
return None, None, None
print("Recommended shaper is %s @ %.1f Hz" % (shaper.name, shaper.freq))
if csv_output is not None:
helper.save_calibration_data(
Expand Down Expand Up @@ -140,28 +151,94 @@ def main():
opts.add_option("-c", "--csv", type="string", dest="csv",
default=None, help="filename of output csv file")
opts.add_option("-f", "--max_freq", type="float", default=200.,
help="maximum frequency to graph")
opts.add_option("-s", "--max_smoothing", type="float", default=None,
help="maximum shaper smoothing to allow")
help="maximum frequency to plot")
opts.add_option("-s", "--max_smoothing", type="float", dest="max_smoothing",
default=None, help="maximum shaper smoothing to allow")
opts.add_option("--scv", "--square_corner_velocity", type="float",
dest="scv", default=5., help="square corner velocity")
opts.add_option("--shaper_freq", type="string", dest="shaper_freq",
default=None, help="shaper frequency(-ies) to test, " +
"either a comma-separated list of floats, or a range in " +
"the format [start]:end[:step]")
opts.add_option("--shapers", type="string", dest="shapers", default=None,
help="a comma-separated list of shapers to test")
opts.add_option("--damping_ratio", type="float", dest="damping_ratio",
default=None, help="shaper damping_ratio parameter")
opts.add_option("--test_damping_ratios", type="string",
dest="test_damping_ratios", default=None,
help="a comma-separated liat of damping ratios to test " +
"input shaper for")
options, args = opts.parse_args()
if len(args) < 1:
opts.error("Incorrect number of arguments")
if options.max_smoothing is not None and options.max_smoothing < 0.05:
opts.error("Too small max_smoothing specified (must be at least 0.05)")

max_freq = options.max_freq
if options.shaper_freq is None:
shaper_freqs = []
elif options.shaper_freq.find(':') >= 0:
freq_start = None
freq_end = None
freq_step = None
try:
freqs_parsed = options.shaper_freq.partition(':')
if freqs_parsed[0]:
freq_start = float(freqs_parsed[0])
freqs_parsed = freqs_parsed[-1].partition(':')
freq_end = float(freqs_parsed[0])
if freq_start and freq_start > freq_end:
opts.error("Invalid --shaper_freq param: start range larger " +
"than its end")
if freqs_parsed[-1].find(':') >= 0:
opts.error("Invalid --shaper_freq param format")
if freqs_parsed[-1]:
freq_step = float(freqs_parsed[-1])
except ValueError:
opts.error("--shaper_freq param does not specify correct range " +
"in the format [start]:end[:step]")
shaper_freqs = (freq_start, freq_end, freq_step)
max_freq = max(max_freq, freq_end * 4./3.)
else:
try:
shaper_freqs = [float(s) for s in options.shaper_freq.split(',')]
except ValueError:
opts.error("invalid floating point value in --shaper_freq param")
max_freq = max(max_freq, max(shaper_freqs) * 4./3.)
if options.test_damping_ratios:
try:
test_damping_ratios = [float(s) for s in
options.test_damping_ratios.split(',')]
except ValueError:
opts.error("invalid floating point value in " +
"--test_damping_ratios param")
else:
test_damping_ratios = None
if options.shapers is None:
shapers = None
else:
shapers = options.shapers.lower().split(',')

# Parse data
datas = [parse_log(fn) for fn in args]

# Calibrate shaper and generate outputs
selected_shaper, shapers, calibration_data = calibrate_shaper(
datas, options.csv, options.max_smoothing)
datas, options.csv, shapers=shapers,
damping_ratio=options.damping_ratio,
scv=options.scv, shaper_freqs=shaper_freqs,
max_smoothing=options.max_smoothing,
test_damping_ratios=test_damping_ratios,
max_freq=max_freq)
if selected_shaper is None:
return

if not options.csv or options.output:
# Draw graph
setup_matplotlib(options.output is not None)

fig = plot_freq_response(args, calibration_data, shapers,
selected_shaper, options.max_freq)
selected_shaper, max_freq)

# Show graph
if options.output is None:
Expand Down

0 comments on commit a5cfc32

Please sign in to comment.