Ruff and Flake8 code refactoring and linting
This commit is contained in:
@@ -5,40 +5,45 @@
|
||||
##################################################
|
||||
# Written by Frix_x#0161 #
|
||||
|
||||
# Be sure to make this script executable using SSH: type 'chmod +x ./graph_dir_vibrations.py' when in the folder !
|
||||
|
||||
#####################################################################
|
||||
################ !!! DO NOT EDIT BELOW THIS LINE !!! ################
|
||||
#####################################################################
|
||||
|
||||
import math
|
||||
import optparse, matplotlib, re, os
|
||||
from datetime import datetime
|
||||
import optparse
|
||||
import os
|
||||
import re
|
||||
from collections import defaultdict
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.font_manager, matplotlib.ticker, matplotlib.gridspec
|
||||
from datetime import datetime
|
||||
|
||||
import matplotlib
|
||||
import matplotlib.font_manager
|
||||
import matplotlib.gridspec
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.ticker
|
||||
import numpy as np
|
||||
|
||||
matplotlib.use('Agg')
|
||||
|
||||
from locale_utils import set_locale, print_with_c_locale
|
||||
from common_func import get_git_version, parse_log, setup_klipper_import, identify_low_energy_zones, compute_curve_similarity_factor, compute_mechanical_parameters, detect_peaks
|
||||
|
||||
from common_func import (
|
||||
compute_mechanical_parameters,
|
||||
detect_peaks,
|
||||
get_git_version,
|
||||
identify_low_energy_zones,
|
||||
parse_log,
|
||||
setup_klipper_import,
|
||||
)
|
||||
from locale_utils import print_with_c_locale, set_locale
|
||||
|
||||
PEAKS_DETECTION_THRESHOLD = 0.05
|
||||
PEAKS_RELATIVE_HEIGHT_THRESHOLD = 0.04
|
||||
CURVE_SIMILARITY_SIGMOID_K = 0.5
|
||||
SPEEDS_VALLEY_DETECTION_THRESHOLD = 0.7 # Lower is more sensitive
|
||||
SPEEDS_AROUND_PEAK_DELETION = 3 # to delete +-3mm/s around a peak
|
||||
ANGLES_VALLEY_DETECTION_THRESHOLD = 1.1 # Lower is more sensitive
|
||||
SPEEDS_VALLEY_DETECTION_THRESHOLD = 0.7 # Lower is more sensitive
|
||||
SPEEDS_AROUND_PEAK_DELETION = 3 # to delete +-3mm/s around a peak
|
||||
ANGLES_VALLEY_DETECTION_THRESHOLD = 1.1 # Lower is more sensitive
|
||||
|
||||
KLIPPAIN_COLORS = {
|
||||
"purple": "#70088C",
|
||||
"orange": "#FF8D32",
|
||||
"dark_purple": "#150140",
|
||||
"dark_orange": "#F24130",
|
||||
"red_pink": "#F2055C"
|
||||
'purple': '#70088C',
|
||||
'orange': '#FF8D32',
|
||||
'dark_purple': '#150140',
|
||||
'dark_orange': '#F24130',
|
||||
'red_pink': '#F2055C',
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +51,7 @@ KLIPPAIN_COLORS = {
|
||||
# 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)
|
||||
@@ -54,7 +60,10 @@ def calc_freq_response(data):
|
||||
|
||||
# Calculate motor frequency profiles based on the measured Power Spectral Density (PSD) measurements for the machine kinematics
|
||||
# main angles and then create a global motor profile as a weighted average (from their own vibrations) of all calculated profiles
|
||||
def compute_motor_profiles(freqs, psds, all_angles_energy, measured_angles=[0, 90], energy_amplification_factor=2):
|
||||
def compute_motor_profiles(freqs, psds, all_angles_energy, measured_angles=None, energy_amplification_factor=2):
|
||||
if measured_angles is None:
|
||||
measured_angles = [0, 90]
|
||||
|
||||
motor_profiles = {}
|
||||
weighted_sum_profiles = np.zeros_like(freqs)
|
||||
total_weight = 0
|
||||
@@ -67,8 +76,12 @@ def compute_motor_profiles(freqs, psds, all_angles_energy, measured_angles=[0, 9
|
||||
motor_profiles[angle] = np.convolve(sum_curve / len(psds[angle]), conv_filter, mode='same')
|
||||
|
||||
# Calculate weights
|
||||
angle_energy = all_angles_energy[angle] ** energy_amplification_factor # First weighting factor is based on the total vibrations of the machine at the specified angle
|
||||
curve_area = np.trapz(motor_profiles[angle], freqs) ** energy_amplification_factor # Additional weighting factor is based on the area under the current motor profile at this specified angle
|
||||
angle_energy = (
|
||||
all_angles_energy[angle] ** energy_amplification_factor
|
||||
) # First weighting factor is based on the total vibrations of the machine at the specified angle
|
||||
curve_area = (
|
||||
np.trapz(motor_profiles[angle], freqs) ** energy_amplification_factor
|
||||
) # Additional weighting factor is based on the area under the current motor profile at this specified angle
|
||||
total_angle_weight = angle_energy * curve_area
|
||||
|
||||
# Update weighted sum profiles to get the global motor profile
|
||||
@@ -85,20 +98,25 @@ def compute_motor_profiles(freqs, psds, all_angles_energy, measured_angles=[0, 9
|
||||
# the effects of each speeds at each angles, this function simplify it by using only the main motors axes (X/Y for Cartesian
|
||||
# printers and A/B for CoreXY) measurements and project each points on the [0,360] degrees range using trigonometry
|
||||
# to "sum" the vibration impact of each axis at every points of the generated spectrogram. The result is very similar at the end.
|
||||
def compute_dir_speed_spectrogram(measured_speeds, data, kinematics="cartesian", measured_angles=[0, 90]):
|
||||
def compute_dir_speed_spectrogram(measured_speeds, data, kinematics='cartesian', measured_angles=None):
|
||||
if measured_angles is None:
|
||||
measured_angles = [0, 90]
|
||||
|
||||
# We want to project the motor vibrations measured on their own axes on the [0, 360] range
|
||||
spectrum_angles = np.linspace(0, 360, 720) # One point every 0.5 degrees
|
||||
spectrum_angles = np.linspace(0, 360, 720) # One point every 0.5 degrees
|
||||
spectrum_speeds = np.linspace(min(measured_speeds), max(measured_speeds), len(measured_speeds) * 6)
|
||||
spectrum_vibrations = np.zeros((len(spectrum_angles), len(spectrum_speeds)))
|
||||
|
||||
def get_interpolated_vibrations(data, speed, speeds):
|
||||
idx = np.clip(np.searchsorted(speeds, speed, side="left"), 1, len(speeds) - 1)
|
||||
idx = np.clip(np.searchsorted(speeds, speed, side='left'), 1, len(speeds) - 1)
|
||||
lower_speed = speeds[idx - 1]
|
||||
upper_speed = speeds[idx]
|
||||
lower_vibrations = data.get(lower_speed, 0)
|
||||
upper_vibrations = data.get(upper_speed, 0)
|
||||
return lower_vibrations + (speed - lower_speed) * (upper_vibrations - lower_vibrations) / (upper_speed - lower_speed)
|
||||
|
||||
return lower_vibrations + (speed - lower_speed) * (upper_vibrations - lower_vibrations) / (
|
||||
upper_speed - lower_speed
|
||||
)
|
||||
|
||||
# Precompute trigonometric values and constant before the loop
|
||||
angle_radians = np.deg2rad(spectrum_angles)
|
||||
cos_vals = np.cos(angle_radians)
|
||||
@@ -106,12 +124,12 @@ def compute_dir_speed_spectrogram(measured_speeds, data, kinematics="cartesian",
|
||||
sqrt_2_inv = 1 / math.sqrt(2)
|
||||
|
||||
# Compute the spectrum vibrations for each angle and speed combination
|
||||
for target_angle_idx, (cos_val, sin_val) in enumerate(zip(cos_vals, sin_vals)):
|
||||
for target_angle_idx, (cos_val, sin_val) in enumerate(zip(cos_vals, sin_vals, strict=True)):
|
||||
for target_speed_idx, target_speed in enumerate(spectrum_speeds):
|
||||
if kinematics == "cartesian":
|
||||
if kinematics == 'cartesian':
|
||||
speed_1 = np.abs(target_speed * cos_val)
|
||||
speed_2 = np.abs(target_speed * sin_val)
|
||||
elif kinematics == "corexy":
|
||||
elif kinematics == 'corexy':
|
||||
speed_1 = np.abs(target_speed * (cos_val + sin_val) * sqrt_2_inv)
|
||||
speed_2 = np.abs(target_speed * (cos_val - sin_val) * sqrt_2_inv)
|
||||
|
||||
@@ -129,7 +147,7 @@ def compute_angle_powers(spectrogram_data):
|
||||
# the array to start and end of it to smooth transitions when doing the convolution
|
||||
# and get the same value at modulo 360. Then we return the array without the extras
|
||||
extended_angles_powers = np.concatenate([angles_powers[-9:], angles_powers, angles_powers[:9]])
|
||||
convolved_extended = np.convolve(extended_angles_powers, np.ones(15)/15, mode='same')
|
||||
convolved_extended = np.convolve(extended_angles_powers, np.ones(15) / 15, mode='same')
|
||||
|
||||
return convolved_extended[9:-9]
|
||||
|
||||
@@ -145,10 +163,11 @@ def compute_speed_powers(spectrogram_data, smoothing_window=15):
|
||||
# Create a vibration metric that is the product of the max values and the variance to quantify the best
|
||||
# speeds that have at the same time a low global energy level that is also consistent at every angles
|
||||
vibration_metric = max_values * var_values
|
||||
|
||||
|
||||
# utility function to pad and smooth the data avoiding edge effects
|
||||
conv_filter = np.ones(smoothing_window) / smoothing_window
|
||||
window = int(smoothing_window / 2)
|
||||
|
||||
def pad_and_smooth(data):
|
||||
data_padded = np.pad(data, (window,), mode='edge')
|
||||
smoothed_data = np.convolve(data_padded, conv_filter, mode='valid')
|
||||
@@ -163,7 +182,10 @@ def compute_speed_powers(spectrogram_data, smoothing_window=15):
|
||||
|
||||
# This function allow the computation of a symmetry score that reflect the spectrogram apparent symmetry between
|
||||
# measured axes on both the shape of the signal and the energy level consistency across both side of the signal
|
||||
def compute_symmetry_analysis(all_angles, spectrogram_data, measured_angles=[0, 90]):
|
||||
def compute_symmetry_analysis(all_angles, spectrogram_data, measured_angles=None):
|
||||
if measured_angles is None:
|
||||
measured_angles = [0, 90]
|
||||
|
||||
total_spectrogram_angles = len(all_angles)
|
||||
half_spectrogram_angles = total_spectrogram_angles // 2
|
||||
|
||||
@@ -176,8 +198,8 @@ def compute_symmetry_analysis(all_angles, spectrogram_data, measured_angles=[0,
|
||||
half_segment_length = half_spectrogram_angles // 2
|
||||
|
||||
# Slice out the two segments of the spectrogram and flatten them for comparison
|
||||
segment_1_flattened = extended_spectrogram[split_index - half_segment_length:split_index].flatten()
|
||||
segment_2_flattened = extended_spectrogram[split_index:split_index + half_segment_length].flatten()
|
||||
segment_1_flattened = extended_spectrogram[split_index - half_segment_length : split_index].flatten()
|
||||
segment_2_flattened = extended_spectrogram[split_index : split_index + half_segment_length].flatten()
|
||||
|
||||
# Compute the correlation coefficient between the two segments of spectrogram
|
||||
correlation = np.corrcoef(segment_1_flattened, segment_2_flattened)[0, 1]
|
||||
@@ -190,10 +212,11 @@ def compute_symmetry_analysis(all_angles, spectrogram_data, measured_angles=[0,
|
||||
# Graphing
|
||||
######################################################################
|
||||
|
||||
|
||||
def plot_angle_profile_polar(ax, angles, angles_powers, low_energy_zones, symmetry_factor):
|
||||
angles_radians = np.deg2rad(angles)
|
||||
|
||||
ax.set_title("Polar angle energy profile", fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
|
||||
ax.set_title('Polar angle energy profile', fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
|
||||
ax.set_theta_zero_location('E')
|
||||
ax.set_theta_direction(1)
|
||||
|
||||
@@ -202,14 +225,38 @@ def plot_angle_profile_polar(ax, angles, angles_powers, low_energy_zones, symmet
|
||||
ax.set_xlim([0, np.deg2rad(360)])
|
||||
ymax = angles_powers.max() * 1.05
|
||||
ax.set_ylim([0, ymax])
|
||||
ax.set_thetagrids([theta * 15 for theta in range(360//15)])
|
||||
ax.set_thetagrids([theta * 15 for theta in range(360 // 15)])
|
||||
|
||||
ax.text(0, 0, f'Symmetry: {symmetry_factor:.1f}%', ha='center', va='center', color=KLIPPAIN_COLORS['red_pink'], fontsize=12, fontweight='bold', zorder=6)
|
||||
ax.text(
|
||||
0,
|
||||
0,
|
||||
f'Symmetry: {symmetry_factor:.1f}%',
|
||||
ha='center',
|
||||
va='center',
|
||||
color=KLIPPAIN_COLORS['red_pink'],
|
||||
fontsize=12,
|
||||
fontweight='bold',
|
||||
zorder=6,
|
||||
)
|
||||
|
||||
for _, (start, end, _) in enumerate(low_energy_zones):
|
||||
ax.axvline(angles_radians[start], angles_powers[start]/ymax, color=KLIPPAIN_COLORS['red_pink'], linestyle='dotted', linewidth=1.5)
|
||||
ax.axvline(angles_radians[end], angles_powers[end]/ymax, color=KLIPPAIN_COLORS['red_pink'], linestyle='dotted', linewidth=1.5)
|
||||
ax.fill_between(angles_radians[start:end], angles_powers[start:end], angles_powers.max() * 1.05, color='green', alpha=0.2)
|
||||
ax.axvline(
|
||||
angles_radians[start],
|
||||
angles_powers[start] / ymax,
|
||||
color=KLIPPAIN_COLORS['red_pink'],
|
||||
linestyle='dotted',
|
||||
linewidth=1.5,
|
||||
)
|
||||
ax.axvline(
|
||||
angles_radians[end],
|
||||
angles_powers[end] / ymax,
|
||||
color=KLIPPAIN_COLORS['red_pink'],
|
||||
linestyle='dotted',
|
||||
linewidth=1.5,
|
||||
)
|
||||
ax.fill_between(
|
||||
angles_radians[start:end], angles_powers[start:end], angles_powers.max() * 1.05, color='green', alpha=0.2
|
||||
)
|
||||
|
||||
ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||
@@ -223,8 +270,19 @@ def plot_angle_profile_polar(ax, angles, angles_powers, low_energy_zones, symmet
|
||||
|
||||
return
|
||||
|
||||
def plot_global_speed_profile(ax, all_speeds, sp_min_energy, sp_max_energy, sp_variance_energy, vibration_metric, num_peaks, peaks, low_energy_zones):
|
||||
ax.set_title("Global speed energy profile", fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
|
||||
|
||||
def plot_global_speed_profile(
|
||||
ax,
|
||||
all_speeds,
|
||||
sp_min_energy,
|
||||
sp_max_energy,
|
||||
sp_variance_energy,
|
||||
vibration_metric,
|
||||
num_peaks,
|
||||
peaks,
|
||||
low_energy_zones,
|
||||
):
|
||||
ax.set_title('Global speed energy profile', fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
|
||||
ax.set_xlabel('Speed (mm/s)')
|
||||
ax.set_ylabel('Energy')
|
||||
ax2 = ax.twinx()
|
||||
@@ -233,7 +291,13 @@ def plot_global_speed_profile(ax, all_speeds, sp_min_energy, sp_max_energy, sp_v
|
||||
ax.plot(all_speeds, sp_min_energy, label='Minimum', color=KLIPPAIN_COLORS['dark_purple'], zorder=5)
|
||||
ax.plot(all_speeds, sp_max_energy, label='Maximum', color=KLIPPAIN_COLORS['purple'], zorder=5)
|
||||
ax.plot(all_speeds, sp_variance_energy, label='Variance', color=KLIPPAIN_COLORS['orange'], zorder=5, linestyle='--')
|
||||
ax2.plot(all_speeds, vibration_metric, label=f'Vibration metric ({num_peaks} bad peaks)', color=KLIPPAIN_COLORS['red_pink'], zorder=5)
|
||||
ax2.plot(
|
||||
all_speeds,
|
||||
vibration_metric,
|
||||
label=f'Vibration metric ({num_peaks} bad peaks)',
|
||||
color=KLIPPAIN_COLORS['red_pink'],
|
||||
zorder=5,
|
||||
)
|
||||
|
||||
ax.set_xlim([all_speeds.min(), all_speeds.max()])
|
||||
ax.set_ylim([0, sp_max_energy.max() * 1.15])
|
||||
@@ -243,16 +307,31 @@ def plot_global_speed_profile(ax, all_speeds, sp_min_energy, sp_max_energy, sp_v
|
||||
ax2.set_ylim([y2min, y2max])
|
||||
|
||||
if peaks is not None:
|
||||
ax2.plot(all_speeds[peaks], vibration_metric[peaks], "x", color='black', markersize=8, zorder=10)
|
||||
ax2.plot(all_speeds[peaks], vibration_metric[peaks], 'x', color='black', markersize=8, zorder=10)
|
||||
for idx, peak in enumerate(peaks):
|
||||
ax2.annotate(f"{idx+1}", (all_speeds[peak], vibration_metric[peak]),
|
||||
textcoords="offset points", xytext=(5, 5), fontweight='bold',
|
||||
ha='left', fontsize=13, color=KLIPPAIN_COLORS['red_pink'], zorder=10)
|
||||
ax2.annotate(
|
||||
f'{idx+1}',
|
||||
(all_speeds[peak], vibration_metric[peak]),
|
||||
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):
|
||||
# ax2.axvline(all_speeds[start], color=KLIPPAIN_COLORS['red_pink'], linestyle='dotted', linewidth=1.5, zorder=8)
|
||||
# ax2.axvline(all_speeds[end], color=KLIPPAIN_COLORS['red_pink'], linestyle='dotted', linewidth=1.5, zorder=8)
|
||||
ax2.fill_between(all_speeds[start:end], y2min, vibration_metric[start:end], color='green', alpha=0.2, label=f'Zone {idx+1}: {all_speeds[start]:.1f} to {all_speeds[end]:.1f} mm/s')
|
||||
ax2.fill_between(
|
||||
all_speeds[start:end],
|
||||
y2min,
|
||||
vibration_metric[start:end],
|
||||
color='green',
|
||||
alpha=0.2,
|
||||
label=f'Zone {idx+1}: {all_speeds[start]:.1f} to {all_speeds[end]:.1f} mm/s',
|
||||
)
|
||||
|
||||
ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||
@@ -266,8 +345,9 @@ def plot_global_speed_profile(ax, all_speeds, sp_min_energy, sp_max_energy, sp_v
|
||||
|
||||
return
|
||||
|
||||
def plot_angular_speed_profiles(ax, speeds, angles, spectrogram_data, kinematics="cartesian"):
|
||||
ax.set_title("Angular speed energy profiles", fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
|
||||
|
||||
def plot_angular_speed_profiles(ax, speeds, angles, spectrogram_data, kinematics='cartesian'):
|
||||
ax.set_title('Angular speed energy profiles', fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
|
||||
ax.set_xlabel('Speed (mm/s)')
|
||||
ax.set_ylabel('Energy')
|
||||
|
||||
@@ -275,19 +355,19 @@ def plot_angular_speed_profiles(ax, speeds, angles, spectrogram_data, kinematics
|
||||
angle_settings = {
|
||||
0: ('X (0 deg)', 'purple', 10),
|
||||
90: ('Y (90 deg)', 'dark_purple', 5),
|
||||
45: ('A (45 deg)' if kinematics == "corexy" else '45 deg', 'orange', 10),
|
||||
135: ('B (135 deg)' if kinematics == "corexy" else '135 deg', 'dark_orange', 5),
|
||||
45: ('A (45 deg)' if kinematics == 'corexy' else '45 deg', 'orange', 10),
|
||||
135: ('B (135 deg)' if kinematics == 'corexy' else '135 deg', 'dark_orange', 5),
|
||||
}
|
||||
|
||||
# Plot each angle using settings from the dictionary
|
||||
for angle, (label, color, zorder) in angle_settings.items():
|
||||
idx = np.searchsorted(angles, angle, side="left")
|
||||
idx = np.searchsorted(angles, angle, side='left')
|
||||
ax.plot(speeds, spectrogram_data[idx], label=label, color=KLIPPAIN_COLORS[color], zorder=zorder)
|
||||
|
||||
ax.set_xlim([speeds.min(), speeds.max()])
|
||||
max_value = max(spectrogram_data[angle].max() for angle in [0, 45, 90, 135])
|
||||
ax.set_ylim([0, max_value * 1.1])
|
||||
|
||||
|
||||
ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||
ax.grid(which='major', color='grey')
|
||||
@@ -299,8 +379,9 @@ def plot_angular_speed_profiles(ax, speeds, angles, spectrogram_data, kinematics
|
||||
|
||||
return
|
||||
|
||||
|
||||
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_title('Motor frequency profile', fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
|
||||
ax.set_ylabel('Energy')
|
||||
ax.set_xlabel('Frequency (Hz)')
|
||||
|
||||
@@ -308,49 +389,61 @@ def plot_motor_profiles(ax, freqs, main_angles, motor_profiles, global_motor_pro
|
||||
ax2.yaxis.set_visible(False)
|
||||
|
||||
# Global weighted average motor profile
|
||||
ax.plot(freqs, global_motor_profile, label="Combined", color=KLIPPAIN_COLORS['purple'], zorder=5)
|
||||
ax.plot(freqs, global_motor_profile, label='Combined', color=KLIPPAIN_COLORS['purple'], zorder=5)
|
||||
max_value = global_motor_profile.max()
|
||||
|
||||
# Mapping of angles to axis names
|
||||
angle_settings = {
|
||||
0: "X",
|
||||
90: "Y",
|
||||
45: "A",
|
||||
135: "B"
|
||||
}
|
||||
angle_settings = {0: 'X', 90: 'Y', 45: 'A', 135: 'B'}
|
||||
|
||||
# And then plot the motor profiles at each measured angles
|
||||
for angle in main_angles:
|
||||
profile_max = motor_profiles[angle].max()
|
||||
if profile_max > max_value:
|
||||
max_value = profile_max
|
||||
label = f"{angle_settings[angle]} ({angle} deg)" if angle in angle_settings else f"{angle} deg"
|
||||
label = f'{angle_settings[angle]} ({angle} deg)' if angle in angle_settings else f'{angle} deg'
|
||||
ax.plot(freqs, motor_profiles[angle], linestyle='--', label=label, zorder=2)
|
||||
|
||||
ax.set_xlim([0, max_freq])
|
||||
ax.set_ylim([0, max_value * 1.1])
|
||||
ax.ticklabel_format(axis='y', style='scientific', scilimits=(0,0))
|
||||
ax.ticklabel_format(axis='y', style='scientific', scilimits=(0, 0))
|
||||
|
||||
# Then add the motor resonance peak to the graph and print some infos about it
|
||||
motor_fr, motor_zeta, motor_res_idx, lowfreq_max = compute_mechanical_parameters(global_motor_profile, freqs, 30)
|
||||
if lowfreq_max:
|
||||
print_with_c_locale("[WARNING] There are a lot of low frequency vibrations that can alter the readings. This is probably due to the test being performed at too high an acceleration!")
|
||||
print_with_c_locale("Try lowering the ACCEL value and/or increasing the SIZE value before restarting the macro to ensure that only constant speeds are being recorded and that the dynamic behavior of the machine is not affecting the measurements")
|
||||
print_with_c_locale(
|
||||
'[WARNING] There are a lot of low frequency vibrations that can alter the readings. This is probably due to the test being performed at too high an acceleration!'
|
||||
)
|
||||
print_with_c_locale(
|
||||
'Try lowering the ACCEL value and/or increasing the SIZE value before restarting the macro to ensure that only constant speeds are being recorded and that the dynamic behavior of the machine is not affecting the measurements'
|
||||
)
|
||||
if motor_zeta is not None:
|
||||
print_with_c_locale("Motors have a main resonant frequency at %.1fHz with an estimated damping ratio of %.3f" % (motor_fr, motor_zeta))
|
||||
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("Motors have a main resonant frequency at %.1fHz but it was impossible to estimate a damping ratio." % (motor_fr))
|
||||
print_with_c_locale(
|
||||
'Motors have a main resonant frequency at %.1fHz but it was impossible to estimate a damping ratio.'
|
||||
% (motor_fr)
|
||||
)
|
||||
|
||||
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=(15, 5),
|
||||
ha='right', fontsize=14, color=KLIPPAIN_COLORS['red_pink'], weight='bold')
|
||||
ax.plot(freqs[motor_res_idx], global_motor_profile[motor_res_idx], 'x', color='black', markersize=10)
|
||||
ax.annotate(
|
||||
'R',
|
||||
(freqs[motor_res_idx], global_motor_profile[motor_res_idx]),
|
||||
textcoords='offset points',
|
||||
xytext=(15, 5),
|
||||
ha='right',
|
||||
fontsize=14,
|
||||
color=KLIPPAIN_COLORS['red_pink'],
|
||||
weight='bold',
|
||||
)
|
||||
|
||||
ax2.plot([], [], ' ', label="Motor resonant frequency (ω0): %.1fHz" % (motor_fr))
|
||||
ax2.plot([], [], ' ', label='Motor resonant frequency (ω0): %.1fHz' % (motor_fr))
|
||||
if motor_zeta is not None:
|
||||
ax2.plot([], [], ' ', label="Motor damping ratio (ζ): %.3f" % (motor_zeta))
|
||||
ax2.plot([], [], ' ', label='Motor damping ratio (ζ): %.3f' % (motor_zeta))
|
||||
else:
|
||||
ax2.plot([], [], ' ', label="No damping ratio computed")
|
||||
ax2.plot([], [], ' ', label='No damping ratio computed')
|
||||
|
||||
ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||
@@ -364,6 +457,7 @@ def plot_motor_profiles(ax, freqs, main_angles, motor_profiles, global_motor_pro
|
||||
|
||||
return
|
||||
|
||||
|
||||
def plot_vibration_spectrogram_polar(ax, angles, speeds, spectrogram_data):
|
||||
angles_radians = np.radians(angles)
|
||||
|
||||
@@ -371,12 +465,14 @@ def plot_vibration_spectrogram_polar(ax, angles, speeds, spectrogram_data):
|
||||
# for both angles and speeds to map the spectrogram data onto a polar plot correctly
|
||||
radius, theta = np.meshgrid(speeds, angles_radians)
|
||||
|
||||
ax.set_title("Polar vibrations heatmap", fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold', va='bottom')
|
||||
ax.set_theta_zero_location("E")
|
||||
ax.set_title(
|
||||
'Polar vibrations heatmap', fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold', va='bottom'
|
||||
)
|
||||
ax.set_theta_zero_location('E')
|
||||
ax.set_theta_direction(1)
|
||||
|
||||
ax.pcolormesh(theta, radius, spectrogram_data, norm=matplotlib.colors.LogNorm(), cmap='inferno', shading='auto')
|
||||
ax.set_thetagrids([theta * 15 for theta in range(360//15)])
|
||||
ax.set_thetagrids([theta * 15 for theta in range(360 // 15)])
|
||||
ax.tick_params(axis='y', which='both', colors='white', labelsize='medium')
|
||||
ax.set_ylim([0, max(speeds)])
|
||||
|
||||
@@ -387,22 +483,36 @@ def plot_vibration_spectrogram_polar(ax, angles, speeds, spectrogram_data):
|
||||
|
||||
return
|
||||
|
||||
|
||||
def plot_vibration_spectrogram(ax, angles, speeds, spectrogram_data, peaks):
|
||||
ax.set_title("Vibrations heatmap", fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
|
||||
ax.set_title('Vibrations heatmap', fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
|
||||
ax.set_xlabel('Speed (mm/s)')
|
||||
ax.set_ylabel('Angle (deg)')
|
||||
|
||||
ax.imshow(spectrogram_data, norm=matplotlib.colors.LogNorm(), cmap='inferno',
|
||||
aspect='auto', extent=[speeds[0], speeds[-1], angles[0], angles[-1]],
|
||||
origin='lower', interpolation='antialiased')
|
||||
|
||||
ax.imshow(
|
||||
spectrogram_data,
|
||||
norm=matplotlib.colors.LogNorm(),
|
||||
cmap='inferno',
|
||||
aspect='auto',
|
||||
extent=[speeds[0], speeds[-1], angles[0], angles[-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(speeds[peak], color='cyan', linewidth=0.75)
|
||||
ax.annotate(f"Peak {idx+1}", (speeds[peak], angles[-1]*0.9),
|
||||
textcoords="data", color='cyan', rotation=90, fontsize=10,
|
||||
verticalalignment='top', horizontalalignment='right')
|
||||
ax.annotate(
|
||||
f'Peak {idx+1}',
|
||||
(speeds[peak], angles[-1] * 0.9),
|
||||
textcoords='data',
|
||||
color='cyan',
|
||||
rotation=90,
|
||||
fontsize=10,
|
||||
verticalalignment='top',
|
||||
horizontalalignment='right',
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
@@ -411,26 +521,29 @@ def plot_vibration_spectrogram(ax, angles, speeds, spectrogram_data, peaks):
|
||||
# Startup and main routines
|
||||
######################################################################
|
||||
|
||||
|
||||
def extract_angle_and_speed(logname):
|
||||
try:
|
||||
match = re.search(r'an(\d+)_\d+sp(\d+)_\d+', os.path.basename(logname))
|
||||
if match:
|
||||
angle = match.group(1)
|
||||
speed = match.group(2)
|
||||
except AttributeError:
|
||||
raise ValueError(f"File {logname} does not match expected format.")
|
||||
except AttributeError as err:
|
||||
raise ValueError(f'File {logname} does not match expected format.') from err
|
||||
return float(angle), float(speed)
|
||||
|
||||
|
||||
def 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.0):
|
||||
set_locale()
|
||||
global shaper_calibrate
|
||||
shaper_calibrate = setup_klipper_import(klipperdir)
|
||||
|
||||
if kinematics == "cartesian": main_angles = [0, 90]
|
||||
elif kinematics == "corexy": main_angles = [45, 135]
|
||||
if kinematics == 'cartesian':
|
||||
main_angles = [0, 90]
|
||||
elif kinematics == 'corexy':
|
||||
main_angles = [45, 135]
|
||||
else:
|
||||
raise ValueError("Only Cartesian and CoreXY kinematics are supported by this tool at the moment!")
|
||||
raise ValueError('Only Cartesian and CoreXY kinematics are supported by this tool at the moment!')
|
||||
|
||||
psds = defaultdict(lambda: defaultdict(list))
|
||||
psds_sum = defaultdict(lambda: defaultdict(list))
|
||||
@@ -446,7 +559,7 @@ def vibrations_profile(lognames, klipperdir="~/klipper", kinematics="cartesian",
|
||||
if not target_freqs_initialized:
|
||||
target_freqs = first_freqs[first_freqs <= max_freq]
|
||||
target_freqs_initialized = True
|
||||
|
||||
|
||||
psd_sum = psd_sum[first_freqs <= max_freq]
|
||||
first_freqs = first_freqs[first_freqs <= max_freq]
|
||||
|
||||
@@ -459,28 +572,36 @@ def vibrations_profile(lognames, klipperdir="~/klipper", kinematics="cartesian",
|
||||
|
||||
for main_angle in main_angles:
|
||||
if main_angle not in measured_angles:
|
||||
raise ValueError("Measurements not taken at the correct angles for the specified kinematics!")
|
||||
raise ValueError('Measurements not taken at the correct angles for the specified kinematics!')
|
||||
|
||||
# Precompute the variables used in plot functions
|
||||
all_angles, all_speeds, spectrogram_data = compute_dir_speed_spectrogram(measured_speeds, psds_sum, kinematics, main_angles)
|
||||
all_angles, all_speeds, spectrogram_data = compute_dir_speed_spectrogram(
|
||||
measured_speeds, psds_sum, kinematics, main_angles
|
||||
)
|
||||
all_angles_energy = compute_angle_powers(spectrogram_data)
|
||||
sp_min_energy, sp_max_energy, sp_variance_energy, vibration_metric = compute_speed_powers(spectrogram_data)
|
||||
motor_profiles, global_motor_profile = compute_motor_profiles(target_freqs, psds, all_angles_energy, main_angles)
|
||||
|
||||
# symmetry_factor = compute_symmetry_analysis(all_angles, all_angles_energy)
|
||||
symmetry_factor = compute_symmetry_analysis(all_angles, spectrogram_data, main_angles)
|
||||
print_with_c_locale(f"Machine estimated vibration symmetry: {symmetry_factor:.1f}%")
|
||||
print_with_c_locale(f'Machine estimated vibration symmetry: {symmetry_factor:.1f}%')
|
||||
|
||||
# Analyze low variance ranges of vibration energy across all angles for each speed to identify clean speeds
|
||||
# and highlight them. Also find the peaks to identify speeds to avoid due to high resonances
|
||||
num_peaks, vibration_peaks, peaks_speeds = detect_peaks(
|
||||
vibration_metric, all_speeds,
|
||||
vibration_metric,
|
||||
all_speeds,
|
||||
PEAKS_DETECTION_THRESHOLD * vibration_metric.max(),
|
||||
PEAKS_RELATIVE_HEIGHT_THRESHOLD, 10, 10
|
||||
)
|
||||
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))))
|
||||
|
||||
PEAKS_RELATIVE_HEIGHT_THRESHOLD,
|
||||
10,
|
||||
10,
|
||||
)
|
||||
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)))
|
||||
)
|
||||
|
||||
good_speeds = identify_low_energy_zones(vibration_metric, SPEEDS_VALLEY_DETECTION_THRESHOLD)
|
||||
if good_speeds is not None:
|
||||
deletion_range = int(SPEEDS_AROUND_PEAK_DELETION / (all_speeds[1] - all_speeds[0]))
|
||||
@@ -490,10 +611,13 @@ def vibrations_profile(lognames, klipperdir="~/klipper", kinematics="cartesian",
|
||||
for start, end, energy in good_speeds:
|
||||
# Check for peaks within the current good speed range
|
||||
start_speed, end_speed = all_speeds[start], all_speeds[end]
|
||||
intersecting_peaks_indices = [idx for speed, idx in peak_speed_indices.items() if start_speed <= speed <= end_speed]
|
||||
intersecting_peaks_indices = [
|
||||
idx for speed, idx in peak_speed_indices.items() if start_speed <= speed <= end_speed
|
||||
]
|
||||
|
||||
# If no peaks intersect any good_speed range, add it as is, else iterate through intersecting peaks to split the range
|
||||
if not intersecting_peaks_indices: filtered_good_speeds.append((start, end, energy))
|
||||
if not intersecting_peaks_indices:
|
||||
filtered_good_speeds.append((start, end, energy))
|
||||
else:
|
||||
for peak_index in intersecting_peaks_indices:
|
||||
before_peak_end = max(start, peak_index - deletion_range)
|
||||
@@ -505,7 +629,7 @@ def vibrations_profile(lognames, klipperdir="~/klipper", kinematics="cartesian",
|
||||
|
||||
good_speeds = filtered_good_speeds
|
||||
print_with_c_locale(f'Lowest vibrations speeds ({len(good_speeds)} ranges sorted from best to worse):')
|
||||
for idx, (start, end, energy) in enumerate(good_speeds):
|
||||
for idx, (start, end, _) in enumerate(good_speeds):
|
||||
print_with_c_locale(f'{idx+1}: {all_speeds[start]:.1f} to {all_speeds[end]:.1f} mm/s')
|
||||
|
||||
# Angle low energy valleys identification (good angles ranges) and print them to the console
|
||||
@@ -513,19 +637,25 @@ def vibrations_profile(lognames, klipperdir="~/klipper", kinematics="cartesian",
|
||||
if good_angles is not None:
|
||||
print_with_c_locale(f'Lowest vibrations angles ({len(good_angles)} ranges sorted from best to worse):')
|
||||
for idx, (start, end, energy) in enumerate(good_angles):
|
||||
print_with_c_locale(f'{idx+1}: {all_angles[start]:.1f}° to {all_angles[end]:.1f}° (mean vibrations energy: {energy:.2f}% of max)')
|
||||
print_with_c_locale(
|
||||
f'{idx+1}: {all_angles[start]:.1f}° to {all_angles[end]:.1f}° (mean vibrations energy: {energy:.2f}% of max)'
|
||||
)
|
||||
|
||||
# Create graph layout
|
||||
fig, ((ax1, ax2, ax3), (ax4, ax5, ax6)) = plt.subplots(2, 3, gridspec_kw={
|
||||
'height_ratios':[1, 1],
|
||||
'width_ratios':[4, 8, 6],
|
||||
'bottom':0.050,
|
||||
'top':0.890,
|
||||
'left':0.040,
|
||||
'right':0.985,
|
||||
'hspace':0.166,
|
||||
'wspace':0.138
|
||||
})
|
||||
fig, ((ax1, ax2, ax3), (ax4, ax5, ax6)) = plt.subplots(
|
||||
2,
|
||||
3,
|
||||
gridspec_kw={
|
||||
'height_ratios': [1, 1],
|
||||
'width_ratios': [4, 8, 6],
|
||||
'bottom': 0.050,
|
||||
'top': 0.890,
|
||||
'left': 0.040,
|
||||
'right': 0.985,
|
||||
'hspace': 0.166,
|
||||
'wspace': 0.138,
|
||||
},
|
||||
)
|
||||
|
||||
# Transform ax3 and ax4 to polar plots
|
||||
ax1.remove()
|
||||
@@ -537,16 +667,18 @@ def vibrations_profile(lognames, klipperdir="~/klipper", kinematics="cartesian",
|
||||
fig.set_size_inches(20, 11.5)
|
||||
|
||||
# Add title
|
||||
title_line1 = "MACHINE VIBRATIONS ANALYSIS TOOL"
|
||||
fig.text(0.060, 0.965, title_line1, ha='left', va='bottom', fontsize=20, color=KLIPPAIN_COLORS['purple'], weight='bold')
|
||||
title_line1 = 'MACHINE VIBRATIONS ANALYSIS TOOL'
|
||||
fig.text(
|
||||
0.060, 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")
|
||||
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 accel is not None:
|
||||
title_line2 += ' at ' + str(accel) + ' mm/s² -- ' + kinematics.upper() + ' kinematics'
|
||||
except:
|
||||
print_with_c_locale("Warning: CSV filenames appear to be different than expected (%s)" % (lognames[0]))
|
||||
except Exception:
|
||||
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'])
|
||||
|
||||
@@ -554,7 +686,17 @@ def vibrations_profile(lognames, klipperdir="~/klipper", kinematics="cartesian",
|
||||
plot_angle_profile_polar(ax1, all_angles, all_angles_energy, good_angles, symmetry_factor)
|
||||
plot_vibration_spectrogram_polar(ax4, all_angles, all_speeds, spectrogram_data)
|
||||
|
||||
plot_global_speed_profile(ax2, all_speeds, sp_min_energy, sp_max_energy, sp_variance_energy, vibration_metric, num_peaks, vibration_peaks, good_speeds)
|
||||
plot_global_speed_profile(
|
||||
ax2,
|
||||
all_speeds,
|
||||
sp_min_energy,
|
||||
sp_max_energy,
|
||||
sp_variance_energy,
|
||||
vibration_metric,
|
||||
num_peaks,
|
||||
vibration_peaks,
|
||||
good_speeds,
|
||||
)
|
||||
plot_angular_speed_profiles(ax3, all_speeds, all_angles, spectrogram_data, kinematics)
|
||||
plot_vibration_spectrogram(ax5, all_angles, all_speeds, spectrogram_data, vibration_peaks)
|
||||
|
||||
@@ -575,25 +717,31 @@ def vibrations_profile(lognames, klipperdir="~/klipper", kinematics="cartesian",
|
||||
|
||||
def main():
|
||||
# Parse command-line arguments
|
||||
usage = "%prog [options] <raw logs>"
|
||||
usage = '%prog [options] <raw logs>'
|
||||
opts = optparse.OptionParser(usage)
|
||||
opts.add_option("-o", "--output", type="string", dest="output",
|
||||
default=None, help="filename of output 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("-k", "--klipper_dir", type="string", dest="klipperdir",
|
||||
default="~/klipper", help="main klipper directory")
|
||||
opts.add_option("-m", "--kinematics", type="string", dest="kinematics",
|
||||
default="cartesian", help="machine kinematics configuration")
|
||||
opts.add_option('-o', '--output', type='string', dest='output', default=None, help='filename of output 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.0, help='maximum frequency to graph')
|
||||
opts.add_option(
|
||||
'-k', '--klipper_dir', type='string', dest='klipperdir', default='~/klipper', help='main klipper directory'
|
||||
)
|
||||
opts.add_option(
|
||||
'-m',
|
||||
'--kinematics',
|
||||
type='string',
|
||||
dest='kinematics',
|
||||
default='cartesian',
|
||||
help='machine kinematics configuration',
|
||||
)
|
||||
options, args = opts.parse_args()
|
||||
if len(args) < 1:
|
||||
opts.error("No CSV file(s) to analyse")
|
||||
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.kinematics not in ["cartesian", "corexy"]:
|
||||
opts.error("Only cartesian and corexy kinematics are supported by this tool at the moment!")
|
||||
opts.error('You must specify an output file.png to use the script (option -o)')
|
||||
if options.kinematics not in ['cartesian', 'corexy']:
|
||||
opts.error('Only cartesian and corexy kinematics are supported by this tool at the moment!')
|
||||
|
||||
fig = vibrations_profile(args, options.klipperdir, options.kinematics, options.accel, options.max_freq)
|
||||
fig.savefig(options.output, dpi=150)
|
||||
|
||||
Reference in New Issue
Block a user