From 4297aef0f5c26219afb68002b7346fba63bf2530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Boisselier?= Date: Thu, 28 Mar 2024 17:46:14 +0100 Subject: [PATCH] cleaning up and automating the vibrations measurement --- K-ShakeTune/K-SnT_speed_vibrations.cfg | 166 ------- ...al_vibrations.cfg => K-SnT_vibrations.cfg} | 20 +- K-ShakeTune/scripts/graph_speed_vibrations.py | 426 ------------------ ..._dir_vibrations.py => graph_vibrations.py} | 28 +- K-ShakeTune/scripts/is_workflow.py | 32 +- 5 files changed, 44 insertions(+), 628 deletions(-) delete mode 100644 K-ShakeTune/K-SnT_speed_vibrations.cfg rename K-ShakeTune/{K-SnT_directional_vibrations.cfg => K-SnT_vibrations.cfg} (92%) delete mode 100755 K-ShakeTune/scripts/graph_speed_vibrations.py rename K-ShakeTune/scripts/{graph_dir_vibrations.py => graph_vibrations.py} (96%) diff --git a/K-ShakeTune/K-SnT_speed_vibrations.cfg b/K-ShakeTune/K-SnT_speed_vibrations.cfg deleted file mode 100644 index 333e3d2..0000000 --- a/K-ShakeTune/K-SnT_speed_vibrations.cfg +++ /dev/null @@ -1,166 +0,0 @@ -####################################### -###### SPEED VIBRATIONS ANALYSIS ###### -####################################### -# Written by Frix_x#0161 # - -[gcode_macro SPEED_VIBRATIONS_PROFILE] -gcode: - {% set size = params.SIZE|default(60)|int %} # size of the area where the movements are done - {% set direction = params.DIRECTION|default('XY') %} # can be set to either XY, AB, ABXY, A, B, X, Y, Z - {% set z_height = params.Z_HEIGHT|default(20)|int %} # z height to put the toolhead before starting the movements - - {% set min_speed = params.MIN_SPEED|default(20)|float * 60 %} # minimum feedrate for the movements - {% set max_speed = params.MAX_SPEED|default(200)|float * 60 %} # maximum feedrate for the movements - {% set speed_increment = params.SPEED_INCREMENT|default(2)|float * 60 %} # feedrate increment between each move - {% set feedrate_travel = params.TRAVEL_SPEED|default(200)|int * 60 %} # travel feedrate between moves - {% set accel = params.ACCEL|default(3000)|int %} # accel value used to move on the pattern - {% set accel_chip = params.ACCEL_CHIP|default("adxl345") %} # ADXL chip name in the config - - {% set keep_results = params.KEEP_N_RESULTS|default(3)|int %} - {% set keep_csv = params.KEEP_CSV|default(True) %} - - {% set mid_x = printer.toolhead.axis_maximum.x|float / 2 %} - {% set mid_y = printer.toolhead.axis_maximum.y|float / 2 %} - {% set nb_samples = ((max_speed - min_speed) / speed_increment + 1) | int %} - - {% set accel = [accel, printer.configfile.settings.printer.max_accel]|min %} - {% set old_accel = printer.toolhead.max_accel %} - {% set old_accel_to_decel = printer.toolhead.max_accel_to_decel %} - {% set old_sqv = printer.toolhead.square_corner_velocity %} - - {% set direction_factor = { - 'XY' : { - 'start' : {'x': -0.5, 'y': -0.5 }, - 'move_factors' : { - '0' : {'x': 0.5, 'y': -0.5, 'z': 0.0 }, - '1' : {'x': 0.5, 'y': 0.5, 'z': 0.0 }, - '2' : {'x': -0.5, 'y': 0.5, 'z': 0.0 }, - '3' : {'x': -0.5, 'y': -0.5, 'z': 0.0 } - } - }, - 'AB' : { - 'start' : {'x': 0.0, 'y': 0.0 }, - 'move_factors' : { - '0' : {'x': 0.5, 'y': -0.5, 'z': 0.0 }, - '1' : {'x': -0.5, 'y': 0.5, 'z': 0.0 }, - '2' : {'x': 0.0, 'y': 0.0, 'z': 0.0 }, - '3' : {'x': 0.5, 'y': 0.5, 'z': 0.0 }, - '4' : {'x': -0.5, 'y': -0.5, 'z': 0.0 }, - '5' : {'x': 0.0, 'y': 0.0, 'z': 0.0 } - } - }, - 'ABXY' : { - 'start' : {'x': -0.5, 'y': 0.5 }, - 'move_factors' : { - '0' : {'x': -0.5, 'y': -0.5, 'z': 0.0 }, - '1' : {'x': 0.5, 'y': -0.5, 'z': 0.0 }, - '2' : {'x': -0.5, 'y': 0.5, 'z': 0.0 }, - '3' : {'x': 0.5, 'y': 0.5, 'z': 0.0 }, - '4' : {'x': -0.5, 'y': -0.5, 'z': 0.0 }, - '5' : {'x': -0.5, 'y': 0.5, 'z': 0.0 } - } - }, - 'B' : { - 'start' : {'x': 0.5, 'y': 0.5 }, - 'move_factors' : { - '0' : {'x': -0.5, 'y': -0.5, 'z': 0.0 }, - '1' : {'x': 0.5, 'y': 0.5, 'z': 0.0 } - } - }, - 'A' : { - 'start' : {'x': -0.5, 'y': 0.5 }, - 'move_factors' : { - '0' : {'x': 0.5, 'y': -0.5, 'z': 0.0 }, - '1' : {'x': -0.5, 'y': 0.5, 'z': 0.0 } - } - }, - 'X' : { - 'start' : {'x': -0.5, 'y': 0.0 }, - 'move_factors' : { - '0' : {'x': 0.5, 'y': 0.0, 'z': 0.0 }, - '1' : {'x': -0.5, 'y': 0.0, 'z': 0.0 } - } - }, - 'Y' : { - 'start' : {'x': 0.0, 'y': 0.5 }, - 'move_factors' : { - '0' : {'x': 0.0, 'y': -0.5, 'z': 0.0 }, - '1' : {'x': 0.0, 'y': 0.5, 'z': 0.0 } - } - }, - 'Z' : { - 'start' : {'x': 0.0, 'y': 0.0 }, - 'move_factors' : { - '0' : {'x': 0.0, 'y': 0.0, 'z': 1.0 }, - '1' : {'x': 0.0, 'y': 0.0, 'z': 0.0 } - } - }, - 'E' : { - 'start' : {'x': 0.0, 'y': 0.0 }, - 'move_factor' : 0.05 - } - } - %} - - - {% if not 'xyz' in printer.toolhead.homed_axes %} - { action_raise_error("Must Home printer first!") } - {% endif %} - - {% if params.SPEED_INCREMENT|default(2)|float * 100 != (params.SPEED_INCREMENT|default(2)|float * 100)|int %} - { action_raise_error("Only 2 decimal digits are allowed for SPEED_INCREMENT") } - {% endif %} - - {% if (size / (max_speed / 60)) < 0.25 and direction != 'E' %} - { action_raise_error("SIZE is too small for this MAX_SPEED. Increase SIZE or decrease MAX_SPEED!") } - {% endif %} - - {% if not (direction in direction_factor) %} - { action_raise_error("DIRECTION is not valid. Only XY, AB, ABXY, A, B, X, Y, Z or E is allowed!") } - {% endif %} - - {action_respond_info("")} - {action_respond_info("Starting machine speed vibrations profile measurement")} - {action_respond_info("This operation can not be interrupted by normal means. Hit the \"emergency stop\" button to stop it if needed")} - {action_respond_info("")} - - SAVE_GCODE_STATE NAME=STATE_SPEED_VIBRATIONS_PROFILE - - G90 - - # Set the wanted acceleration values (not too high to avoid oscillation, not too low to be able to reach constant speed on each segments) - SET_VELOCITY_LIMIT ACCEL={accel} ACCEL_TO_DECEL={accel} SQUARE_CORNER_VELOCITY={[(accel / 1000), 5.0]|max} - - # Going to the start position - G1 Z{z_height} F{feedrate_travel / 10} - G1 X{mid_x + (size * direction_factor[direction].start.x) } Y{mid_y + (size * direction_factor[direction].start.y)} F{feedrate_travel} - - # vibration pattern for each frequency - {% for curr_sample in range(0, nb_samples) %} - {% set curr_speed = min_speed + curr_sample * speed_increment %} - RESPOND MSG="{"Current speed: %.2f mm/s" % (curr_speed / 60)|float}" - - ACCELEROMETER_MEASURE CHIP={accel_chip} - {% if direction == 'E' %} - G0 E{curr_speed*direction_factor[direction].move_factor} F{curr_speed} - {% else %} - {% for key, factor in direction_factor[direction].move_factors|dictsort %} - G1 X{mid_x + (size * factor.x) } Y{mid_y + (size * factor.y)} Z{z_height + (size * factor.z)} F{curr_speed} - {% endfor %} - {% endif %} - ACCELEROMETER_MEASURE CHIP={accel_chip} NAME=sp{("%.2f" % (curr_speed / 60)|float)|replace('.','_')}n1 - G4 P300 - - M400 - {% endfor %} - - RESPOND MSG="Machine speed vibrations profile generation..." - RESPOND MSG="This may take some time (3-5min)" - RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type speed_vibrations --axis_name {direction} --accel {accel|int} --chip_name {accel_chip} {% if keep_csv %}--keep_csv{% endif %}" - M400 - RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type clean --keep_results {keep_results}" - - # Restore the previous acceleration values - SET_VELOCITY_LIMIT ACCEL={old_accel} ACCEL_TO_DECEL={old_accel_to_decel} SQUARE_CORNER_VELOCITY={old_sqv} - - RESTORE_GCODE_STATE NAME=STATE_SPEED_VIBRATIONS_PROFILE diff --git a/K-ShakeTune/K-SnT_directional_vibrations.cfg b/K-ShakeTune/K-SnT_vibrations.cfg similarity index 92% rename from K-ShakeTune/K-SnT_directional_vibrations.cfg rename to K-ShakeTune/K-SnT_vibrations.cfg index 315679e..58587c3 100644 --- a/K-ShakeTune/K-SnT_directional_vibrations.cfg +++ b/K-ShakeTune/K-SnT_vibrations.cfg @@ -1,9 +1,9 @@ -############################################# -###### DIRECTIONAL VIBRATIONS ANALYSIS ###### -############################################# +######################################### +###### MACHINE VIBRATIONS ANALYSIS ###### +######################################### # Written by Frix_x#0161 # -[gcode_macro DIRECTIONAL_VIBRATIONS_PROFILE] +[gcode_macro CREATE_VIBRATIONS_PROFILE] gcode: {% set size = params.SIZE|default(100)|int %} # size of the circle where the angled lines are done {% set z_height = params.Z_HEIGHT|default(20)|int %} # z height to put the toolhead before starting the movements @@ -44,11 +44,11 @@ gcode: {% endif %} {action_respond_info("")} - {action_respond_info("Starting machine directional vibrations profile measurement")} + {action_respond_info("Starting machine vibrations profile measurement")} {action_respond_info("This operation can not be interrupted by normal means. Hit the \"emergency stop\" button to stop it if needed")} {action_respond_info("")} - SAVE_GCODE_STATE NAME=STATE_DIRECTIONAL_VIBRATIONS_PROFILE + SAVE_GCODE_STATE NAME=CREATE_VIBRATIONS_PROFILE G90 @@ -158,13 +158,13 @@ gcode: {% endfor %} - RESPOND MSG="Machine directional vibrations profile generation..." + RESPOND MSG="Machine vibrations profile generation..." RESPOND MSG="This may take some time (3-5min)" - # RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type dir_vibrations --accel {accel|int} --chip_name {accel_chip} {% if keep_csv %}--keep_csv{% endif %}" + RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type vibrations --accel {accel|int} --kinematics {kinematics} --chip_name {accel_chip} {% if keep_csv %}--keep_csv{% endif %}" M400 - # RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type clean --keep_results {keep_results}" + RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type clean --keep_results {keep_results}" # Restore the previous acceleration values SET_VELOCITY_LIMIT ACCEL={old_accel} ACCEL_TO_DECEL={old_accel_to_decel} SQUARE_CORNER_VELOCITY={old_sqv} - RESTORE_GCODE_STATE NAME=STATE_DIRECTIONAL_VIBRATIONS_PROFILE + RESTORE_GCODE_STATE NAME=CREATE_VIBRATIONS_PROFILE diff --git a/K-ShakeTune/scripts/graph_speed_vibrations.py b/K-ShakeTune/scripts/graph_speed_vibrations.py deleted file mode 100755 index 0fb66da..0000000 --- a/K-ShakeTune/scripts/graph_speed_vibrations.py +++ /dev/null @@ -1,426 +0,0 @@ -#!/usr/bin/env python3 - -################################################## -###### SPEED AND VIBRATIONS PLOTTING SCRIPT ###### -################################################## -# Written by Frix_x#0161 # - -# Be sure to make this script executable using SSH: type 'chmod +x ./graph_speed_vibrations.py' when in the folder ! - -##################################################################### -################ !!! DO NOT EDIT BELOW THIS LINE !!! ################ -##################################################################### - -import optparse, matplotlib, re, os, operator -from datetime import datetime -from collections import OrderedDict -import numpy as np -import matplotlib.pyplot as plt -import matplotlib.font_manager, matplotlib.ticker, matplotlib.gridspec - -matplotlib.use('Agg') - -from locale_utils import set_locale, print_with_c_locale -from common_func import compute_mechanical_parameters, detect_peaks, get_git_version, parse_log, setup_klipper_import, identify_low_energy_zones - - -PEAKS_DETECTION_THRESHOLD = 0.05 -PEAKS_RELATIVE_HEIGHT_THRESHOLD = 0.04 -VALLEY_DETECTION_THRESHOLD = 0.1 # Lower is more sensitive - -KLIPPAIN_COLORS = { - "purple": "#70088C", - "orange": "#FF8D32", - "dark_purple": "#150140", - "dark_orange": "#F24130", - "red_pink": "#F2055C" -} - - -###################################################################### -# Computation -###################################################################### - -# Call to the official Klipper input shaper object to do the PSD computation -def calc_freq_response(data): - helper = shaper_calibrate.ShaperCalibrate(printer=None) - return helper.process_accelerometer_data(data) - - -def compute_vibration_spectrogram(datas, group, max_freq): - psd_list = [] - first_freqs = None - signal_axes = ['x', 'y', 'z', 'all'] - - for i in range(0, len(datas), group): - # Round up to the nearest power of 2 for faster FFT - N = datas[i].shape[0] - T = datas[i][-1,0] - datas[i][0,0] - M = 1 << int((N/T) * 0.5 - 1).bit_length() - if N <= M: - # If there is not enough lines in the array to be able to round up to the - # nearest power of 2, we need to pad some zeros at the end of the array to - # avoid entering a blocking state from Klipper shaper_calibrate.py - datas[i] = np.pad(datas[i], [(0, (M-N)+1), (0, 0)], mode='constant', constant_values=0) - - freqrsp = calc_freq_response(datas[i]) - for n in range(group - 1): - data = datas[i + n + 1] - - # Round up to the nearest power of 2 for faster FFT - N = data.shape[0] - T = data[-1,0] - data[0,0] - M = 1 << int((N/T) * 0.5 - 1).bit_length() - if N <= M: - # If there is not enough lines in the array to be able to round up to the - # nearest power of 2, we need to pad some zeros at the end of the array to - # avoid entering a blocking state from Klipper shaper_calibrate.py - data = np.pad(data, [(0, (M-N)+1), (0, 0)], mode='constant', constant_values=0) - - freqrsp.add_data(calc_freq_response(data)) - - if not psd_list: - # First group, just put it in the result list - first_freqs = freqrsp.freq_bins - psd = freqrsp.psd_sum[first_freqs <= max_freq] - px = freqrsp.psd_x[first_freqs <= max_freq] - py = freqrsp.psd_y[first_freqs <= max_freq] - pz = freqrsp.psd_z[first_freqs <= max_freq] - psd_list.append([psd, px, py, pz]) - else: - # Not the first group, we need to interpolate every new signals - # to the first one to equalize the frequency_bins between them - signal_normalized = dict() - freqs = freqrsp.freq_bins - for axe in signal_axes: - signal = freqrsp.get_psd(axe) - signal_normalized[axe] = np.interp(first_freqs, freqs, signal) - - # Remove data above max_freq on all axes and add to the result list - psd = signal_normalized['all'][first_freqs <= max_freq] - px = signal_normalized['x'][first_freqs <= max_freq] - py = signal_normalized['y'][first_freqs <= max_freq] - pz = signal_normalized['z'][first_freqs <= max_freq] - psd_list.append([psd, px, py, pz]) - - return np.array(first_freqs[first_freqs <= max_freq]), np.array(psd_list) - - -def compute_speed_profile(speeds, freqs, psd_list): - # Preallocate arrays as psd_list is known and consistent - pwrtot_sum = np.zeros(len(psd_list)) - pwrtot_x = np.zeros(len(psd_list)) - pwrtot_y = np.zeros(len(psd_list)) - pwrtot_z = np.zeros(len(psd_list)) - - for i, psd in enumerate(psd_list): - pwrtot_sum[i] = np.trapz(psd[0], freqs) - pwrtot_x[i] = np.trapz(psd[1], freqs) - pwrtot_y[i] = np.trapz(psd[2], freqs) - pwrtot_z[i] = np.trapz(psd[3], freqs) - - # Resample the signals to get a better detection of the valleys of low energy - # and avoid getting limited by the speed increment defined by the user - resampled_speeds, resampled_power_sum = resample_signal(speeds, pwrtot_sum) - _, resampled_pwrtot_x = resample_signal(speeds, pwrtot_x) - _, resampled_pwrtot_y = resample_signal(speeds, pwrtot_y) - _, resampled_pwrtot_z = resample_signal(speeds, pwrtot_z) - - return resampled_speeds, [resampled_power_sum, resampled_pwrtot_x, resampled_pwrtot_y, resampled_pwrtot_z] - - -def compute_motor_profile(power_spectral_densities): - # Sum the PSD across all speeds for each frequency of the spectrogram. Basically this - # is equivalent to sum up all the spectrogram column by column to plot the total on the right - motor_total_vibration = np.sum([psd[0] for psd in power_spectral_densities], axis=0) - - # Then a very little smoothing of the signal is applied to avoid too much noise and sharp peaks on it and simplify - # the resonance frequency and damping ratio estimation later on. Also, too much smoothing is bad and would alter the results - smoothed_motor_total_vibration = np.convolve(motor_total_vibration, np.ones(10)/10, mode='same') - - return smoothed_motor_total_vibration - - -# Resample the signal to achieve denser data points in order to get more precise valley placing and -# avoid having to use the original sampling of the signal (that is equal to the speed increment used for the test) -def resample_signal(speeds, power_total, new_spacing=0.1): - new_speeds = np.arange(speeds[0], speeds[-1] + new_spacing, new_spacing) - new_power_total = np.interp(new_speeds, speeds, power_total) - return np.array(new_speeds), np.array(new_power_total) - - -###################################################################### -# Graphing -###################################################################### - -def plot_speed_profile(ax, speeds, power_total, num_peaks, peaks, low_energy_zones): - # For this function, we have two array for the speeds. Indeed, since the power total sum was resampled to better detect - # the valleys of low energy later on, we also need the resampled speed array to plot it. For the rest - ax.set_title("Machine speed profile", fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold') - ax.set_xlabel('Speed (mm/s)') - ax.set_ylabel('Energy') - - ax2 = ax.twinx() - ax2.yaxis.set_visible(False) - - max_y = power_total[0].max() + power_total[0].max() * 0.05 - ax.set_xlim([speeds.min(), speeds.max()]) - ax.set_ylim([0, max_y]) - ax2.set_ylim([0, max_y]) - - ax.plot(speeds, power_total[0], label="X+Y+Z", color='purple', zorder=5) - ax.plot(speeds, power_total[1], label="X", color='red') - ax.plot(speeds, power_total[2], label="Y", color='green') - ax.plot(speeds, power_total[3], label="Z", color='blue') - - if peaks.size: - ax.plot(speeds[peaks], power_total[0][peaks], "x", color='black', markersize=8) - for idx, peak in enumerate(peaks): - fontcolor = 'red' - fontweight = 'bold' - ax.annotate(f"{idx+1}", (speeds[peak], power_total[0][peak]), - textcoords="offset points", xytext=(8, 5), - ha='left', fontsize=13, color=fontcolor, weight=fontweight) - ax2.plot([], [], ' ', label=f'Number of peaks: {num_peaks}') - else: - ax2.plot([], [], ' ', label=f'No peaks detected') - - for idx, (start, end, energy) in enumerate(low_energy_zones): - ax.axvline(speeds[start], color='red', linestyle='dotted', linewidth=1.5) - ax.axvline(speeds[end], color='red', linestyle='dotted', linewidth=1.5) - ax2.fill_between(speeds[start:end], 0, power_total[0][start:end], color='green', alpha=0.2, label=f'Zone {idx+1}: {speeds[start]:.1f} to {speeds[end]:.1f} mm/s (mean energy: {energy:.2f}%)') - - ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator()) - ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator()) - ax.grid(which='major', color='grey') - ax.grid(which='minor', color='lightgrey') - fontP = matplotlib.font_manager.FontProperties() - fontP.set_size('small') - ax.legend(loc='upper left', prop=fontP) - ax2.legend(loc='upper right', prop=fontP) - - return - - -def plot_vibration_spectrogram(ax, speeds, freqs, power_spectral_densities, peaks, fr, max_freq): - # Prepare the spectrum data - spectrum = np.empty([len(freqs), len(speeds)]) - for i in range(len(speeds)): - for j in range(len(freqs)): - spectrum[j, i] = power_spectral_densities[i][0][j] - - ax.set_title("Speed vibrations spectrogram", fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold') - # ax.pcolormesh(speeds, freqs, spectrum, norm=matplotlib.colors.LogNorm(), - # cmap='inferno', shading='gouraud') - - ax.imshow(spectrum, norm=matplotlib.colors.LogNorm(), cmap='inferno', - aspect='auto', extent=[speeds[0], speeds[-1], freqs[0], freqs[-1]], - origin='lower', interpolation='antialiased') - - # Add peaks lines in the spectrogram to get hint from peaks found in the first graph - if peaks is not None: - for idx, peak in enumerate(peaks): - ax.axvline(peak, color='cyan', linestyle='dotted', linewidth=0.75) - ax.annotate(f"Peak {idx+1}", (peak, freqs[-1]*0.9), - textcoords="data", color='cyan', rotation=90, fontsize=10, - verticalalignment='top', horizontalalignment='right') - - # Add motor resonance line - if fr is not None and fr > 25: - ax.axhline(fr, color='cyan', linestyle='dotted', linewidth=1) - ax.annotate(f"Motor resonance", (speeds[-1]*0.95, fr+2), - textcoords="data", color='cyan', fontsize=10, - verticalalignment='bottom', horizontalalignment='right') - - ax.set_ylim([0., max_freq]) - ax.set_ylabel('Frequency (hz)') - ax.set_xlabel('Speed (mm/s)') - - return - - -def plot_motor_profile(ax, freqs, motor_vibration_power, motor_fr, motor_zeta, motor_max_power_index): - ax.set_title("Motors frequency profile", fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold') - ax.set_xlabel('Energy') - ax.set_ylabel('Frequency (hz)') - - ax2 = ax.twinx() - ax2.yaxis.set_visible(False) - - ax.set_ylim([freqs.min(), freqs.max()]) - ax.set_xlim([0, motor_vibration_power.max() + motor_vibration_power.max() * 0.1]) - - # Plot the profile curve - ax.plot(motor_vibration_power, freqs, color=KLIPPAIN_COLORS['orange']) - - # Tag the resonance peak - ax.plot(motor_vibration_power[motor_max_power_index], freqs[motor_max_power_index], "x", color='black', markersize=8) - fontcolor = KLIPPAIN_COLORS['purple'] - fontweight = 'bold' - ax.annotate(f"R", (motor_vibration_power[motor_max_power_index], freqs[motor_max_power_index]), - textcoords="offset points", xytext=(8, 8), - ha='right', fontsize=13, color=fontcolor, weight=fontweight) - - # Add the legend - ax2.plot([], [], ' ', label="Motor resonant frequency (ω0): %.1fHz" % (motor_fr)) - ax2.plot([], [], ' ', label="Motor damping ratio (ζ): %.3f" % (motor_zeta)) - - ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator()) - ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator()) - ax.grid(which='major', color='grey') - ax.grid(which='minor', color='lightgrey') - fontP = matplotlib.font_manager.FontProperties() - fontP.set_size('small') - ax2.legend(loc='upper right', prop=fontP) - - return - - -###################################################################### -# Startup and main routines -###################################################################### - -def extract_speed(logname): - try: - speed = re.search('sp(.+?)n', os.path.basename(logname)).group(1).replace('_','.') - except AttributeError: - raise ValueError("File %s does not contain speed in its name and therefore " - "is not supported by this script." % (logname,)) - return float(speed) - - -def sort_and_slice(raw_speeds, raw_datas, remove): - # Sort to get the speeds and their datas aligned and in ascending order - raw_speeds, raw_datas = zip(*sorted(zip(raw_speeds, raw_datas), key=operator.itemgetter(0))) - - # Optionally remove the beginning and end of each data file to get only - # the constant speed part of the segments and remove the start/stop phase - sliced_datas = [] - for data in raw_datas: - sliced = round((len(data) * remove / 100) / 2) - sliced_datas.append(data[sliced:len(data)-sliced]) - - return raw_speeds, sliced_datas - - -def speed_vibrations_profile(lognames, klipperdir="~/klipper", axisname=None, accel=None, max_freq=1000., remove=0): - set_locale() - global shaper_calibrate - shaper_calibrate = setup_klipper_import(klipperdir) - - # Parse the raw data and get them ready for analysis - raw_datas = [parse_log(filename) for filename in lognames] - raw_speeds = [extract_speed(filename) for filename in lognames] - speeds, datas = sort_and_slice(raw_speeds, raw_datas, remove) - del raw_datas, raw_speeds - - # As we assume that we have the same number of file for each speed increment, we can group - # the PSD results by this number (to combine all the segments of the pattern at a constant speed) - group_by = speeds.count(speeds[0]) - - # Remove speeds duplicates and graph the processed datas - speeds = list(OrderedDict((x, True) for x in speeds).keys()) - - # Compute speed profile, vibration spectrogram and motor resonance profile - freqs, psd = compute_vibration_spectrogram(datas, group_by, max_freq) - upsampled_speeds, speeds_powers = compute_speed_profile(speeds, freqs, psd) - motor_vibration_power = compute_motor_profile(psd) - - # Peak detection and low energy valleys (good speeds) identification between the peaks - num_peaks, vibration_peaks, peaks_speeds = detect_peaks( - speeds_powers[0], upsampled_speeds, - PEAKS_DETECTION_THRESHOLD * speeds_powers[0].max(), - PEAKS_RELATIVE_HEIGHT_THRESHOLD, 10, 10 - ) - low_energy_zones = identify_low_energy_zones(speeds_powers[0], VALLEY_DETECTION_THRESHOLD) - - # Print the vibration peaks info in the console - formated_peaks_speeds = ["{:.1f}".format(pspeed) for pspeed in peaks_speeds] - print_with_c_locale("Vibrations peaks detected: %d @ %s mm/s (avoid setting a speed near these values in your slicer print profile)" % (num_peaks, ", ".join(map(str, formated_peaks_speeds)))) - - # Motor resonance estimation - motor_fr, motor_zeta, motor_max_power_index = compute_mechanical_parameters(motor_vibration_power, freqs) - if motor_fr > 25: - print_with_c_locale("Motors have a main resonant frequency at %.1fHz with an estimated damping ratio of %.3f" % (motor_fr, motor_zeta)) - else: - print_with_c_locale("The detected resonance frequency of the motors is too low (%.1fHz). This is probably due to the test run with too high acceleration!" % motor_fr) - print_with_c_locale("Try lowering the ACCEL value before restarting the macro to ensure that only constant speeds are recorded and that the dynamic behavior in the corners is not impacting the measurements.") - - # Create graph layout - fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, gridspec_kw={ - 'height_ratios':[4, 3], - 'width_ratios':[5, 3], - 'bottom':0.050, - 'top':0.890, - 'left':0.057, - 'right':0.985, - 'hspace':0.166, - 'wspace':0.138 - }) - ax2.remove() # top right graph is not used and left blank for now... - fig.set_size_inches(14, 11.6) - - # Add title - title_line1 = "SPEED VIBRATIONS ANALYSIS" - fig.text(0.075, 0.965, title_line1, ha='left', va='bottom', fontsize=20, color=KLIPPAIN_COLORS['purple'], weight='bold') - try: - filename_parts = (lognames[0].split('/')[-1]).split('_') - dt = datetime.strptime(f"{filename_parts[1]} {filename_parts[2].split('-')[0]}", "%Y%m%d %H%M%S") - title_line2 = dt.strftime('%x %X') - if axisname is not None: - title_line2 += ' -- ' + str(axisname).upper() + ' axis' - if accel is not None: - title_line2 += ' at ' + str(accel) + ' mm/s²' - except: - print_with_c_locale("Warning: CSV filename look to be different than expected (%s)" % (lognames[0])) - title_line2 = lognames[0].split('/')[-1] - fig.text(0.075, 0.957, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple']) - - # Plot the graphs - plot_speed_profile(ax1, upsampled_speeds, speeds_powers, num_peaks, vibration_peaks, low_energy_zones) - plot_motor_profile(ax4, freqs, motor_vibration_power, motor_fr, motor_zeta, motor_max_power_index) - plot_vibration_spectrogram(ax3, speeds, freqs, psd, peaks_speeds, motor_fr, max_freq) - - # Adding a small Klippain logo to the top left corner of the figure - 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.axis('off') - - # Adding Shake&Tune version in the top right corner - st_version = get_git_version() - if st_version is not None: - fig.text(0.995, 0.985, st_version, ha='right', va='bottom', fontsize=8, color=KLIPPAIN_COLORS['purple']) - - return fig - - -def main(): - # Parse command-line arguments - usage = "%prog [options] " - opts = optparse.OptionParser(usage) - opts.add_option("-o", "--output", type="string", dest="output", - default=None, help="filename of output graph") - opts.add_option("-a", "--axis", type="string", dest="axisname", - default=None, help="axis name to be printed on the graph") - opts.add_option("-c", "--accel", type="int", dest="accel", - default=None, help="accel value to be printed on the graph") - opts.add_option("-f", "--max_freq", type="float", default=1000., - help="maximum frequency to graph") - opts.add_option("-r", "--remove", type="int", default=0, - help="percentage of data removed at start/end of each CSV files") - opts.add_option("-k", "--klipper_dir", type="string", dest="klipperdir", - default="~/klipper", help="main klipper directory") - options, args = opts.parse_args() - if len(args) < 1: - opts.error("No CSV file(s) to analyse") - if options.output is None: - opts.error("You must specify an output file.png to use the script (option -o)") - if options.remove > 50 or options.remove < 0: - opts.error("You must specify a correct percentage (option -r) in the 0-50 range") - - fig = speed_vibrations_profile(args, options.klipperdir, options.axisname, options.accel, options.max_freq, options.remove) - fig.savefig(options.output, dpi=150) - - -if __name__ == '__main__': - main() diff --git a/K-ShakeTune/scripts/graph_dir_vibrations.py b/K-ShakeTune/scripts/graph_vibrations.py similarity index 96% rename from K-ShakeTune/scripts/graph_dir_vibrations.py rename to K-ShakeTune/scripts/graph_vibrations.py index d6826e4..8c25551 100755 --- a/K-ShakeTune/scripts/graph_dir_vibrations.py +++ b/K-ShakeTune/scripts/graph_vibrations.py @@ -255,7 +255,7 @@ def plot_speed_profile(ax, all_speeds, sp_min_energy, sp_max_energy, sp_avg_ener ax2.plot(all_speeds[peaks], sp_energy_variance[peaks], "x", color='black', markersize=8, zorder=10) for idx, peak in enumerate(peaks): ax2.annotate(f"{idx+1}", (all_speeds[peak], sp_energy_variance[peak]), - textcoords="offset points", xytext=(8, 5), fontweight='bold', + textcoords="offset points", xytext=(5, 5), fontweight='bold', ha='left', fontsize=13, color=KLIPPAIN_COLORS['red_pink'], zorder=10) for idx, (start, end, _) in enumerate(low_energy_zones): @@ -275,7 +275,7 @@ def plot_speed_profile(ax, all_speeds, sp_min_energy, sp_max_energy, sp_avg_ener return -def plot_motor_profiles(ax, freqs, main_angles, motor_profiles, global_motor_profile): +def plot_motor_profiles(ax, freqs, main_angles, motor_profiles, global_motor_profile, max_freq): ax.set_title("Motor frequency profile", fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold') ax.set_ylabel('Energy') ax.set_xlabel('Frequency (Hz)') @@ -291,7 +291,7 @@ def plot_motor_profiles(ax, freqs, main_angles, motor_profiles, global_motor_pro max_value = profile_max ax.plot(freqs, motor_profiles[angle], linestyle='--', label=f'{angle} deg', zorder=2) - ax.set_xlim([0, 400]) + ax.set_xlim([0, max_freq]) ax.set_ylim([0, max_value * 1.1]) # Then add the motor resonance peak to the graph and print some infos about it @@ -302,15 +302,15 @@ def plot_motor_profiles(ax, freqs, main_angles, motor_profiles, global_motor_pro print_with_c_locale("The detected resonance frequency of the motors is too low (%.1fHz). This is probably due to the test run with too high acceleration!" % motor_fr) print_with_c_locale("Try lowering the ACCEL value before restarting the macro to ensure that only constant speeds are recorded and that the dynamic behavior of the machine is not impacting the measurements.") - ax.plot(freqs[motor_res_idx], global_motor_profile[motor_res_idx], "x", color='black', markersize=8) + ax.plot(freqs[motor_res_idx], global_motor_profile[motor_res_idx], "x", color='black', markersize=10) ax.annotate(f"R", (freqs[motor_res_idx], global_motor_profile[motor_res_idx]), - textcoords="offset points", xytext=(10, 5), - ha='right', fontsize=13, color=KLIPPAIN_COLORS['purple'], weight='bold') + textcoords="offset points", xytext=(15, 5), + ha='right', fontsize=14, color=KLIPPAIN_COLORS['red_pink'], weight='bold') - legend_texts = ["Motor resonant frequency (ω0): %.1fHz" % (motor_fr), - "Motor damping ratio (ζ): %.3f" % (motor_zeta)] + legend_texts = ["Resonant frequency (ω0): %.1fHz" % (motor_fr), + "Damping ratio (ζ): %.3f" % (motor_zeta)] for i, text in enumerate(legend_texts): - ax.text(0.90 + i*0.05, 0.98, text, transform=ax.transAxes, color=KLIPPAIN_COLORS['red_pink'], fontsize=12, + ax.text(0.90 + i*0.05, 0.85, text, transform=ax.transAxes, color=KLIPPAIN_COLORS['red_pink'], fontsize=12, fontweight='bold', verticalalignment='top', rotation='vertical', zorder=10) ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator()) @@ -320,7 +320,7 @@ def plot_motor_profiles(ax, freqs, main_angles, motor_profiles, global_motor_pro fontP = matplotlib.font_manager.FontProperties() fontP.set_size('small') - ax.legend(loc='upper left', prop=fontP) + ax.legend(loc='upper right', prop=fontP) return @@ -382,7 +382,7 @@ def extract_angle_and_speed(logname): return float(angle), float(speed) -def dir_vibrations_profile(lognames, klipperdir="~/klipper", kinematics="cartesian", accel=None, max_freq=1000.): +def vibrations_profile(lognames, klipperdir="~/klipper", kinematics="cartesian", accel=None, max_freq=1000.): set_locale() global shaper_calibrate shaper_calibrate = setup_klipper_import(klipperdir) @@ -484,7 +484,7 @@ def dir_vibrations_profile(lognames, klipperdir="~/klipper", kinematics="cartesi if accel is not None: title_line2 += ' at ' + str(accel) + ' mm/s²' except: - print_with_c_locale("Warning: CSV filename look to be different than expected (%s)" % (lognames[0])) + print_with_c_locale("Warning: CSV filenames appear to be different than expected (%s)" % (lognames[0])) title_line2 = lognames[0].split('/')[-1] fig.text(0.060, 0.957, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple']) @@ -492,7 +492,7 @@ def dir_vibrations_profile(lognames, klipperdir="~/klipper", kinematics="cartesi plot_angle_profile_polar(ax3, all_angles, all_angles_energy, good_angles, symmetry_factor) plot_vibration_spectrogram_polar(ax4, all_angles, all_speeds, spectrogram_data) - plot_motor_profiles(ax1, target_freqs, main_angles, motor_profiles, global_motor_profile) + plot_motor_profiles(ax1, target_freqs, main_angles, motor_profiles, global_motor_profile, max_freq) plot_angle_profile(ax6, all_angles, all_angles_energy, good_angles) plot_speed_profile(ax2, all_speeds, sp_min_energy, sp_max_energy, sp_avg_energy, sp_energy_variance, num_peaks, vibration_peaks, good_speeds) @@ -533,7 +533,7 @@ def main(): if options.kinematics not in ["cartesian", "corexy"]: opts.error("Only Cartesian and CoreXY kinematics are supported by this tool at the moment!") - fig = dir_vibrations_profile(args, options.klipperdir, options.kinematics, options.accel, options.max_freq) + fig = vibrations_profile(args, options.klipperdir, options.kinematics, options.accel, options.max_freq) fig.savefig(options.output, dpi=150) diff --git a/K-ShakeTune/scripts/is_workflow.py b/K-ShakeTune/scripts/is_workflow.py index 8c4f865..790a6e9 100755 --- a/K-ShakeTune/scripts/is_workflow.py +++ b/K-ShakeTune/scripts/is_workflow.py @@ -25,10 +25,10 @@ KLIPPER_FOLDER = os.path.expanduser('~/klipper') from graph_belts import belts_calibration from graph_shaper import shaper_calibration -from graph_speed_vibrations import speed_vibrations_profile +from graph_vibrations import vibrations_profile from analyze_axesmap import axesmap_calibration -RESULTS_SUBFOLDERS = ['belts', 'inputshaper', 'speed_vibrations', 'dir_vibrations'] +RESULTS_SUBFOLDERS = ['belts', 'inputshaper', 'vibrations'] def is_file_open(filepath): @@ -132,7 +132,7 @@ def create_shaper_graph(keep_csv, max_smoothing, scv): return axis -def create_speed_vibrations_graph(axis_name, accel, chip_name, keep_csv): +def create_vibrations_graph(accel, kinematics, chip_name, keep_csv): current_date = datetime.now().strftime('%Y%m%d_%H%M%S') lognames = [] @@ -162,15 +162,15 @@ def create_speed_vibrations_graph(axis_name, accel, chip_name, keep_csv): time.sleep(5) # Generate the vibration graph and its name - fig = speed_vibrations_profile(lognames, KLIPPER_FOLDER, axis_name, accel) - png_filename = os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[2], f'vibrations_{current_date}_{axis_name}.png') + fig = vibrations_profile(lognames, KLIPPER_FOLDER, kinematics, accel) + png_filename = os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[2], f'vibrations_{current_date}.png') fig.savefig(png_filename, dpi=150) # Archive all the csv files in a tarball in case the user want to keep them if keep_csv: - with tarfile.open(os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[2], f'vibrations_{current_date}_{axis_name}.tar.gz'), 'w:gz') as tar: + with tarfile.open(os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[2], f'vibrations_{current_date}.tar.gz'), 'w:gz') as tar: for csv_file in lognames: - tar.add(csv_file, recursive=False) + tar.add(csv_file, arcname=os.path.basename(csv_file), recursive=False) # Remove the remaining CSV files not needed anymore (tarball is safe if it was created) for csv_file in lognames: @@ -267,14 +267,19 @@ def main(): help="square corner velocity used to compute max accel for axis shapers graphs") opts.add_option("--max_smoothing", type="float", dest="max_smoothing", default=None, help="maximum shaper smoothing to allow") + opts.add_option("-m", "--kinematics", type="string", dest="kinematics", + default="cartesian", help="machine kinematics configuration used for the vibrations graphs") options, args = opts.parse_args() if options.type is None: opts.error("You must specify the type of output graph you want to produce (option -t)") - elif options.type.lower() is None or options.type.lower() not in ['belts', 'shaper', 'speed_vibrations', 'axesmap', 'clean']: - opts.error("Type of output graph need to be in the list of 'belts', 'shaper', 'speed_vibrations', 'axesmap' or 'clean'") + elif options.type.lower() is None or options.type.lower() not in ['belts', 'shaper', 'vibrations', 'axesmap', 'clean']: + opts.error("Type of output graph need to be in the list of 'belts', 'shaper', 'vibrations', 'axesmap' or 'clean'") else: graph_mode = options.type + + if graph_mode.lower() == "vibrations" and options.kinematics not in ["cartesian", "corexy"]: + opts.error("Only Cartesian and CoreXY kinematics are supported by this tool at the moment!") # Check if results folders are there or create them before doing anything else for result_subfolder in RESULTS_SUBFOLDERS: @@ -288,9 +293,9 @@ def main(): elif graph_mode.lower() == 'shaper': axis = create_shaper_graph(keep_csv=options.keep_csv, max_smoothing=options.max_smoothing, scv=options.scv) print(f"{axis} input shaper graph created. You will find the results in {RESULTS_FOLDER}/{RESULTS_SUBFOLDERS[1]}") - elif graph_mode.lower() == 'speed_vibrations': - create_speed_vibrations_graph(axis_name=options.axis_name, accel=options.accel_used, chip_name=options.chip_name, keep_csv=options.keep_csv) - print(f"{options.axis_name} vibration graph created. You will find the results in {RESULTS_FOLDER}/{RESULTS_SUBFOLDERS[2]}") + elif graph_mode.lower() == 'vibrations': + create_vibrations_graph(accel=options.accel_used, kinematics=options.kinematics, chip_name=options.chip_name, keep_csv=options.keep_csv) + print(f"Vibrations graph created. You will find the results in {RESULTS_FOLDER}/{RESULTS_SUBFOLDERS[2]}") elif graph_mode.lower() == 'axesmap': print(f"WARNING: AXES_MAP_CALIBRATION is currently very experimental and may produce incorrect results... Please validate the output!") find_axesmap(accel=options.accel_used, chip_name=options.chip_name) @@ -298,6 +303,9 @@ def main(): print(f"Cleaning output folder to keep only the last {options.keep_results} results...") clean_files(keep_results=options.keep_results) + if options.keep_csv is False and graph_mode.lower() != 'clean': + print(f"Deleting raw CSV files... If you want to keep them, use the --keep_csv option!") + if __name__ == '__main__': main()