input_shaper: Define input shapers in a single place in Python code

Signed-off-by: Dmitry Butyugin <dmbutyugin@google.com>
This commit is contained in:
Dmitry Butyugin
2021-10-22 20:46:20 +02:00
committed by KevinOConnor
parent 6c395fd016
commit d5a7a7f00f
7 changed files with 209 additions and 385 deletions

View File

@@ -5,31 +5,30 @@
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import chelper
from . import shaper_defs
class InputShaper:
def __init__(self, config):
self.printer = config.get_printer()
self.printer.register_event_handler("klippy:connect", self.connect)
self.toolhead = None
self.shapers = {s.name : s.init_func for s in shaper_defs.INPUT_SHAPERS}
self.damping_ratio_x = config.getfloat(
'damping_ratio_x', 0.1, minval=0., maxval=1.)
'damping_ratio_x', shaper_defs.DEFAULT_DAMPING_RATIO,
minval=0., maxval=1.)
self.damping_ratio_y = config.getfloat(
'damping_ratio_y', 0.1, minval=0., maxval=1.)
'damping_ratio_y', shaper_defs.DEFAULT_DAMPING_RATIO,
minval=0., maxval=1.)
self.shaper_freq_x = config.getfloat('shaper_freq_x', 0., minval=0.)
self.shaper_freq_y = config.getfloat('shaper_freq_y', 0., minval=0.)
ffi_main, ffi_lib = chelper.get_ffi()
self.shapers = {None: None
, 'zv': ffi_lib.INPUT_SHAPER_ZV
, 'zvd': ffi_lib.INPUT_SHAPER_ZVD
, 'mzv': ffi_lib.INPUT_SHAPER_MZV
, 'ei': ffi_lib.INPUT_SHAPER_EI
, '2hump_ei': ffi_lib.INPUT_SHAPER_2HUMP_EI
, '3hump_ei': ffi_lib.INPUT_SHAPER_3HUMP_EI}
shaper_type = config.get('shaper_type', 'mzv')
self.shaper_type_x = config.getchoice(
'shaper_type_x', self.shapers, shaper_type)
self.shaper_type_y = config.getchoice(
'shaper_type_y', self.shapers, shaper_type)
self.shaper_type_x = config.get('shaper_type_x', 'mzv').lower()
if self.shaper_type_x not in self.shapers:
raise config.error(
'Unsupported shaper type: %s' % (self.shaper_type_x,))
self.shaper_type_y = config.get('shaper_type_y', 'mzv').lower()
if self.shaper_type_y not in self.shapers:
raise config.error(
'Unsupported shaper type: %s' % (self.shaper_type_y,))
self.saved_shaper_freq_x = self.saved_shaper_freq_y = 0.
self.stepper_kinematics = []
self.orig_stepper_kinematics = []
@@ -58,18 +57,25 @@ class InputShaper:
self._set_input_shaper(self.shaper_type_x, self.shaper_type_y,
self.shaper_freq_x, self.shaper_freq_y,
self.damping_ratio_x, self.damping_ratio_y)
def _get_shaper(self, shaper_type, shaper_freq, damping_ratio):
if not shaper_freq:
return shaper_defs.get_none_shaper()
A, T = self.shapers[shaper_type](shaper_freq, damping_ratio)
return len(A), A, T
def _set_input_shaper(self, shaper_type_x, shaper_type_y
, shaper_freq_x, shaper_freq_y
, damping_ratio_x, damping_ratio_y):
if (shaper_type_x != self.shaper_type_x
or shaper_type_y != self.shaper_type_y):
self.toolhead.flush_step_generation()
n_x, A_x, T_x = self._get_shaper(
shaper_type_x, shaper_freq_x, damping_ratio_x)
n_y, A_y, T_y = self._get_shaper(
shaper_type_y, shaper_freq_y, damping_ratio_y)
ffi_main, ffi_lib = chelper.get_ffi()
new_delay = max(
ffi_lib.input_shaper_get_step_generation_window(
shaper_type_x, shaper_freq_x, damping_ratio_x),
ffi_lib.input_shaper_get_step_generation_window(
shaper_type_y, shaper_freq_y, damping_ratio_y))
ffi_lib.input_shaper_get_step_generation_window(n_x, A_x, T_x),
ffi_lib.input_shaper_get_step_generation_window(n_y, A_y, T_y))
self.toolhead.note_step_generation_scan_time(new_delay,
old_delay=self.old_delay)
self.old_delay = new_delay
@@ -80,10 +86,8 @@ class InputShaper:
self.damping_ratio_x = damping_ratio_x
self.damping_ratio_y = damping_ratio_y
for sk in self.stepper_kinematics:
ffi_lib.input_shaper_set_shaper_params(sk
, shaper_type_x, shaper_type_y
, shaper_freq_x, shaper_freq_y
, damping_ratio_x, damping_ratio_y)
ffi_lib.input_shaper_set_shaper_params(
sk, len(A_x), A_x, T_x, len(A_y), A_y, T_y)
def disable_shaping(self):
if (self.saved_shaper_freq_x or self.saved_shaper_freq_y) and not (
self.shaper_freq_x or self.shaper_freq_y):
@@ -113,35 +117,29 @@ class InputShaper:
shaper_freq_y = gcmd.get_float(
'SHAPER_FREQ_Y', self.shaper_freq_y, minval=0.)
def parse_shaper(shaper_type_str):
shaper_type_str = shaper_type_str.lower()
if shaper_type_str not in self.shapers:
raise gcmd.error(
"Requested shaper type '%s' is not supported" % (
shaper_type_str))
return self.shapers[shaper_type_str]
shaper_type = gcmd.get('SHAPER_TYPE', None, parser=parse_shaper)
shaper_type = gcmd.get('SHAPER_TYPE', None)
if shaper_type is None:
shaper_type_x = gcmd.get('SHAPER_TYPE_X', self.shaper_type_x,
parser=parse_shaper)
shaper_type_y = gcmd.get('SHAPER_TYPE_Y', self.shaper_type_y,
parser=parse_shaper)
shaper_type_x = gcmd.get(
'SHAPER_TYPE_X', self.shaper_type_x).lower()
shaper_type_y = gcmd.get(
'SHAPER_TYPE_Y', self.shaper_type_y).lower()
else:
shaper_type_x = shaper_type_y = shaper_type
shaper_type_x = shaper_type_y = shaper_type.lower()
if shaper_type_x not in self.shapers:
raise gcmd.error('Unsupported shaper type: %s' % (shaper_type_x,))
if shaper_type_y not in self.shapers:
raise gcmd.error('Unsupported shaper type: %s' % (shaper_type_y,))
self._set_input_shaper(shaper_type_x, shaper_type_y,
shaper_freq_x, shaper_freq_y,
damping_ratio_x, damping_ratio_y)
id_to_name = {v: n for n, v in self.shapers.items()}
gcmd.respond_info("shaper_type_x:%s shaper_type_y:%s "
"shaper_freq_x:%.3f shaper_freq_y:%.3f "
"damping_ratio_x:%.6f damping_ratio_y:%.6f"
% (id_to_name[shaper_type_x],
id_to_name[shaper_type_y],
shaper_freq_x, shaper_freq_y,
damping_ratio_x, damping_ratio_y))
% (self.shaper_type_x, self.shaper_type_y,
self.shaper_freq_x, self.shaper_freq_y,
self.damping_ratio_x, self.damping_ratio_y))
def load_config(config):
return InputShaper(config)

View File

@@ -4,128 +4,16 @@
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import collections, importlib, logging, math, multiprocessing
shaper_defs = importlib.import_module('.shaper_defs', 'extras')
MIN_FREQ = 5.
MAX_FREQ = 200.
WINDOW_T_SEC = 0.5
MAX_SHAPER_FREQ = 150.
SHAPER_VIBRATION_REDUCTION=20.
TEST_DAMPING_RATIOS=[0.075, 0.1, 0.15]
SHAPER_DAMPING_RATIO = 0.1
######################################################################
# Input shapers
######################################################################
InputShaperCfg = collections.namedtuple(
'InputShaperCfg', ('name', 'init_func', 'min_freq'))
def get_zv_shaper(shaper_freq, damping_ratio):
df = math.sqrt(1. - damping_ratio**2)
K = math.exp(-damping_ratio * math.pi / df)
t_d = 1. / (shaper_freq * df)
A = [1., K]
T = [0., .5*t_d]
return (A, T)
def get_zvd_shaper(shaper_freq, damping_ratio):
df = math.sqrt(1. - damping_ratio**2)
K = math.exp(-damping_ratio * math.pi / df)
t_d = 1. / (shaper_freq * df)
A = [1., 2.*K, K**2]
T = [0., .5*t_d, t_d]
return (A, T)
def get_mzv_shaper(shaper_freq, damping_ratio):
df = math.sqrt(1. - damping_ratio**2)
K = math.exp(-.75 * damping_ratio * math.pi / df)
t_d = 1. / (shaper_freq * df)
a1 = 1. - 1. / math.sqrt(2.)
a2 = (math.sqrt(2.) - 1.) * K
a3 = a1 * K * K
A = [a1, a2, a3]
T = [0., .375*t_d, .75*t_d]
return (A, T)
def get_ei_shaper(shaper_freq, damping_ratio):
v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance
df = math.sqrt(1. - damping_ratio**2)
K = math.exp(-damping_ratio * math.pi / df)
t_d = 1. / (shaper_freq * df)
a1 = .25 * (1. + v_tol)
a2 = .5 * (1. - v_tol) * K
a3 = a1 * K * K
A = [a1, a2, a3]
T = [0., .5*t_d, t_d]
return (A, T)
def get_2hump_ei_shaper(shaper_freq, damping_ratio):
v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance
df = math.sqrt(1. - damping_ratio**2)
K = math.exp(-damping_ratio * math.pi / df)
t_d = 1. / (shaper_freq * df)
V2 = v_tol**2
X = pow(V2 * (math.sqrt(1. - V2) + 1.), 1./3.)
a1 = (3.*X*X + 2.*X + 3.*V2) / (16.*X)
a2 = (.5 - a1) * K
a3 = a2 * K
a4 = a1 * K * K * K
A = [a1, a2, a3, a4]
T = [0., .5*t_d, t_d, 1.5*t_d]
return (A, T)
def get_3hump_ei_shaper(shaper_freq, damping_ratio):
v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance
df = math.sqrt(1. - damping_ratio**2)
K = math.exp(-damping_ratio * math.pi / df)
t_d = 1. / (shaper_freq * df)
K2 = K*K
a1 = 0.0625 * (1. + 3. * v_tol + 2. * math.sqrt(2. * (v_tol + 1.) * v_tol))
a2 = 0.25 * (1. - v_tol) * K
a3 = (0.5 * (1. + v_tol) - 2. * a1) * K2
a4 = a2 * K2
a5 = a1 * K2 * K2
A = [a1, a2, a3, a4, a5]
T = [0., .5*t_d, t_d, 1.5*t_d, 2.*t_d]
return (A, T)
def get_shaper_smoothing(shaper, accel=5000, scv=5.):
half_accel = accel * .5
A, T = shaper
inv_D = 1. / 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.
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_180 *= inv_D
return max(offset_90, offset_180)
# min_freq for each shaper is chosen to have projected max_accel ~= 1500
INPUT_SHAPERS = [
InputShaperCfg('zv', get_zv_shaper, min_freq=21.),
InputShaperCfg('mzv', get_mzv_shaper, min_freq=23.),
InputShaperCfg('ei', get_ei_shaper, min_freq=29.),
InputShaperCfg('2hump_ei', get_2hump_ei_shaper, min_freq=39.),
InputShaperCfg('3hump_ei', get_3hump_ei_shaper, min_freq=48.),
]
AUTOTUNE_SHAPERS = ['zv', 'mzv', 'ei', '2hump_ei', '3hump_ei']
######################################################################
# Frequency response calculation and shaper auto-tuning
@@ -313,12 +201,32 @@ class ShaperCalibrate:
# The input shaper can only reduce the amplitude of vibrations by
# SHAPER_VIBRATION_REDUCTION times, so all vibrations below that
# threshold can be igonred
vibrations_threshold = psd.max() / SHAPER_VIBRATION_REDUCTION
vibr_threshold = psd.max() / shaper_defs.SHAPER_VIBRATION_REDUCTION
remaining_vibrations = self.numpy.maximum(
vals * psd - vibrations_threshold, 0).sum()
all_vibrations = self.numpy.maximum(psd - vibrations_threshold, 0).sum()
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
A, T = shaper
inv_D = 1. / 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.
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_180 *= inv_D
return max(offset_90, offset_180)
def fit_shaper(self, shaper_cfg, calibration_data, max_smoothing):
np = self.numpy
@@ -333,8 +241,9 @@ class ShaperCalibrate:
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_DAMPING_RATIO)
shaper_smoothing = get_shaper_smoothing(shaper)
shaper = shaper_cfg.init_func(
test_freq, shaper_defs.DEFAULT_DAMPING_RATIO)
shaper_smoothing = self._get_shaper_smoothing(shaper)
if max_smoothing and shaper_smoothing > max_smoothing and best_res:
return best_res
# Exact damping ratio of the printer is unknown, pessimizing
@@ -387,14 +296,16 @@ 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: get_shaper_smoothing(
max_accel = self._bisect(lambda test_accel: self._get_shaper_smoothing(
shaper, test_accel) <= TARGET_SMOOTHING)
return max_accel
def find_best_shaper(self, calibration_data, max_smoothing, logger=None):
best_shaper = None
all_shapers = []
for shaper_cfg in INPUT_SHAPERS:
for shaper_cfg in shaper_defs.INPUT_SHAPERS:
if shaper_cfg.name not in AUTOTUNE_SHAPERS:
continue
shaper = self.background_process_exec(self.fit_shaper, (
shaper_cfg, calibration_data, max_smoothing))
if logger is not None:

View File

@@ -0,0 +1,102 @@
# Definitions of the supported input shapers
#
# Copyright (C) 2020-2021 Dmitry Butyugin <dmbutyugin@google.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import collections, math
SHAPER_VIBRATION_REDUCTION=20.
DEFAULT_DAMPING_RATIO = 0.1
InputShaperCfg = collections.namedtuple(
'InputShaperCfg', ('name', 'init_func', 'min_freq'))
def get_none_shaper():
return ([], [])
def get_zv_shaper(shaper_freq, damping_ratio):
df = math.sqrt(1. - damping_ratio**2)
K = math.exp(-damping_ratio * math.pi / df)
t_d = 1. / (shaper_freq * df)
A = [1., K]
T = [0., .5*t_d]
return (A, T)
def get_zvd_shaper(shaper_freq, damping_ratio):
df = math.sqrt(1. - damping_ratio**2)
K = math.exp(-damping_ratio * math.pi / df)
t_d = 1. / (shaper_freq * df)
A = [1., 2.*K, K**2]
T = [0., .5*t_d, t_d]
return (A, T)
def get_mzv_shaper(shaper_freq, damping_ratio):
df = math.sqrt(1. - damping_ratio**2)
K = math.exp(-.75 * damping_ratio * math.pi / df)
t_d = 1. / (shaper_freq * df)
a1 = 1. - 1. / math.sqrt(2.)
a2 = (math.sqrt(2.) - 1.) * K
a3 = a1 * K * K
A = [a1, a2, a3]
T = [0., .375*t_d, .75*t_d]
return (A, T)
def get_ei_shaper(shaper_freq, damping_ratio):
v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance
df = math.sqrt(1. - damping_ratio**2)
K = math.exp(-damping_ratio * math.pi / df)
t_d = 1. / (shaper_freq * df)
a1 = .25 * (1. + v_tol)
a2 = .5 * (1. - v_tol) * K
a3 = a1 * K * K
A = [a1, a2, a3]
T = [0., .5*t_d, t_d]
return (A, T)
def get_2hump_ei_shaper(shaper_freq, damping_ratio):
v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance
df = math.sqrt(1. - damping_ratio**2)
K = math.exp(-damping_ratio * math.pi / df)
t_d = 1. / (shaper_freq * df)
V2 = v_tol**2
X = pow(V2 * (math.sqrt(1. - V2) + 1.), 1./3.)
a1 = (3.*X*X + 2.*X + 3.*V2) / (16.*X)
a2 = (.5 - a1) * K
a3 = a2 * K
a4 = a1 * K * K * K
A = [a1, a2, a3, a4]
T = [0., .5*t_d, t_d, 1.5*t_d]
return (A, T)
def get_3hump_ei_shaper(shaper_freq, damping_ratio):
v_tol = 1. / SHAPER_VIBRATION_REDUCTION # vibration tolerance
df = math.sqrt(1. - damping_ratio**2)
K = math.exp(-damping_ratio * math.pi / df)
t_d = 1. / (shaper_freq * df)
K2 = K*K
a1 = 0.0625 * (1. + 3. * v_tol + 2. * math.sqrt(2. * (v_tol + 1.) * v_tol))
a2 = 0.25 * (1. - v_tol) * K
a3 = (0.5 * (1. + v_tol) - 2. * a1) * K2
a4 = a2 * K2
a5 = a1 * K2 * K2
A = [a1, a2, a3, a4, a5]
T = [0., .5*t_d, t_d, 1.5*t_d, 2.*t_d]
return (A, T)
# min_freq for each shaper is chosen to have projected max_accel ~= 1500
INPUT_SHAPERS = [
InputShaperCfg('zv', get_zv_shaper, min_freq=21.),
InputShaperCfg('mzv', get_mzv_shaper, min_freq=23.),
InputShaperCfg('zvd', get_zvd_shaper, min_freq=29.),
InputShaperCfg('ei', get_ei_shaper, min_freq=29.),
InputShaperCfg('2hump_ei', get_2hump_ei_shaper, min_freq=39.),
InputShaperCfg('3hump_ei', get_3hump_ei_shaper, min_freq=48.),
]