fixed items from code review
This commit is contained in:
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
############################################
|
############################################
|
||||||
###### INPUT SHAPER KLIPPAIN WORKFLOW ######
|
###### INPUT SHAPER KLIPPAIN WORKFLOW ######
|
||||||
############################################
|
############################################
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
from .axes_map_calibration import axes_map_calibration as axes_map_calibration
|
from .axes_map_calibration import axes_map_calibration as axes_map_calibration
|
||||||
from .axes_shaper_calibration import axes_shaper_calibration as axes_shaper_calibration
|
from .axes_shaper_calibration import axes_shaper_calibration as axes_shaper_calibration
|
||||||
from .compare_belts_responses import compare_belts_responses as compare_belts_responses
|
from .compare_belts_responses import compare_belts_responses as compare_belts_responses
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
# This file provides a custom and internal Shake&Tune Accelerometer helper that is
|
# This file provides a custom and internal Shake&Tune Accelerometer helper that is
|
||||||
# an interface to Klipper's own accelerometer classes. It is used to start and
|
# an interface to Klipper's own accelerometer classes. It is used to start and
|
||||||
# stop accelerometer measurements and write the data to a file in a blocking manner.
|
# stop accelerometer measurements and write the data to a file in a blocking manner.
|
||||||
@@ -18,7 +16,7 @@ class Accelerometer:
|
|||||||
def find_axis_accelerometer(printer, axis: str = 'xy'):
|
def find_axis_accelerometer(printer, axis: str = 'xy'):
|
||||||
accel_chip_names = printer.lookup_object('resonance_tester').accel_chip_names
|
accel_chip_names = printer.lookup_object('resonance_tester').accel_chip_names
|
||||||
for chip_axis, chip_name in accel_chip_names:
|
for chip_axis, chip_name in accel_chip_names:
|
||||||
if axis in ['x', 'y'] and chip_axis == 'xy':
|
if axis in {'x', 'y'} and chip_axis == 'xy':
|
||||||
return chip_name
|
return chip_name
|
||||||
elif chip_axis == axis:
|
elif chip_axis == axis:
|
||||||
return chip_name
|
return chip_name
|
||||||
@@ -57,4 +55,4 @@ class Accelerometer:
|
|||||||
f.write('#time,accel_x,accel_y,accel_z\n')
|
f.write('#time,accel_x,accel_y,accel_z\n')
|
||||||
samples = bg_client.samples or bg_client.get_samples()
|
samples = bg_client.samples or bg_client.get_samples()
|
||||||
for t, accel_x, accel_y, accel_z in samples:
|
for t, accel_x, accel_y, accel_z in samples:
|
||||||
f.write('%.6f,%.6f,%.6f,%.6f\n' % (t, accel_x, accel_y, accel_z))
|
f.write(f'{t:.6f},{accel_x:.6f},{accel_y:.6f},{accel_z:.6f}\n')
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
|
|
||||||
from ..helpers.console_output import ConsoleOutput
|
from ..helpers.console_output import ConsoleOutput
|
||||||
from ..shaketune_process import ShakeTuneProcess
|
from ..shaketune_process import ShakeTuneProcess
|
||||||
from .accelerometer import Accelerometer
|
from .accelerometer import Accelerometer
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
|
|
||||||
from ..helpers.common_func import AXIS_CONFIG
|
from ..helpers.common_func import AXIS_CONFIG
|
||||||
from ..helpers.console_output import ConsoleOutput
|
from ..helpers.console_output import ConsoleOutput
|
||||||
from ..helpers.resonance_test import vibrate_axis
|
from ..helpers.resonance_test import vibrate_axis
|
||||||
@@ -14,7 +11,7 @@ def axes_shaper_calibration(gcmd, config, st_process: ShakeTuneProcess) -> None:
|
|||||||
hz_per_sec = gcmd.get_float('HZ_PER_SEC', default=1, minval=1)
|
hz_per_sec = gcmd.get_float('HZ_PER_SEC', default=1, minval=1)
|
||||||
accel_per_hz = gcmd.get_float('ACCEL_PER_HZ', default=None)
|
accel_per_hz = gcmd.get_float('ACCEL_PER_HZ', default=None)
|
||||||
axis_input = gcmd.get('AXIS', default='all').lower()
|
axis_input = gcmd.get('AXIS', default='all').lower()
|
||||||
if axis_input not in ['x', 'y', 'all']:
|
if axis_input not in {'x', 'y', 'all'}:
|
||||||
raise gcmd.error('AXIS selection invalid. Should be either x, y, or all!')
|
raise gcmd.error('AXIS selection invalid. Should be either x, y, or all!')
|
||||||
scv = gcmd.get_float('SCV', default=None, minval=0)
|
scv = gcmd.get_float('SCV', default=None, minval=0)
|
||||||
max_sm = gcmd.get_float('MAX_SMOOTHING', default=None, minval=0)
|
max_sm = gcmd.get_float('MAX_SMOOTHING', default=None, minval=0)
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
|
|
||||||
from ..helpers.common_func import AXIS_CONFIG
|
from ..helpers.common_func import AXIS_CONFIG
|
||||||
from ..helpers.console_output import ConsoleOutput
|
from ..helpers.console_output import ConsoleOutput
|
||||||
from ..helpers.motors_config_parser import MotorsConfigParser
|
from ..helpers.motors_config_parser import MotorsConfigParser
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
|
|
||||||
import math
|
import math
|
||||||
|
|
||||||
from ..helpers.console_output import ConsoleOutput
|
from ..helpers.console_output import ConsoleOutput
|
||||||
@@ -39,7 +36,7 @@ def create_vibrations_profile(gcmd, config, st_process: ShakeTuneProcess) -> Non
|
|||||||
raise gcmd.error('Input shaper is not configured! Please run the shaper calibration macro first.')
|
raise gcmd.error('Input shaper is not configured! Please run the shaper calibration macro first.')
|
||||||
|
|
||||||
motors_config_parser = MotorsConfigParser(config, motors=['stepper_x', 'stepper_y'])
|
motors_config_parser = MotorsConfigParser(config, motors=['stepper_x', 'stepper_y'])
|
||||||
if motors_config_parser.kinematics == 'cartesian' or motors_config_parser.kinematics == 'corexz':
|
if motors_config_parser.kinematics in {'cartesian', 'corexz'}:
|
||||||
main_angles = [0, 90] # Cartesian motors are on X and Y axis directly, same for CoreXZ
|
main_angles = [0, 90] # Cartesian motors are on X and Y axis directly, same for CoreXZ
|
||||||
elif motors_config_parser.kinematics == 'corexy':
|
elif motors_config_parser.kinematics == 'corexy':
|
||||||
main_angles = [45, 135] # CoreXY motors are on A and B axis (45 and 135 degrees)
|
main_angles = [45, 135] # CoreXY motors are on A and B axis (45 and 135 degrees)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
from ..helpers.common_func import AXIS_CONFIG
|
from ..helpers.common_func import AXIS_CONFIG
|
||||||
from ..helpers.console_output import ConsoleOutput
|
from ..helpers.console_output import ConsoleOutput
|
||||||
from ..helpers.resonance_test import vibrate_axis_at_static_freq
|
from ..helpers.resonance_test import vibrate_axis_at_static_freq
|
||||||
@@ -29,7 +27,7 @@ def excitate_axis_at_freq(gcmd, config, st_process: ShakeTuneProcess) -> None:
|
|||||||
if create_graph:
|
if create_graph:
|
||||||
printer = config.get_printer()
|
printer = config.get_printer()
|
||||||
if accel_chip is None:
|
if accel_chip is None:
|
||||||
accel_chip = Accelerometer.find_axis_accelerometer(printer, 'xy' if axis in ['a', 'b'] else axis)
|
accel_chip = Accelerometer.find_axis_accelerometer(printer, 'xy' if axis in {'a', 'b'} else axis)
|
||||||
k_accelerometer = printer.lookup_object(accel_chip, None)
|
k_accelerometer = printer.lookup_object(accel_chip, None)
|
||||||
if k_accelerometer is None:
|
if k_accelerometer is None:
|
||||||
raise gcmd.error(f'Accelerometer chip [{accel_chip}] was not found!')
|
raise gcmd.error(f'Accelerometer chip [{accel_chip}] was not found!')
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
from .axes_map_graph_creator import AxesMapGraphCreator as AxesMapGraphCreator
|
from .axes_map_graph_creator import AxesMapGraphCreator as AxesMapGraphCreator
|
||||||
from .belts_graph_creator import BeltsGraphCreator as BeltsGraphCreator
|
from .belts_graph_creator import BeltsGraphCreator as BeltsGraphCreator
|
||||||
from .graph_creator import GraphCreator as GraphCreator
|
from .graph_creator import GraphCreator as GraphCreator
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
######################################
|
######################################
|
||||||
###### AXE_MAP DETECTION SCRIPT ######
|
###### AXE_MAP DETECTION SCRIPT ######
|
||||||
######################################
|
######################################
|
||||||
@@ -66,7 +64,7 @@ class AxesMapGraphCreator(GraphCreator):
|
|||||||
return # No need to delete any files
|
return # No need to delete any files
|
||||||
for old_file in files[keep_results:]:
|
for old_file in files[keep_results:]:
|
||||||
file_date = '_'.join(old_file.stem.split('_')[1:3])
|
file_date = '_'.join(old_file.stem.split('_')[1:3])
|
||||||
for suffix in ['X', 'Y', 'Z']:
|
for suffix in {'X', 'Y', 'Z'}:
|
||||||
csv_file = self._folder / f'axesmap_{file_date}_{suffix}.csv'
|
csv_file = self._folder / f'axesmap_{file_date}_{suffix}.csv'
|
||||||
csv_file.unlink(missing_ok=True)
|
csv_file.unlink(missing_ok=True)
|
||||||
old_file.unlink()
|
old_file.unlink()
|
||||||
@@ -421,8 +419,7 @@ def axesmap_calibration(
|
|||||||
title_line2 += f' -- at {accel:0.0f} mm/s²'
|
title_line2 += f' -- at {accel:0.0f} mm/s²'
|
||||||
except Exception:
|
except Exception:
|
||||||
ConsoleOutput.print(
|
ConsoleOutput.print(
|
||||||
'Warning: CSV filenames look to be different than expected (%s , %s, %s)'
|
f'Warning: CSV filenames look to be different than expected ({lognames[0]}, {lognames[1]}, {lognames[2]})'
|
||||||
% (lognames[0], lognames[1], lognames[2])
|
|
||||||
)
|
)
|
||||||
title_line2 = lognames[0].split('/')[-1] + ' ...'
|
title_line2 = lognames[0].split('/')[-1] + ' ...'
|
||||||
fig.text(0.060, 0.939, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple'])
|
fig.text(0.060, 0.939, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple'])
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
#################################################
|
#################################################
|
||||||
######## CoreXY BELTS CALIBRATION SCRIPT ########
|
######## CoreXY BELTS CALIBRATION SCRIPT ########
|
||||||
#################################################
|
#################################################
|
||||||
@@ -88,7 +86,7 @@ class BeltsGraphCreator(GraphCreator):
|
|||||||
return # No need to delete any files
|
return # No need to delete any files
|
||||||
for old_file in files[keep_results:]:
|
for old_file in files[keep_results:]:
|
||||||
file_date = '_'.join(old_file.stem.split('_')[1:3])
|
file_date = '_'.join(old_file.stem.split('_')[1:3])
|
||||||
for suffix in ['A', 'B']:
|
for suffix in {'A', 'B'}:
|
||||||
csv_file = self._folder / f'beltscomparison_{file_date}_{suffix}.csv'
|
csv_file = self._folder / f'beltscomparison_{file_date}_{suffix}.csv'
|
||||||
csv_file.unlink(missing_ok=True)
|
csv_file.unlink(missing_ok=True)
|
||||||
old_file.unlink()
|
old_file.unlink()
|
||||||
@@ -192,11 +190,10 @@ def mhi_lut(mhi: float) -> str:
|
|||||||
(0, 15, 'Mechanical issue detected'),
|
(0, 15, 'Mechanical issue detected'),
|
||||||
]
|
]
|
||||||
mhi = np.clip(mhi, 1, 100)
|
mhi = np.clip(mhi, 1, 100)
|
||||||
for lower, upper, message in ranges:
|
return next(
|
||||||
if lower < mhi <= upper:
|
(message for lower, upper, message in ranges if lower < mhi <= upper),
|
||||||
return message
|
'Unknown mechanical health',
|
||||||
|
)
|
||||||
return 'Unknown mechanical health' # Should never happen
|
|
||||||
|
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
@@ -220,9 +217,6 @@ def plot_compare_frequency(
|
|||||||
|
|
||||||
for _, (peak1, peak2) in enumerate(signal1.paired_peaks):
|
for _, (peak1, peak2) in enumerate(signal1.paired_peaks):
|
||||||
label = ALPHABET[paired_peak_count]
|
label = ALPHABET[paired_peak_count]
|
||||||
# amplitude_offset = abs(
|
|
||||||
# ((signal2.psd[peak2[0]] - signal1.psd[peak1[0]]) / max(signal1.psd[peak1[0]], signal2.psd[peak2[0]])) * 100
|
|
||||||
# )
|
|
||||||
amplitude_offset = abs(((signal2.psd[peak2[0]] - signal1.psd[peak1[0]]) / psd_highest_max) * 100)
|
amplitude_offset = abs(((signal2.psd[peak2[0]] - signal1.psd[peak1[0]]) / psd_highest_max) * 100)
|
||||||
frequency_offset = abs(signal2.freqs[peak2[0]] - signal1.freqs[peak1[0]])
|
frequency_offset = abs(signal2.freqs[peak2[0]] - signal1.freqs[peak1[0]])
|
||||||
offsets_table_data.append([f'Peaks {label}', f'{frequency_offset:.1f} Hz', f'{amplitude_offset:.1f} %'])
|
offsets_table_data.append([f'Peaks {label}', f'{frequency_offset:.1f} Hz', f'{amplitude_offset:.1f} %'])
|
||||||
@@ -507,7 +501,7 @@ def belts_calibration(
|
|||||||
|
|
||||||
# Parse data from the log files while ignoring CSV in the wrong format
|
# Parse data from the log files while ignoring CSV in the wrong format
|
||||||
datas = [data for data in (parse_log(fn) for fn in lognames) if data is not None]
|
datas = [data for data in (parse_log(fn) for fn in lognames) if data is not None]
|
||||||
if len(datas) > 2:
|
if len(datas) != 2:
|
||||||
raise ValueError('Incorrect number of .csv files used (this function needs exactly two files to compare them)!')
|
raise ValueError('Incorrect number of .csv files used (this function needs exactly two files to compare them)!')
|
||||||
|
|
||||||
# Get the belts name for the legend to avoid putting the full file name
|
# Get the belts name for the legend to avoid putting the full file name
|
||||||
@@ -569,15 +563,13 @@ def belts_calibration(
|
|||||||
if kinematics is not None:
|
if kinematics is not None:
|
||||||
title_line2 += ' -- ' + kinematics.upper() + ' kinematics'
|
title_line2 += ' -- ' + kinematics.upper() + ' kinematics'
|
||||||
except Exception:
|
except Exception:
|
||||||
ConsoleOutput.print(
|
ConsoleOutput.print(f'Warning: Unable to parse the date from the filename ({lognames[0]}, {lognames[1]})')
|
||||||
'Warning: CSV filenames look to be different than expected (%s , %s)' % (lognames[0], lognames[1])
|
|
||||||
)
|
|
||||||
title_line2 = lognames[0].split('/')[-1] + ' / ' + lognames[1].split('/')[-1]
|
title_line2 = lognames[0].split('/')[-1] + ' / ' + lognames[1].split('/')[-1]
|
||||||
fig.text(0.060, 0.939, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple'])
|
fig.text(0.060, 0.939, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple'])
|
||||||
|
|
||||||
# We add the estimated similarity and the MHI value to the title only if the kinematics is CoreXY
|
# We add the estimated similarity and the MHI value to the title only if the kinematics is CoreXY
|
||||||
# as it make no sense to compute these values for other kinematics that doesn't have paired belts
|
# as it make no sense to compute these values for other kinematics that doesn't have paired belts
|
||||||
if kinematics in ['corexy', 'corexz']:
|
if kinematics in {'corexy', 'corexz'}:
|
||||||
title_line3 = f'| Estimated similarity: {similarity_factor:.1f}%'
|
title_line3 = f'| Estimated similarity: {similarity_factor:.1f}%'
|
||||||
title_line4 = f'| {mhi} (experimental)'
|
title_line4 = f'| {mhi} (experimental)'
|
||||||
fig.text(0.55, 0.985, title_line3, ha='left', va='top', fontsize=14, color=KLIPPAIN_COLORS['dark_purple'])
|
fig.text(0.55, 0.985, title_line3, ha='left', va='top', fontsize=14, color=KLIPPAIN_COLORS['dark_purple'])
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
import shutil
|
import shutil
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
#################################################
|
#################################################
|
||||||
######## INPUT SHAPER CALIBRATION SCRIPT ########
|
######## INPUT SHAPER CALIBRATION SCRIPT ########
|
||||||
#################################################
|
#################################################
|
||||||
@@ -131,8 +129,7 @@ def calibrate_shaper(datas: List[np.ndarray], max_smoothing: Optional[float], sc
|
|||||||
shaper, all_shapers = helper.find_best_shaper(calibration_data, max_smoothing, ConsoleOutput.print)
|
shaper, all_shapers = helper.find_best_shaper(calibration_data, max_smoothing, ConsoleOutput.print)
|
||||||
|
|
||||||
ConsoleOutput.print(
|
ConsoleOutput.print(
|
||||||
'\n-> Recommended shaper is %s @ %.1f Hz (when using a square corner velocity of %.1f and a damping ratio of %.3f)'
|
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})'
|
||||||
% (shaper.name.upper(), shaper.freq, scv, zeta)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return shaper.name, all_shapers, calibration_data, fr, zeta, compat
|
return shaper.name, all_shapers, calibration_data, fr, zeta, compat
|
||||||
@@ -190,13 +187,7 @@ def plot_freq_response(
|
|||||||
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
|
shaper_max_accel = round(shaper.max_accel / 100.0) * 100.0
|
||||||
label = '%s (%.1f Hz, vibr=%.1f%%, sm~=%.2f, accel<=%.f)' % (
|
label = f'{shaper.name.upper()} ({shaper.freq:.1f} Hz, vibr={shaper.vibrs * 100.0:.1f}%, sm~={shaper.smoothing:.2f}, accel<={shaper_max_accel:.0f})'
|
||||||
shaper.name.upper(),
|
|
||||||
shaper.freq,
|
|
||||||
shaper.vibrs * 100.0,
|
|
||||||
shaper.smoothing,
|
|
||||||
shaper_max_accel,
|
|
||||||
)
|
|
||||||
ax2.plot(freqs, shaper.vals, label=label, linestyle='dotted')
|
ax2.plot(freqs, shaper.vals, label=label, linestyle='dotted')
|
||||||
|
|
||||||
# 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)
|
||||||
@@ -226,40 +217,37 @@ def plot_freq_response(
|
|||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
' ',
|
' ',
|
||||||
label='Recommended performance shaper: %s @ %.1f Hz' % (perf_shaper_choice.upper(), perf_shaper_freq),
|
label=f'Recommended performance shaper: {perf_shaper_choice.upper()} @ {perf_shaper_freq:.1f} Hz',
|
||||||
)
|
)
|
||||||
ax.plot(
|
ax.plot(
|
||||||
freqs,
|
freqs,
|
||||||
psd * perf_shaper_vals,
|
psd * perf_shaper_vals,
|
||||||
label='With %s applied' % (perf_shaper_choice.upper()),
|
label=f'With {perf_shaper_choice.upper()} applied',
|
||||||
color='cyan',
|
color='cyan',
|
||||||
)
|
)
|
||||||
ax2.plot(
|
ax2.plot(
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
' ',
|
' ',
|
||||||
label='Recommended low vibrations shaper: %s @ %.1f Hz'
|
label=f'Recommended low vibrations shaper: {klipper_shaper_choice.upper()} @ {klipper_shaper_freq:.1f} Hz',
|
||||||
% (klipper_shaper_choice.upper(), klipper_shaper_freq),
|
|
||||||
)
|
|
||||||
ax.plot(
|
|
||||||
freqs, psd * klipper_shaper_vals, label='With %s applied' % (klipper_shaper_choice.upper()), color='lime'
|
|
||||||
)
|
)
|
||||||
|
ax.plot(freqs, psd * klipper_shaper_vals, label=f'With {klipper_shaper_choice.upper()} applied', color='lime')
|
||||||
else:
|
else:
|
||||||
ax2.plot(
|
ax2.plot(
|
||||||
[],
|
[],
|
||||||
[],
|
[],
|
||||||
' ',
|
' ',
|
||||||
label='Recommended best shaper: %s @ %.1f Hz' % (klipper_shaper_choice.upper(), klipper_shaper_freq),
|
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,
|
||||||
label='With %s applied' % (klipper_shaper_choice.upper()),
|
label=f'With {klipper_shaper_choice.upper()} applied',
|
||||||
color='cyan',
|
color='cyan',
|
||||||
)
|
)
|
||||||
|
|
||||||
# And the estimated damping ratio is finally added at the end of the legend
|
# And the estimated damping ratio is finally added at the end of the legend
|
||||||
ax2.plot([], [], ' ', label='Estimated damping ratio (ζ): %.3f' % (zeta))
|
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")
|
||||||
@@ -288,7 +276,7 @@ def plot_freq_response(
|
|||||||
|
|
||||||
# Add the main resonant frequency and damping ratio of the axis to the graph title
|
# Add the main resonant frequency and damping ratio of the axis to the graph title
|
||||||
ax.set_title(
|
ax.set_title(
|
||||||
'Axis Frequency Profile (ω0=%.1fHz, ζ=%.3f)' % (fr, zeta),
|
f'Axis Frequency Profile (ω0={fr:.1f}Hz, ζ={zeta:.3f})',
|
||||||
fontsize=14,
|
fontsize=14,
|
||||||
color=KLIPPAIN_COLORS['dark_orange'],
|
color=KLIPPAIN_COLORS['dark_orange'],
|
||||||
weight='bold',
|
weight='bold',
|
||||||
@@ -368,6 +356,8 @@ def shaper_calibration(
|
|||||||
|
|
||||||
# Parse data from the log files while ignoring CSV in the wrong format
|
# Parse data from the log files while ignoring CSV in the wrong format
|
||||||
datas = [data for data in (parse_log(fn) for fn in lognames) if data is not None]
|
datas = [data for data in (parse_log(fn) for fn in lognames) if data is not None]
|
||||||
|
if len(datas) == 0:
|
||||||
|
raise ValueError('No valid data found in the provided CSV files!')
|
||||||
if len(datas) > 1:
|
if len(datas) > 1:
|
||||||
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!')
|
||||||
|
|
||||||
@@ -397,8 +387,7 @@ 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(
|
||||||
'\nPeaks detected on the graph: %d @ %s Hz (%d above effect threshold)'
|
f"\nPeaks detected on the graph: {num_peaks} @ {', '.join(map(str, peak_freqs_formated))} Hz ({num_peaks_above_effect_threshold} above effect threshold)"
|
||||||
% (num_peaks, ', '.join(map(str, peak_freqs_formated)), num_peaks_above_effect_threshold)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create graph layout
|
# Create graph layout
|
||||||
@@ -435,7 +424,7 @@ def shaper_calibration(
|
|||||||
title_line4 = f'| Max allowed smoothing: {max_smoothing}'
|
title_line4 = f'| Max allowed smoothing: {max_smoothing}'
|
||||||
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('Warning: CSV filename look to be different than expected (%s)' % (lognames[0]))
|
ConsoleOutput.print(f'Warning: CSV filename look to be different than expected ({lognames[0]})')
|
||||||
title_line2 = lognames[0].split('/')[-1]
|
title_line2 = lognames[0].split('/')[-1]
|
||||||
title_line3 = ''
|
title_line3 = ''
|
||||||
title_line4 = ''
|
title_line4 = ''
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import optparse
|
import optparse
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -137,6 +135,8 @@ def static_frequency_tool(
|
|||||||
raise ValueError('Error: missing frequency or duration parameters!')
|
raise ValueError('Error: missing frequency or duration parameters!')
|
||||||
|
|
||||||
datas = [data for data in (parse_log(fn) for fn in lognames) if data is not None]
|
datas = [data for data in (parse_log(fn) for fn in lognames) if data is not None]
|
||||||
|
if len(datas) == 0:
|
||||||
|
raise ValueError('No valid data found in the provided CSV files!')
|
||||||
if len(datas) > 1:
|
if len(datas) > 1:
|
||||||
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!')
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
##################################################
|
##################################################
|
||||||
#### DIRECTIONAL VIBRATIONS PLOTTING SCRIPT ######
|
#### DIRECTIONAL VIBRATIONS PLOTTING SCRIPT ######
|
||||||
##################################################
|
##################################################
|
||||||
@@ -32,7 +30,7 @@ from ..helpers.common_func import (
|
|||||||
setup_klipper_import,
|
setup_klipper_import,
|
||||||
)
|
)
|
||||||
from ..helpers.console_output import ConsoleOutput
|
from ..helpers.console_output import ConsoleOutput
|
||||||
from ..helpers.motors_config_parser import MotorsConfigParser
|
from ..helpers.motors_config_parser import Motor, MotorsConfigParser
|
||||||
from ..shaketune_config import ShakeTuneConfig
|
from ..shaketune_config import ShakeTuneConfig
|
||||||
from .graph_creator import GraphCreator
|
from .graph_creator import GraphCreator
|
||||||
|
|
||||||
@@ -62,7 +60,7 @@ class VibrationsGraphCreator(GraphCreator):
|
|||||||
def configure(self, kinematics: str, accel: float, motor_config_parser: MotorsConfigParser) -> None:
|
def configure(self, kinematics: str, accel: float, motor_config_parser: MotorsConfigParser) -> None:
|
||||||
self._kinematics = kinematics
|
self._kinematics = kinematics
|
||||||
self._accel = accel
|
self._accel = accel
|
||||||
self._motors = motor_config_parser.get_motors()
|
self._motors: List[Motor] = motor_config_parser.get_motors()
|
||||||
|
|
||||||
def _archive_files(self, lognames: List[Path]) -> None:
|
def _archive_files(self, lognames: List[Path]) -> None:
|
||||||
tar_path = self._folder / f'{self._type}_{self._graph_date}.tar.gz'
|
tar_path = self._folder / f'{self._type}_{self._graph_date}.tar.gz'
|
||||||
@@ -482,7 +480,7 @@ def plot_angular_speed_profiles(
|
|||||||
ax.plot(speeds, spectrogram_data[idx], label=label, color=KLIPPAIN_COLORS[color], zorder=zorder)
|
ax.plot(speeds, spectrogram_data[idx], label=label, color=KLIPPAIN_COLORS[color], zorder=zorder)
|
||||||
|
|
||||||
ax.set_xlim([speeds.min(), speeds.max()])
|
ax.set_xlim([speeds.min(), speeds.max()])
|
||||||
max_value = max(spectrogram_data[angle].max() for angle in [0, 45, 90, 135])
|
max_value = max(spectrogram_data[angle].max() for angle in {0, 45, 90, 135})
|
||||||
ax.set_ylim([0, max_value * 1.1])
|
ax.set_ylim([0, max_value * 1.1])
|
||||||
|
|
||||||
ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||||
@@ -542,13 +540,11 @@ def plot_motor_profiles(
|
|||||||
)
|
)
|
||||||
if motor_zeta is not None:
|
if motor_zeta is not None:
|
||||||
ConsoleOutput.print(
|
ConsoleOutput.print(
|
||||||
'Motors have a main resonant frequency at %.1fHz with an estimated damping ratio of %.3f'
|
f'Motors have a main resonant frequency at {motor_fr:.1f}Hz with an estimated damping ratio of {motor_zeta:.3f}'
|
||||||
% (motor_fr, motor_zeta)
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
ConsoleOutput.print(
|
ConsoleOutput.print(
|
||||||
'Motors have a main resonant frequency at %.1fHz but it was impossible to estimate a damping ratio.'
|
f'Motors have a main resonant frequency at {motor_fr:.1f}Hz 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.plot(freqs[motor_res_idx], global_motor_profile[motor_res_idx], 'x', color='black', markersize=10)
|
||||||
@@ -563,9 +559,9 @@ def plot_motor_profiles(
|
|||||||
weight='bold',
|
weight='bold',
|
||||||
)
|
)
|
||||||
|
|
||||||
ax2.plot([], [], ' ', label='Motor resonant frequency (ω0): %.1fHz' % (motor_fr))
|
ax2.plot([], [], ' ', label=f'Motor resonant frequency (ω0): {motor_fr:.1f}Hz')
|
||||||
if motor_zeta is not None:
|
if motor_zeta is not None:
|
||||||
ax2.plot([], [], ' ', label='Motor damping ratio (ζ): %.3f' % (motor_zeta))
|
ax2.plot([], [], ' ', label=f'Motor damping ratio (ζ): {motor_zeta:.3f}')
|
||||||
else:
|
else:
|
||||||
ax2.plot([], [], ' ', label='No damping ratio computed')
|
ax2.plot([], [], ' ', label='No damping ratio computed')
|
||||||
|
|
||||||
@@ -792,8 +788,7 @@ def vibrations_profile(
|
|||||||
)
|
)
|
||||||
formated_peaks_speeds = ['{:.1f}'.format(pspeed) for pspeed in peaks_speeds]
|
formated_peaks_speeds = ['{:.1f}'.format(pspeed) for pspeed in peaks_speeds]
|
||||||
ConsoleOutput.print(
|
ConsoleOutput.print(
|
||||||
'Vibrations peaks detected: %d @ %s mm/s (avoid setting a speed near these values in your slicer print profile)'
|
f"Vibrations peaks detected: {num_peaks} @ {', '.join(map(str, formated_peaks_speeds))} 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)
|
good_speeds = identify_low_energy_zones(vibration_metric, SPEEDS_VALLEY_DETECTION_THRESHOLD)
|
||||||
@@ -855,7 +850,7 @@ def vibrations_profile(
|
|||||||
if accel is not None:
|
if accel is not None:
|
||||||
title_line2 += ' at ' + str(accel) + ' mm/s² -- ' + kinematics.upper() + ' kinematics'
|
title_line2 += ' at ' + str(accel) + ' mm/s² -- ' + kinematics.upper() + ' kinematics'
|
||||||
except Exception:
|
except Exception:
|
||||||
ConsoleOutput.print('Warning: CSV filenames appear to be different than expected (%s)' % (lognames[0]))
|
ConsoleOutput.print(f'Warning: CSV filenames appear to be different than expected ({lognames[0]})')
|
||||||
title_line2 = lognames[0].split('/')[-1]
|
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'])
|
fig.text(0.060, 0.957, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple'])
|
||||||
|
|
||||||
@@ -923,7 +918,7 @@ def main():
|
|||||||
opts.error('No CSV file(s) to analyse')
|
opts.error('No CSV file(s) to analyse')
|
||||||
if options.output is None:
|
if options.output is None:
|
||||||
opts.error('You must specify an output file.png to use the script (option -o)')
|
opts.error('You must specify an output file.png to use the script (option -o)')
|
||||||
if options.kinematics not in ['cartesian', 'corexy', 'corexz']:
|
if options.kinematics not in {'cartesian', 'corexy', 'corexz'}:
|
||||||
opts.error('Only cartesian, corexy and corexz kinematics are supported by this tool at the moment!')
|
opts.error('Only cartesian, corexy and corexz kinematics are supported by this tool at the moment!')
|
||||||
|
|
||||||
fig = 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)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
# Common functions for the Shake&Tune package
|
# Common functions for the Shake&Tune package
|
||||||
# Written by Frix_x#0161 #
|
# Written by Frix_x#0161 #
|
||||||
|
|
||||||
@@ -35,9 +33,9 @@ def parse_log(logname):
|
|||||||
# Check for a PSD file generated by Klipper and raise a warning
|
# Check for a PSD file generated by Klipper and raise a warning
|
||||||
if cleaned_line.startswith('#freq,psd_x,psd_y,psd_z,psd_xyz'):
|
if cleaned_line.startswith('#freq,psd_x,psd_y,psd_z,psd_xyz'):
|
||||||
ConsoleOutput.print(
|
ConsoleOutput.print(
|
||||||
'Warning: %s does not contain raw accelerometer data. '
|
f'Warning: {logname} does not contain raw accelerometer data. '
|
||||||
'Please use the official Klipper script to process it instead. '
|
'Please use the official Klipper script to process it instead. '
|
||||||
'It will be ignored by Shake&Tune!' % (logname,)
|
'It will be ignored by Shake&Tune!'
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -48,8 +46,8 @@ def parse_log(logname):
|
|||||||
|
|
||||||
if not header:
|
if not header:
|
||||||
ConsoleOutput.print(
|
ConsoleOutput.print(
|
||||||
'Warning: file %s has an incorrect header and will be ignored by Shake&Tune!\n'
|
f'Warning: file {logname} has an incorrect header and will be ignored by Shake&Tune!\n'
|
||||||
"Expected '#time,accel_x,accel_y,accel_z', but got '%s'." % (logname, header.strip())
|
f"Expected '#time,accel_x,accel_y,accel_z', but got '{header.strip()}'."
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -57,8 +55,8 @@ def parse_log(logname):
|
|||||||
data = np.loadtxt(logname, comments='#', delimiter=',', skiprows=1)
|
data = np.loadtxt(logname, comments='#', delimiter=',', skiprows=1)
|
||||||
if data.ndim == 1 or data.shape[1] != 4:
|
if data.ndim == 1 or data.shape[1] != 4:
|
||||||
ConsoleOutput.print(
|
ConsoleOutput.print(
|
||||||
'Warning: %s does not have the correct data format; expected 4 columns. '
|
f'Warning: {logname} does not have the correct data format; expected 4 columns. '
|
||||||
'It will be ignored by Shake&Tune!' % (logname,)
|
'It will be ignored by Shake&Tune!'
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
# Classes to retrieve a couple of motors infos and extract the relevant information
|
# Classes to retrieve a couple of motors infos and extract the relevant information
|
||||||
# from the Klipper configuration and the TMC registers
|
# from the Klipper configuration and the TMC registers
|
||||||
# Written by Frix_x#0161 #
|
# Written by Frix_x#0161 #
|
||||||
@@ -41,7 +39,7 @@ class Motor:
|
|||||||
value_dict = new_value_dict
|
value_dict = new_value_dict
|
||||||
|
|
||||||
# Then gets merged all the thresholds into the same THRS virtual register
|
# Then gets merged all the thresholds into the same THRS virtual register
|
||||||
if register in ['TPWMTHRS', 'TCOOLTHRS']:
|
if register in {'TPWMTHRS', 'TCOOLTHRS'}:
|
||||||
existing_thrs = self._registers.get('THRS', {})
|
existing_thrs = self._registers.get('THRS', {})
|
||||||
merged_values = {**existing_thrs, **value_dict}
|
merged_values = {**existing_thrs, **value_dict}
|
||||||
self._registers['THRS'] = merged_values
|
self._registers['THRS'] = merged_values
|
||||||
@@ -97,10 +95,7 @@ class Motor:
|
|||||||
if not differences['registers']:
|
if not differences['registers']:
|
||||||
del differences['registers']
|
del differences['registers']
|
||||||
|
|
||||||
if not differences:
|
return None if not differences else differences
|
||||||
return None
|
|
||||||
|
|
||||||
return differences
|
|
||||||
|
|
||||||
|
|
||||||
class MotorsConfigParser:
|
class MotorsConfigParser:
|
||||||
@@ -180,10 +175,7 @@ class MotorsConfigParser:
|
|||||||
|
|
||||||
# Find and return the motor by its name
|
# Find and return the motor by its name
|
||||||
def get_motor(self, motor_name: str) -> Optional[Motor]:
|
def get_motor(self, motor_name: str) -> Optional[Motor]:
|
||||||
for motor in self._motors:
|
return next((motor for motor in self._motors if motor.name == motor_name), None)
|
||||||
if motor.name == motor_name:
|
|
||||||
return motor
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Get all the motor list at once
|
# Get all the motor list at once
|
||||||
def get_motors(self) -> List[Motor]:
|
def get_motors(self) -> List[Motor]:
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
# The logic in this file was "extracted" from Klipper's orignal resonance_tester.py file
|
# The logic in this file was "extracted" from Klipper's orignal resonance_tester.py file
|
||||||
# Courtesy of Dmitry Butyugin <dmbutyugin@google.com> for the original implementation
|
# Courtesy of Dmitry Butyugin <dmbutyugin@google.com> for the original implementation
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -84,7 +81,7 @@ class ShakeTune:
|
|||||||
try:
|
try:
|
||||||
dummy_macros_cfg = pconfig.read_config(filename)
|
dummy_macros_cfg = pconfig.read_config(filename)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
raise config.error("Cannot load Shake&Tune dummy macro '%s'" % (filename,)) from err
|
raise config.error(f'Cannot load Shake&Tune dummy macro {filename}') from err
|
||||||
|
|
||||||
for gcode_macro in dummy_macros_cfg.get_prefix_sections('gcode_macro '):
|
for gcode_macro in dummy_macros_cfg.get_prefix_sections('gcode_macro '):
|
||||||
gcode_macro_name = gcode_macro.get_name()
|
gcode_macro_name = gcode_macro.get_name()
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .helpers.console_output import ConsoleOutput
|
from .helpers.console_output import ConsoleOutput
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import os
|
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
|
import os
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|||||||
Reference in New Issue
Block a user