Static freq optional graphs (#112)

This commit is contained in:
Félix Boisselier
2024-06-08 17:02:28 +02:00
committed by GitHub
parent 4384a8339e
commit da51082b44
9 changed files with 322 additions and 13 deletions

View File

@@ -7,12 +7,14 @@
[gcode_macro EXCITATE_AXIS_AT_FREQ] [gcode_macro EXCITATE_AXIS_AT_FREQ]
description: dummy description: dummy
gcode: gcode:
{% set dummy = params.CREATE_GRAPH|default(0) %}
{% set dummy = params.FREQUENCY|default(25) %} {% set dummy = params.FREQUENCY|default(25) %}
{% set dummy = params.DURATION|default(10) %} {% set dummy = params.DURATION|default(30) %}
{% set dummy = params.ACCEL_PER_HZ %} {% set dummy = params.ACCEL_PER_HZ %}
{% set dummy = params.AXIS|default('x') %} {% set dummy = params.AXIS|default('x') %}
{% set dummy = params.TRAVEL_SPEED|default(120) %} {% set dummy = params.TRAVEL_SPEED|default(120) %}
{% set dummy = params.Z_HEIGHT %} {% set dummy = params.Z_HEIGHT %}
{% set dummy = params.ACCEL_CHIP %}
_EXCITATE_AXIS_AT_FREQ {rawparams} _EXCITATE_AXIS_AT_FREQ {rawparams}

View File

@@ -18,7 +18,7 @@ from ..helpers.console_output import ConsoleOutput
# to test the resonance frequency of the printer and its components # to test the resonance frequency of the printer and its components
def vibrate_axis(toolhead, gcode, axis_direction, min_freq, max_freq, hz_per_sec, accel_per_hz): def vibrate_axis(toolhead, gcode, axis_direction, min_freq, max_freq, hz_per_sec, accel_per_hz):
freq = min_freq freq = min_freq
X, Y, Z, E = toolhead.get_position() # Get current position X, Y, Z, E = toolhead.get_position()
sign = 1.0 sign = 1.0
while freq <= max_freq + 0.000001: while freq <= max_freq + 0.000001:
@@ -48,3 +48,33 @@ def vibrate_axis(toolhead, gcode, axis_direction, min_freq, max_freq, hz_per_sec
ConsoleOutput.print(f'Testing frequency: {freq:.0f} Hz') ConsoleOutput.print(f'Testing frequency: {freq:.0f} Hz')
toolhead.wait_moves() toolhead.wait_moves()
# This function is used to vibrate the toolhead in a specific axis direction at a static frequency for a specific duration
def vibrate_axis_at_static_freq(toolhead, gcode, axis_direction, freq, duration, accel_per_hz):
X, Y, Z, E = toolhead.get_position()
sign = 1.0
# Compute movements values
t_seg = 0.25 / freq
accel = accel_per_hz * freq
max_v = accel * t_seg
toolhead.cmd_M204(gcode.create_gcode_command('M204', 'M204', {'S': accel}))
L = 0.5 * accel * t_seg**2
# Calculate move points based on axis direction (X, Y and Z)
magnitude = math.sqrt(sum([component**2 for component in axis_direction]))
normalized_direction = tuple(component / magnitude for component in axis_direction)
dX, dY, dZ = normalized_direction[0] * L, normalized_direction[1] * L, normalized_direction[2] * L
# Start a timer to measure the duration of the test and execute the vibration within the specified time
start_time = toolhead.reactor.monotonic()
while toolhead.reactor.monotonic() - start_time < duration:
nX = X + sign * dX
nY = Y + sign * dY
nZ = Z + sign * dZ
toolhead.move([nX, nY, nZ, E], max_v)
toolhead.move([X, Y, Z, E], max_v)
sign *= -1
toolhead.wait_moves()

View File

@@ -2,17 +2,23 @@
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 .resonance_test import vibrate_axis from ..shaketune_process import ShakeTuneProcess
from .accelerometer import Accelerometer
from .resonance_test import vibrate_axis_at_static_freq
def excitate_axis_at_freq(gcmd, config) -> None: def excitate_axis_at_freq(gcmd, config, st_process: ShakeTuneProcess) -> None:
create_graph = gcmd.get_int('CREATE_GRAPH', default=0, minval=0, maxval=1) == 1
freq = gcmd.get_int('FREQUENCY', default=25, minval=1) freq = gcmd.get_int('FREQUENCY', default=25, minval=1)
duration = gcmd.get_int('DURATION', default=10, minval=1) duration = gcmd.get_int('DURATION', default=30, 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 = gcmd.get('AXIS', default='x').lower() axis = gcmd.get('AXIS', default='x').lower()
feedrate_travel = gcmd.get_float('TRAVEL_SPEED', default=120.0, minval=20.0) feedrate_travel = gcmd.get_float('TRAVEL_SPEED', default=120.0, minval=20.0)
z_height = gcmd.get_float('Z_HEIGHT', default=None, minval=1) z_height = gcmd.get_float('Z_HEIGHT', default=None, minval=1)
accel_chip = gcmd.get('ACCEL_CHIP', default=None)
if accel_chip == '':
accel_chip = None
if accel_per_hz == '': if accel_per_hz == '':
accel_per_hz = None accel_per_hz = None
@@ -20,6 +26,15 @@ def excitate_axis_at_freq(gcmd, config) -> None:
if axis_config is None: if axis_config is None:
raise gcmd.error('AXIS selection invalid. Should be either x, y, a or b!') raise gcmd.error('AXIS selection invalid. Should be either x, y, a or b!')
if create_graph:
printer = config.get_printer()
if accel_chip is None:
accel_chip = Accelerometer.find_axis_accelerometer(printer, 'xy' if axis in ['a', 'b'] else axis)
k_accelerometer = printer.lookup_object(accel_chip, None)
if k_accelerometer is None:
raise gcmd.error(f'Accelerometer chip [{accel_chip}] was not found!')
accelerometer = Accelerometer(k_accelerometer)
ConsoleOutput.print(f'Excitating {axis.upper()} axis at {freq}Hz for {duration} seconds') ConsoleOutput.print(f'Excitating {axis.upper()} axis at {freq}Hz for {duration} seconds')
printer = config.get_printer() printer = config.get_printer()
@@ -55,7 +70,29 @@ def excitate_axis_at_freq(gcmd, config) -> None:
toolhead.manual_move(point, feedrate_travel) toolhead.manual_move(point, feedrate_travel)
toolhead.dwell(0.5) toolhead.dwell(0.5)
min_freq = freq - 1 # Deactivate input shaper if it is active to get raw movements
max_freq = freq + 1 input_shaper = printer.lookup_object('input_shaper', None)
hz_per_sec = 1 / (duration / 3) if input_shaper is not None:
vibrate_axis(toolhead, gcode, axis_config['direction'], min_freq, max_freq, hz_per_sec, accel_per_hz) input_shaper.disable_shaping()
else:
input_shaper = None
# If the user want to create a graph, we start accelerometer recording
if create_graph:
accelerometer.start_measurement()
toolhead.dwell(0.5)
vibrate_axis_at_static_freq(toolhead, gcode, axis_config['direction'], freq, duration, accel_per_hz)
toolhead.dwell(0.5)
# Re-enable the input shaper if it was active
if input_shaper is not None:
input_shaper.enable_shaping()
# If the user wanted to create a graph, we stop the recording and generate it
if create_graph:
accelerometer.stop_measurement(f'staticfreq_{axis.upper()}', append_time=True)
creator = st_process.get_graph_creator()
creator.configure(freq, duration, accel_per_hz)
st_process.run()

View File

@@ -4,4 +4,5 @@ from .graph_creator import AxesMapFinder as AxesMapFinder
from .graph_creator import BeltsGraphCreator as BeltsGraphCreator from .graph_creator import BeltsGraphCreator as BeltsGraphCreator
from .graph_creator import GraphCreator as GraphCreator from .graph_creator import GraphCreator as GraphCreator
from .graph_creator import ShaperGraphCreator as ShaperGraphCreator from .graph_creator import ShaperGraphCreator as ShaperGraphCreator
from .graph_creator import StaticGraphCreator as StaticGraphCreator
from .graph_creator import VibrationsGraphCreator as VibrationsGraphCreator from .graph_creator import VibrationsGraphCreator as VibrationsGraphCreator

View File

@@ -239,7 +239,7 @@ def plot_compare_frequency(ax, signal1, signal2, signal1_belt, signal2_belt, max
ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator()) ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator()) ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
ax.ticklabel_format(axis='y', style='scientific', scilimits=(0, 0)) ax.ticklabel_format(axis='x', style='scientific', scilimits=(0, 0))
ax.grid(which='major', color='grey') ax.grid(which='major', color='grey')
ax.grid(which='minor', color='lightgrey') ax.grid(which='minor', color='lightgrey')
fontP = matplotlib.font_manager.FontProperties() fontP = matplotlib.font_manager.FontProperties()

View File

@@ -15,6 +15,7 @@ from ..shaketune_config import ShakeTuneConfig
from .analyze_axesmap import axesmap_calibration from .analyze_axesmap import axesmap_calibration
from .graph_belts import belts_calibration from .graph_belts import belts_calibration
from .graph_shaper import shaper_calibration from .graph_shaper import shaper_calibration
from .graph_static import static_frequency_tool
from .graph_vibrations import vibrations_profile from .graph_vibrations import vibrations_profile
@@ -275,3 +276,52 @@ class AxesMapFinder(GraphCreator):
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()
class StaticGraphCreator(GraphCreator):
def __init__(self, config: ShakeTuneConfig):
super().__init__(config)
self._freq = None
self._duration = None
self._accel_per_hz = None
self._setup_folder('staticfreq')
def configure(self, freq: float, duration: float, accel_per_hz: float = None) -> None:
self._freq = freq
self._duration = duration
self._accel_per_hz = accel_per_hz
def create_graph(self) -> None:
if not self._freq or not self._duration or not self._accel_per_hz:
raise ValueError('freq, duration and accel_per_hz must be set to create the static frequency graph!')
lognames = self._move_and_prepare_files(
glob_pattern='shaketune-staticfreq_*.csv',
min_files_required=1,
custom_name_func=lambda f: f.stem.split('_')[1].upper(),
)
fig = static_frequency_tool(
lognames=[str(path) for path in lognames],
klipperdir=str(self._config.klipper_folder),
freq=self._freq,
duration=self._duration,
max_freq=200.0,
accel_per_hz=self._accel_per_hz,
st_version=self._version,
)
self._save_figure_and_cleanup(fig, lognames, lognames[0].stem.split('_')[-1])
def clean_old_files(self, keep_results: int = 3) -> None:
# Get all PNG files in the directory as a list of Path objects
files = sorted(self._folder.glob('*.png'), key=lambda f: f.stat().st_mtime, reverse=True)
if len(files) <= keep_results:
return # No need to delete any files
# Delete the older files
for old_file in files[keep_results:]:
csv_file = old_file.with_suffix('.csv')
csv_file.unlink(missing_ok=True)
old_file.unlink()

View File

@@ -0,0 +1,175 @@
#!/usr/bin/env python3
import optparse
import os
from datetime import datetime
import matplotlib
import matplotlib.font_manager
import matplotlib.pyplot as plt
import matplotlib.ticker
import numpy as np
matplotlib.use('Agg')
from ..helpers.common_func import (
compute_spectrogram,
parse_log,
)
from ..helpers.console_output import ConsoleOutput
PEAKS_DETECTION_THRESHOLD = 0.05
PEAKS_EFFECT_THRESHOLD = 0.12
SPECTROGRAM_LOW_PERCENTILE_FILTER = 5
MAX_VIBRATIONS = 5.0
KLIPPAIN_COLORS = {
'purple': '#70088C',
'orange': '#FF8D32',
'dark_purple': '#150140',
'dark_orange': '#F24130',
'red_pink': '#F2055C',
}
######################################################################
# Graphing
######################################################################
def plot_spectrogram(ax, t, bins, pdata, max_freq):
ax.set_title('Time-Frequency Spectrogram', fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
vmin_value = np.percentile(pdata, SPECTROGRAM_LOW_PERCENTILE_FILTER)
cm = 'inferno'
norm = matplotlib.colors.LogNorm(vmin=vmin_value)
ax.imshow(
pdata.T,
norm=norm,
cmap=cm,
aspect='auto',
extent=[t[0], t[-1], bins[0], bins[-1]],
origin='lower',
interpolation='antialiased',
)
ax.set_xlim([0.0, max_freq])
ax.set_ylabel('Time (s)')
ax.set_xlabel('Frequency (Hz)')
return
def plot_energy_accumulation(ax, t, bins, pdata):
# Integrate the energy over the frequency bins for each time step and plot this vertically
ax.plot(np.trapz(pdata, t, axis=0), bins, color=KLIPPAIN_COLORS['orange'])
ax.set_title('Vibrations', fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
ax.set_xlabel('Cumulative Energy')
ax.set_ylabel('Time (s)')
ax.set_ylim([bins[0], bins[-1]])
ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
ax.ticklabel_format(axis='x', style='scientific', scilimits=(0, 0))
ax.grid(which='major', color='grey')
ax.grid(which='minor', color='lightgrey')
# ax.legend()
######################################################################
# Startup and main routines
######################################################################
def static_frequency_tool(
lognames,
klipperdir='~/klipper',
freq=None,
duration=None,
max_freq=500.0,
accel_per_hz=None,
st_version='unknown',
):
if freq is None or duration is None:
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]
if len(datas) > 1:
ConsoleOutput.print('Warning: incorrect number of .csv files detected. Only the first one will be used!')
pdata, bins, t = compute_spectrogram(datas[0])
del datas
fig, ((ax1, ax3)) = plt.subplots(
1,
2,
gridspec_kw={
'width_ratios': [5, 3],
'bottom': 0.080,
'top': 0.840,
'left': 0.050,
'right': 0.985,
'hspace': 0.166,
'wspace': 0.138,
},
)
fig.set_size_inches(15, 7)
title_line1 = 'STATIC FREQUENCY HELPER TOOL'
fig.text(
0.060, 0.947, 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]}', '%Y%m%d %H%M%S')
title_line2 = dt.strftime('%x %X') + ' -- ' + filename_parts[3].upper().split('.')[0] + ' axis'
title_line3 = f'| Maintained frequency: {freq}Hz for {duration}s'
title_line4 = f'| Accel per Hz used: {accel_per_hz} mm/s²/Hz' if accel_per_hz is not None else ''
except Exception:
ConsoleOutput.print('Warning: CSV filename look to be different than expected (%s)' % (lognames[0]))
title_line2 = lognames[0].split('/')[-1]
title_line3 = ''
title_line4 = ''
fig.text(0.060, 0.939, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple'])
fig.text(0.55, 0.985, title_line3, ha='left', va='top', fontsize=14, color=KLIPPAIN_COLORS['dark_purple'])
fig.text(0.55, 0.950, title_line4, ha='left', va='top', fontsize=11, color=KLIPPAIN_COLORS['dark_purple'])
plot_spectrogram(ax1, t, bins, pdata, max_freq)
plot_energy_accumulation(ax3, t, bins, pdata)
ax_logo = fig.add_axes([0.001, 0.894, 0.105, 0.105], anchor='NW')
ax_logo.imshow(plt.imread(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'klippain.png')))
ax_logo.axis('off')
if st_version != 'unknown':
fig.text(0.995, 0.980, st_version, ha='right', va='bottom', fontsize=8, color=KLIPPAIN_COLORS['purple'])
return fig
def main():
usage = '%prog [options] <logs>'
opts = optparse.OptionParser(usage)
opts.add_option('-o', '--output', type='string', dest='output', default=None, help='filename of output graph')
opts.add_option('-f', '--freq', type='float', default=None, help='frequency maintained during the measurement')
opts.add_option('-d', '--duration', type='float', default=None, help='duration of the measurement')
opts.add_option('--max_freq', type='float', default=500.0, help='maximum frequency to graph')
opts.add_option('--accel_per_hz', type='float', default=None, help='accel_per_hz used during the measurement')
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('Incorrect number of arguments')
if options.output is None:
opts.error('You must specify an output file.png to use the script (option -o)')
fig = static_frequency_tool(
args, options.klipperdir, options.freq, options.duration, options.max_freq, options.accel_per_hz, 'unknown'
)
fig.savefig(options.output, dpi=150)
if __name__ == '__main__':
main()

View File

@@ -12,7 +12,13 @@ from .measurement import (
create_vibrations_profile, create_vibrations_profile,
excitate_axis_at_freq, excitate_axis_at_freq,
) )
from .post_processing import AxesMapFinder, BeltsGraphCreator, ShaperGraphCreator, VibrationsGraphCreator from .post_processing import (
AxesMapFinder,
BeltsGraphCreator,
ShaperGraphCreator,
StaticGraphCreator,
VibrationsGraphCreator,
)
from .shaketune_config import ShakeTuneConfig from .shaketune_config import ShakeTuneConfig
from .shaketune_process import ShakeTuneProcess from .shaketune_process import ShakeTuneProcess
@@ -103,7 +109,9 @@ class ShakeTune:
def cmd_EXCITATE_AXIS_AT_FREQ(self, gcmd) -> None: def cmd_EXCITATE_AXIS_AT_FREQ(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}') ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')
excitate_axis_at_freq(gcmd, self._pconfig) static_freq_graph_creator = StaticGraphCreator(self._config)
st_process = ShakeTuneProcess(self._config, static_freq_graph_creator, self.timeout)
excitate_axis_at_freq(gcmd, self._pconfig, st_process)
def cmd_AXES_MAP_CALIBRATION(self, gcmd) -> None: def cmd_AXES_MAP_CALIBRATION(self, gcmd) -> None:
ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}') ConsoleOutput.print(f'Shake&Tune version: {ShakeTuneConfig.get_git_version()}')

View File

@@ -7,7 +7,13 @@ from .helpers.console_output import ConsoleOutput
KLIPPER_FOLDER = Path.home() / 'klipper' KLIPPER_FOLDER = Path.home() / 'klipper'
KLIPPER_LOG_FOLDER = Path.home() / 'printer_data/logs' KLIPPER_LOG_FOLDER = Path.home() / 'printer_data/logs'
RESULTS_BASE_FOLDER = Path.home() / 'printer_data/config/K-ShakeTune_results' RESULTS_BASE_FOLDER = Path.home() / 'printer_data/config/K-ShakeTune_results'
RESULTS_SUBFOLDERS = {'axesmap': 'axesmap', 'belts': 'belts', 'shaper': 'inputshaper', 'vibrations': 'vibrations'} RESULTS_SUBFOLDERS = {
'axesmap': 'axes_map',
'belts': 'belts',
'shaper': 'input_shaper',
'vibrations': 'vibrations',
'staticfreq': 'static_freq',
}
class ShakeTuneConfig: class ShakeTuneConfig: