smoothing vs accel plot added

This commit is contained in:
Félix Boisselier
2024-07-15 18:04:49 +02:00
parent ccd95e27e1
commit e364b9079e

View File

@@ -22,7 +22,7 @@
import optparse import optparse
import os import os
from datetime import datetime from datetime import datetime
from typing import List, Optional from typing import Dict, List, Optional
import matplotlib import matplotlib
import matplotlib.font_manager import matplotlib.font_manager
@@ -47,7 +47,8 @@ PEAKS_DETECTION_THRESHOLD = 0.05
PEAKS_EFFECT_THRESHOLD = 0.12 PEAKS_EFFECT_THRESHOLD = 0.12
SPECTROGRAM_LOW_PERCENTILE_FILTER = 5 SPECTROGRAM_LOW_PERCENTILE_FILTER = 5
MAX_VIBRATIONS = 5.0 MAX_VIBRATIONS = 5.0
SMOOTHING_LIST = [0.1]
# SMOOTHING_LIST = np.arange(0.001, 0.80, 0.05)
KLIPPAIN_COLORS = { KLIPPAIN_COLORS = {
'purple': '#70088C', 'purple': '#70088C',
'orange': '#FF8D32', 'orange': '#FF8D32',
@@ -112,15 +113,13 @@ def calibrate_shaper(datas: List[np.ndarray], max_smoothing: Optional[float], sc
calibration_data = helper.process_accelerometer_data(datas) calibration_data = helper.process_accelerometer_data(datas)
calibration_data.normalize_to_frequencies() calibration_data.normalize_to_frequencies()
# We compute the damping ratio using the Klipper's default value if it fails
fr, zeta, _, _ = compute_mechanical_parameters(calibration_data.psd_sum, calibration_data.freq_bins) fr, zeta, _, _ = compute_mechanical_parameters(calibration_data.psd_sum, calibration_data.freq_bins)
zeta = zeta if zeta is not None else 0.1
# If the damping ratio computation fail, we use Klipper default value instead
if zeta is None:
zeta = 0.1
compat = False compat = False
try: try:
shaper, all_shapers = helper.find_best_shaper( k_shaper_choice, all_shapers = helper.find_best_shaper(
calibration_data, calibration_data,
shapers=None, shapers=None,
damping_ratio=zeta, damping_ratio=zeta,
@@ -129,23 +128,79 @@ def calibrate_shaper(datas: List[np.ndarray], max_smoothing: Optional[float], sc
max_smoothing=max_smoothing, max_smoothing=max_smoothing,
test_damping_ratios=None, test_damping_ratios=None,
max_freq=max_freq, max_freq=max_freq,
logger=ConsoleOutput.print, logger=None,
)
ConsoleOutput.print(
(
f'Detected a square corner velocity of {scv:.1f} and a damping ratio of {zeta:.3f}. '
'These values will be used to compute the input shaper filter recommendations'
)
) )
except TypeError: except TypeError:
ConsoleOutput.print( ConsoleOutput.print(
'[WARNING] You seem to be using an older version of Klipper that is not compatible with all the latest Shake&Tune features!' (
) '[WARNING] You seem to be using an older version of Klipper that is not compatible with all the latest '
ConsoleOutput.print( 'Shake&Tune features!\nShake&Tune now runs in compatibility mode: be aware that the results may be '
'Shake&Tune now runs in compatibility mode: be aware that the results may be slightly off, since the real damping ratio cannot be used to create the filter recommendations' 'slightly off, since the real damping ratio cannot be used to craft accurate filter recommendations'
)
) )
compat = True compat = True
shaper, all_shapers = helper.find_best_shaper(calibration_data, max_smoothing, ConsoleOutput.print) k_shaper_choice, all_shapers = helper.find_best_shaper(calibration_data, max_smoothing, None)
ConsoleOutput.print( # If max_smoothing is not None, we run the same computation but without a smoothing value
f'\n-> Recommended shaper is {shaper.name.upper()} @ {shaper.freq:.1f} Hz (when using a square corner velocity of {scv:.1f} and a damping ratio of {zeta:.3f})' # to get the max smoothing values from the filters and create the testing list
all_shapers_nosmoothing = None
if max_smoothing is not None:
if compat:
_, all_shapers_nosmoothing = helper.find_best_shaper(calibration_data, None, None)
else:
_, all_shapers_nosmoothing = helper.find_best_shaper(
calibration_data,
shapers=None,
damping_ratio=zeta,
scv=scv,
shaper_freqs=None,
max_smoothing=None,
test_damping_ratios=None,
max_freq=max_freq,
logger=None,
)
# Then we iterate over the all_shaperts_nosmoothing list to get the max of the smoothing values
max_smoothing = 0.0
if all_shapers_nosmoothing is not None:
for shaper in all_shapers_nosmoothing:
if shaper.smoothing > max_smoothing:
max_smoothing = shaper.smoothing
else:
for shaper in all_shapers:
if shaper.smoothing > max_smoothing:
max_smoothing = shaper.smoothing
# Then we create a list of smoothing values to test (no need to test the max smoothing value as it was already tested)
smoothing_test_list = np.linspace(0.001, max_smoothing, 10)[:-1]
additional_all_shapers = {}
for smoothing in smoothing_test_list:
if compat:
_, all_shapers_bis = helper.find_best_shaper(calibration_data, smoothing, None)
else:
_, all_shapers_bis = helper.find_best_shaper(
calibration_data,
shapers=None,
damping_ratio=zeta,
scv=scv,
shaper_freqs=None,
max_smoothing=smoothing,
test_damping_ratios=None,
max_freq=max_freq,
logger=None,
)
additional_all_shapers[f'sm_{smoothing}'] = all_shapers_bis
additional_all_shapers['max_smoothing'] = (
all_shapers_nosmoothing if all_shapers_nosmoothing is not None else all_shapers
) )
return shaper.name, all_shapers, calibration_data, fr, zeta, compat return k_shaper_choice.name, all_shapers, additional_all_shapers, calibration_data, fr, zeta, max_smoothing, compat
###################################################################### ######################################################################
@@ -164,7 +219,7 @@ def plot_freq_response(
fr: float, fr: float,
zeta: float, zeta: float,
max_freq: float, max_freq: float,
) -> None: ) -> Dict[str, List[Dict[str, str]]]:
freqs = calibration_data.freqs freqs = calibration_data.freqs
psd = calibration_data.psd_sum psd = calibration_data.psd_sum
px = calibration_data.psd_x px = calibration_data.psd_x
@@ -193,27 +248,40 @@ def plot_freq_response(
ax2 = ax.twinx() ax2 = ax.twinx()
ax2.yaxis.set_visible(False) ax2.yaxis.set_visible(False)
shaper_table_data = {
'shapers': [],
'recommendations': [],
'damping_ratio': zeta,
}
# Draw the shappers curves and add their specific parameters in the legend # Draw the shappers curves and add their specific parameters in the legend
perf_shaper_choice = None perf_shaper_choice = None
perf_shaper_vals = None perf_shaper_vals = None
perf_shaper_freq = None perf_shaper_freq = None
perf_shaper_accel = 0 perf_shaper_accel = 0
for shaper in shapers: for shaper in shapers:
shaper_max_accel = round(shaper.max_accel / 100.0) * 100.0 ax2.plot(freqs, shaper.vals, label=shaper.name.upper(), linestyle='dotted')
label = f'{shaper.name.upper()} ({shaper.freq:.1f} Hz, vibr={shaper.vibrs * 100.0:.1f}%, sm~={shaper.smoothing:.2f}, accel<={shaper_max_accel:.0f})'
ax2.plot(freqs, shaper.vals, label=label, linestyle='dotted') shaper_info = {
'type': shaper.name.upper(),
'frequency': shaper.freq,
'vibrations': shaper.vibrs,
'smoothing': shaper.smoothing,
'max_accel': shaper.max_accel,
}
shaper_table_data['shapers'].append(shaper_info)
# Get the Klipper recommended shaper (usually it's a good low vibration compromise) # Get the Klipper recommended shaper (usually it's a good low vibration compromise)
if shaper.name == klipper_shaper_choice: if shaper.name == klipper_shaper_choice:
klipper_shaper_freq = shaper.freq klipper_shaper_freq = shaper.freq
klipper_shaper_vals = shaper.vals klipper_shaper_vals = shaper.vals
klipper_shaper_accel = shaper_max_accel klipper_shaper_accel = shaper.max_accel
# Find the shaper with the highest accel but with vibrs under MAX_VIBRATIONS as it's # Find the shaper with the highest accel but with vibrs under MAX_VIBRATIONS as it's
# a good performance compromise when injecting the SCV and damping ratio in the computation # a good performance compromise when injecting the SCV and damping ratio in the computation
if perf_shaper_accel < shaper_max_accel and shaper.vibrs * 100 < MAX_VIBRATIONS: if perf_shaper_accel < shaper.max_accel and shaper.vibrs * 100 < MAX_VIBRATIONS:
perf_shaper_choice = shaper.name perf_shaper_choice = shaper.name
perf_shaper_accel = shaper_max_accel perf_shaper_accel = shaper.max_accel
perf_shaper_freq = shaper.freq perf_shaper_freq = shaper.freq
perf_shaper_vals = shaper.vals perf_shaper_vals = shaper.vals
@@ -226,32 +294,30 @@ def plot_freq_response(
and perf_shaper_choice != klipper_shaper_choice and perf_shaper_choice != klipper_shaper_choice
and perf_shaper_accel >= klipper_shaper_accel and perf_shaper_accel >= klipper_shaper_accel
): ):
ax2.plot( perf_shaper_string = f'Recommended performance shaper: {perf_shaper_choice.upper()} @ {perf_shaper_freq:.1f} Hz'
[], lowvibr_shaper_string = (
[], f'Recommended low vibrations shaper: {klipper_shaper_choice.upper()} @ {klipper_shaper_freq:.1f} Hz'
' ',
label=f'Recommended performance shaper: {perf_shaper_choice.upper()} @ {perf_shaper_freq:.1f} Hz',
) )
shaper_table_data['recommendations'].append(perf_shaper_string)
shaper_table_data['recommendations'].append(lowvibr_shaper_string)
ConsoleOutput.print(f'{perf_shaper_string} (with a damping ratio of {zeta:.3f})')
ConsoleOutput.print(f'{lowvibr_shaper_string} (with a damping ratio of {zeta:.3f})')
ax.plot( ax.plot(
freqs, freqs,
psd * perf_shaper_vals, psd * perf_shaper_vals,
label=f'With {perf_shaper_choice.upper()} applied', label=f'With {perf_shaper_choice.upper()} applied',
color='cyan', color='cyan',
) )
ax2.plot( ax.plot(
[], freqs,
[], psd * klipper_shaper_vals,
' ', label=f'With {klipper_shaper_choice.upper()} applied',
label=f'Recommended low vibrations shaper: {klipper_shaper_choice.upper()} @ {klipper_shaper_freq:.1f} Hz', color='lime',
) )
ax.plot(freqs, psd * klipper_shaper_vals, label=f'With {klipper_shaper_choice.upper()} applied', color='lime')
else: else:
ax2.plot( shaper_string = f'Recommended best shaper: {klipper_shaper_choice.upper()} @ {klipper_shaper_freq:.1f} Hz'
[], shaper_table_data['recommendations'].append(shaper_string)
[], ConsoleOutput.print(f'{shaper_string} (with a damping ratio of {zeta:.3f})')
' ',
label=f'Recommended performance shaper: {klipper_shaper_choice.upper()} @ {klipper_shaper_freq:.1f} Hz',
)
ax.plot( ax.plot(
freqs, freqs,
psd * klipper_shaper_vals, psd * klipper_shaper_vals,
@@ -259,9 +325,6 @@ def plot_freq_response(
color='cyan', color='cyan',
) )
# And the estimated damping ratio is finally added at the end of the legend
ax2.plot([], [], ' ', label=f'Estimated damping ratio (ζ): {zeta:.3f}')
# Draw the detected peaks and name them # Draw the detected peaks and name them
# This also draw the detection threshold and warning threshold (aka "effect zone") # This also draw the detection threshold and warning threshold (aka "effect zone")
ax.plot(peaks_freqs, psd[peaks], 'x', color='black', markersize=8) ax.plot(peaks_freqs, psd[peaks], 'x', color='black', markersize=8)
@@ -297,7 +360,7 @@ def plot_freq_response(
ax.legend(loc='upper left', prop=fontP) ax.legend(loc='upper left', prop=fontP)
ax2.legend(loc='upper right', prop=fontP) ax2.legend(loc='upper right', prop=fontP)
return return shaper_table_data
# Plot a time-frequency spectrogram to see how the system respond over time during the # Plot a time-frequency spectrogram to see how the system respond over time during the
@@ -350,6 +413,96 @@ def plot_spectrogram(
return return
def plot_smoothing_vs_accel(
ax: plt.Axes,
shaper_table_data: Dict[str, List[Dict[str, str]]],
additional_shapers: Dict[str, List[Dict[str, str]]],
) -> None:
fontP = matplotlib.font_manager.FontProperties()
fontP.set_size('x-small')
ax.xaxis.set_minor_locator(matplotlib.ticker.MultipleLocator(1000))
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
ax.grid(which='major', color='grey')
ax.grid(which='minor', color='lightgrey')
shaper_data = {}
# Extract data from additional_shapers first
for _, shapers in additional_shapers.items():
for shaper in shapers:
shaper_type = shaper.name.upper()
if shaper_type not in shaper_data:
shaper_data[shaper_type] = []
shaper_data[shaper_type].append(
{
'smoothing': shaper.smoothing,
'max_accel': shaper.max_accel,
'vibrs': shaper.vibrs * 100.0,
}
)
# Extract data from shaper_table_data and insert into shaper_data
for shaper in shaper_table_data['shapers']:
shaper_type = shaper['type']
if shaper_type not in shaper_data:
shaper_data[shaper_type] = []
shaper_data[shaper_type].append(
{
'smoothing': float(shaper['smoothing']),
'max_accel': float(shaper['max_accel']),
'vibrs': float(shaper['vibrations']) * 100.0,
}
)
# Plot each shaper type and add colorbar for vibrations
for _, (shaper_type, data) in enumerate(shaper_data.items()):
smoothing_values = [d['smoothing'] for d in data]
max_accel_values = [d['max_accel'] for d in data]
vibrs_values = [d['vibrs'] for d in data]
ax.plot(max_accel_values, smoothing_values, linestyle=':', label=f'{shaper_type}', zorder=10)
scatter = ax.scatter(
max_accel_values, smoothing_values, c=vibrs_values, cmap='plasma', s=100, edgecolors='w', zorder=15
)
# Add colorbar for vibrations
cbar = plt.colorbar(scatter, ax=ax)
cbar.set_label('Remaining Vibrations (%)')
ax.set_xlabel('Max Acceleration')
ax.set_ylabel('Smoothing')
ax.set_title(
'Smoothing vs Max Acceleration',
fontsize=14,
color=KLIPPAIN_COLORS['dark_orange'],
weight='bold',
)
ax.legend(loc='upper right', prop=fontP)
def print_shaper_table(fig: plt.Figure, shaper_table_data: Dict[str, List[Dict[str, str]]]) -> None:
columns = ['Type', 'Frequency', 'Vibrations', 'Smoothing', 'Max Accel']
table_data = []
for shaper_info in shaper_table_data['shapers']:
row = [
f'{shaper_info["type"].upper()}',
f'{shaper_info["frequency"]:.1f} Hz',
f'{shaper_info["vibrations"] * 100:.1f} %',
f'{shaper_info["smoothing"]:.3f}',
f'{round(shaper_info["max_accel"] / 10) * 10:.0f}',
]
table_data.append(row)
table = plt.table(cellText=table_data, colLabels=columns, bbox=[1.12, -0.4, 0.75, 0.25], cellLoc='center')
table.auto_set_font_size(False)
table.set_fontsize(10)
table.auto_set_column_width([0, 1, 2, 3, 4])
table.set_zorder(100)
# Add the recommendations and damping ratio using fig.text
fig.text(0.58, 0.235, f'Estimated damping ratio (ζ): {shaper_table_data["damping_ratio"]:.3f}', fontsize=14)
fig.text(0.58, 0.210, '\n'.join(shaper_table_data['recommendations']), fontsize=14)
###################################################################### ######################################################################
# Startup and main routines # Startup and main routines
###################################################################### ######################################################################
@@ -375,8 +528,8 @@ def shaper_calibration(
ConsoleOutput.print('Warning: incorrect number of .csv files detected. Only the first one will be used!') ConsoleOutput.print('Warning: incorrect number of .csv files detected. Only the first one will be used!')
# Compute shapers, PSD outputs and spectrogram # Compute shapers, PSD outputs and spectrogram
klipper_shaper_choice, shapers, calibration_data, fr, zeta, compat = calibrate_shaper( klipper_shaper_choice, shapers, additional_shapers, calibration_data, fr, zeta, max_smoothing_computed, compat = (
datas[0], max_smoothing, scv, max_freq calibrate_shaper(datas[0], max_smoothing, scv, max_freq)
) )
pdata, bins, t = compute_spectrogram(datas[0]) pdata, bins, t = compute_spectrogram(datas[0])
del datas del datas
@@ -400,29 +553,31 @@ def shaper_calibration(
peak_freqs_formated = ['{:.1f}'.format(f) for f in peaks_freqs] peak_freqs_formated = ['{:.1f}'.format(f) for f in peaks_freqs]
num_peaks_above_effect_threshold = np.sum(calibration_data.psd_sum[peaks] > peaks_threshold[1]) num_peaks_above_effect_threshold = np.sum(calibration_data.psd_sum[peaks] > peaks_threshold[1])
ConsoleOutput.print( ConsoleOutput.print(
f"\nPeaks detected on the graph: {num_peaks} @ {', '.join(map(str, peak_freqs_formated))} Hz ({num_peaks_above_effect_threshold} above effect threshold)" f"Peaks detected on the graph: {num_peaks} @ {', '.join(map(str, peak_freqs_formated))} Hz ({num_peaks_above_effect_threshold} above effect threshold)"
) )
# Create graph layout # Create graph layout
fig, (ax1, ax2) = plt.subplots( fig, ((ax1, ax3), (ax2, ax4)) = plt.subplots(
2,
2, 2,
1,
gridspec_kw={ gridspec_kw={
'height_ratios': [4, 3], 'height_ratios': [4, 3],
'width_ratios': [5, 4],
'bottom': 0.050, 'bottom': 0.050,
'top': 0.890, 'top': 0.890,
'left': 0.085, 'left': 0.048,
'right': 0.966, 'right': 0.966,
'hspace': 0.169, 'hspace': 0.169,
'wspace': 0.200, 'wspace': 0.150,
}, },
) )
fig.set_size_inches(8.3, 11.6) ax4.remove()
fig.set_size_inches(15, 11.6)
# Add a title with some test info # Add a title with some test info
title_line1 = 'INPUT SHAPER CALIBRATION TOOL' title_line1 = 'INPUT SHAPER CALIBRATION TOOL'
fig.text( fig.text(
0.12, 0.965, title_line1, ha='left', va='bottom', fontsize=20, color=KLIPPAIN_COLORS['purple'], weight='bold' 0.065, 0.965, title_line1, ha='left', va='bottom', fontsize=20, color=KLIPPAIN_COLORS['purple'], weight='bold'
) )
try: try:
filename_parts = (lognames[0].split('/')[-1]).split('_') filename_parts = (lognames[0].split('/')[-1]).split('_')
@@ -433,8 +588,11 @@ def shaper_calibration(
title_line4 = '| and SCV are not used for filter recommendations!' title_line4 = '| and SCV are not used for filter recommendations!'
title_line5 = f'| Accel per Hz used: {accel_per_hz} mm/s²/Hz' if accel_per_hz is not None else '' title_line5 = f'| Accel per Hz used: {accel_per_hz} mm/s²/Hz' if accel_per_hz is not None else ''
else: else:
max_smoothing_string = (
f'maximum ({max_smoothing_computed:0.3f})' if max_smoothing is None else f'{max_smoothing:0.3f}'
)
title_line3 = f'| Square corner velocity: {scv} mm/s' title_line3 = f'| Square corner velocity: {scv} mm/s'
title_line4 = f'| Max allowed smoothing: {max_smoothing}' title_line4 = f'| Allowed smoothing: {max_smoothing_string}'
title_line5 = f'| Accel per Hz used: {accel_per_hz} mm/s²/Hz' if accel_per_hz is not None else '' title_line5 = f'| Accel per Hz used: {accel_per_hz} mm/s²/Hz' if accel_per_hz is not None else ''
except Exception: except Exception:
ConsoleOutput.print(f'Warning: CSV filename look to be different than expected ({lognames[0]})') ConsoleOutput.print(f'Warning: CSV filename look to be different than expected ({lognames[0]})')
@@ -442,19 +600,22 @@ def shaper_calibration(
title_line3 = '' title_line3 = ''
title_line4 = '' title_line4 = ''
title_line5 = '' title_line5 = ''
fig.text(0.12, 0.957, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple']) fig.text(0.065, 0.957, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple'])
fig.text(0.58, 0.963, title_line3, ha='left', va='top', fontsize=10, color=KLIPPAIN_COLORS['dark_purple']) fig.text(0.50, 0.990, title_line3, ha='left', va='top', fontsize=14, color=KLIPPAIN_COLORS['dark_purple'])
fig.text(0.58, 0.948, title_line4, ha='left', va='top', fontsize=10, color=KLIPPAIN_COLORS['dark_purple']) fig.text(0.50, 0.968, title_line4, ha='left', va='top', fontsize=14, color=KLIPPAIN_COLORS['dark_purple'])
fig.text(0.58, 0.933, title_line5, ha='left', va='top', fontsize=10, color=KLIPPAIN_COLORS['dark_purple']) fig.text(0.501, 0.945, title_line5, ha='left', va='top', fontsize=10, color=KLIPPAIN_COLORS['dark_purple'])
# Plot the graphs # Plot the graphs
plot_freq_response( shaper_table_data = plot_freq_response(
ax1, calibration_data, shapers, klipper_shaper_choice, peaks, peaks_freqs, peaks_threshold, fr, zeta, max_freq ax1, calibration_data, shapers, klipper_shaper_choice, peaks, peaks_freqs, peaks_threshold, fr, zeta, max_freq
) )
plot_spectrogram(ax2, t, bins, pdata, peaks_freqs, max_freq) plot_spectrogram(ax2, t, bins, pdata, peaks_freqs, max_freq)
plot_smoothing_vs_accel(ax3, shaper_table_data, additional_shapers)
print_shaper_table(fig, shaper_table_data)
# Adding a small Klippain logo to the top left corner of the figure # Adding a small Klippain logo to the top left corner of the figure
ax_logo = fig.add_axes([0.001, 0.8995, 0.1, 0.1], anchor='NW') ax_logo = fig.add_axes([0.001, 0.924, 0.075, 0.075], anchor='NW')
ax_logo.imshow(plt.imread(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'klippain.png'))) ax_logo.imshow(plt.imread(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'klippain.png')))
ax_logo.axis('off') ax_logo.axis('off')