Merge pull request #91 from Frix-x/refact
refactoring code to OOP and with better linting and formating
This commit is contained in:
1
.git-blame-ignore-revs
Normal file
1
.git-blame-ignore-revs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ef006dbd1e31cc7cae2fae978401a818ee2025d1
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -158,3 +158,6 @@ cython_debug/
|
|||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
|
test/
|
||||||
|
.vscode/
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ gcode:
|
|||||||
{% set scv = params.SCV|default(None) %}
|
{% set scv = params.SCV|default(None) %}
|
||||||
{% set max_sm = params.MAX_SMOOTHING|default(None) %}
|
{% set max_sm = params.MAX_SMOOTHING|default(None) %}
|
||||||
{% set keep_results = params.KEEP_N_RESULTS|default(3)|int %}
|
{% set keep_results = params.KEEP_N_RESULTS|default(3)|int %}
|
||||||
{% set keep_csv = params.KEEP_CSV|default(True) %}
|
{% set keep_csv = params.KEEP_CSV|default(0)|int %}
|
||||||
|
|
||||||
{% set X, Y = False, False %}
|
{% set X, Y = False, False %}
|
||||||
|
|
||||||
@@ -27,17 +27,21 @@ gcode:
|
|||||||
{ action_raise_error("AXIS selection invalid. Should be either all, x or y!") }
|
{ action_raise_error("AXIS selection invalid. Should be either all, x or y!") }
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if scv is none %}
|
{% if scv is none or scv == "" %}
|
||||||
{% set scv = printer.toolhead.square_corner_velocity %}
|
{% set scv = printer.toolhead.square_corner_velocity %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if max_sm == "" %}
|
||||||
|
{% set max_sm = none %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if X %}
|
{% if X %}
|
||||||
TEST_RESONANCES AXIS=X OUTPUT=raw_data NAME=x FREQ_START={min_freq} FREQ_END={max_freq} HZ_PER_SEC={hz_per_sec}
|
TEST_RESONANCES AXIS=X OUTPUT=raw_data NAME=x FREQ_START={min_freq} FREQ_END={max_freq} HZ_PER_SEC={hz_per_sec}
|
||||||
M400
|
M400
|
||||||
|
|
||||||
RESPOND MSG="X axis frequency profile generation..."
|
RESPOND MSG="X axis frequency profile generation..."
|
||||||
RESPOND MSG="This may take some time (1-3min)"
|
RESPOND MSG="This may take some time (1-3min)"
|
||||||
RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type shaper --scv {scv} {% if max_sm is not none %}--max_smoothing {max_sm}{% endif %} {% if keep_csv %}--keep_csv{% endif %}"
|
RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type shaper --scv {scv} {% if max_sm is not none %}--max_smoothing {max_sm}{% endif %} {% if keep_csv %}--keep_csv{% endif %} --keep_results {keep_results}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if Y %}
|
{% if Y %}
|
||||||
@@ -46,8 +50,5 @@ gcode:
|
|||||||
|
|
||||||
RESPOND MSG="Y axis frequency profile generation..."
|
RESPOND MSG="Y axis frequency profile generation..."
|
||||||
RESPOND MSG="This may take some time (1-3min)"
|
RESPOND MSG="This may take some time (1-3min)"
|
||||||
RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type shaper --scv {scv} {% if max_sm is not none %}--max_smoothing {max_sm}{% endif %} {% if keep_csv %}--keep_csv{% endif %}"
|
RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type shaper --scv {scv} {% if max_sm is not none %}--max_smoothing {max_sm}{% endif %} {% if keep_csv %}--keep_csv{% endif %} --keep_results {keep_results}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
M400
|
|
||||||
RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type clean --keep_results {keep_results}"
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ gcode:
|
|||||||
{% set max_freq = params.FREQ_END|default(133.33)|float %}
|
{% set max_freq = params.FREQ_END|default(133.33)|float %}
|
||||||
{% set hz_per_sec = params.HZ_PER_SEC|default(1)|float %}
|
{% set hz_per_sec = params.HZ_PER_SEC|default(1)|float %}
|
||||||
{% set keep_results = params.KEEP_N_RESULTS|default(3)|int %}
|
{% set keep_results = params.KEEP_N_RESULTS|default(3)|int %}
|
||||||
{% set keep_csv = params.KEEP_CSV|default(True) %}
|
{% set keep_csv = params.KEEP_CSV|default(0)|int %}
|
||||||
|
|
||||||
TEST_RESONANCES AXIS=1,1 OUTPUT=raw_data NAME=b FREQ_START={min_freq} FREQ_END={max_freq} HZ_PER_SEC={hz_per_sec}
|
TEST_RESONANCES AXIS=1,1 OUTPUT=raw_data NAME=b FREQ_START={min_freq} FREQ_END={max_freq} HZ_PER_SEC={hz_per_sec}
|
||||||
M400
|
M400
|
||||||
@@ -20,6 +20,4 @@ gcode:
|
|||||||
|
|
||||||
RESPOND MSG="Belts comparative frequency profile generation..."
|
RESPOND MSG="Belts comparative frequency profile generation..."
|
||||||
RESPOND MSG="This may take some time (3-5min)"
|
RESPOND MSG="This may take some time (3-5min)"
|
||||||
RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type belts {% if keep_csv %}--keep_csv{% endif %}"
|
RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type belts {% if keep_csv %}--keep_csv{% endif %} --keep_results {keep_results}"
|
||||||
M400
|
|
||||||
RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type clean --keep_results {keep_results}"
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ gcode:
|
|||||||
{% set accel_chip = params.ACCEL_CHIP|default("adxl345") %} # ADXL chip name in the config
|
{% set accel_chip = params.ACCEL_CHIP|default("adxl345") %} # ADXL chip name in the config
|
||||||
|
|
||||||
{% set keep_results = params.KEEP_N_RESULTS|default(3)|int %}
|
{% set keep_results = params.KEEP_N_RESULTS|default(3)|int %}
|
||||||
{% set keep_csv = params.KEEP_CSV|default(True) %}
|
{% set keep_csv = params.KEEP_CSV|default(0)|int %}
|
||||||
|
|
||||||
{% set mid_x = printer.toolhead.axis_maximum.x|float / 2 %}
|
{% set mid_x = printer.toolhead.axis_maximum.x|float / 2 %}
|
||||||
{% set mid_y = printer.toolhead.axis_maximum.y|float / 2 %}
|
{% set mid_y = printer.toolhead.axis_maximum.y|float / 2 %}
|
||||||
@@ -159,9 +159,7 @@ gcode:
|
|||||||
|
|
||||||
RESPOND MSG="Machine vibrations profile generation..."
|
RESPOND MSG="Machine vibrations profile generation..."
|
||||||
RESPOND MSG="This may take some time (3-5min)"
|
RESPOND MSG="This may take some time (3-5min)"
|
||||||
RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type vibrations --accel {accel|int} --kinematics {kinematics} --chip_name {accel_chip} {% if keep_csv %}--keep_csv{% endif %}"
|
RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type vibrations --accel {accel|int} --kinematics {kinematics} --chip_name {accel_chip} {% if keep_csv %}--keep_csv{% endif %} --keep_results {keep_results}"
|
||||||
M400
|
|
||||||
RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type clean --keep_results {keep_results}"
|
|
||||||
|
|
||||||
# Restore the previous acceleration values
|
# Restore the previous acceleration values
|
||||||
SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_cruise_ratio} SQUARE_CORNER_VELOCITY={old_sqv}
|
SET_VELOCITY_LIMIT ACCEL={old_accel} MINIMUM_CRUISE_RATIO={old_cruise_ratio} SQUARE_CORNER_VELOCITY={old_sqv}
|
||||||
|
|||||||
@@ -1,311 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
############################################
|
|
||||||
###### INPUT SHAPER KLIPPAIN WORKFLOW ######
|
|
||||||
############################################
|
|
||||||
# Written by Frix_x#0161 #
|
|
||||||
|
|
||||||
# This script is designed to be used with gcode_shell_commands directly from Klipper
|
|
||||||
# Use the provided Shake&Tune macros instead!
|
|
||||||
|
|
||||||
|
|
||||||
import optparse
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import glob
|
|
||||||
import sys
|
|
||||||
import shutil
|
|
||||||
import tarfile
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
#################################################################################################################
|
|
||||||
RESULTS_FOLDER = os.path.expanduser('~/printer_data/config/K-ShakeTune_results')
|
|
||||||
KLIPPER_FOLDER = os.path.expanduser('~/klipper')
|
|
||||||
#################################################################################################################
|
|
||||||
|
|
||||||
from graph_belts import belts_calibration
|
|
||||||
from graph_shaper import shaper_calibration
|
|
||||||
from graph_vibrations import vibrations_profile
|
|
||||||
from analyze_axesmap import axesmap_calibration
|
|
||||||
|
|
||||||
RESULTS_SUBFOLDERS = ['belts', 'inputshaper', 'vibrations']
|
|
||||||
|
|
||||||
|
|
||||||
def is_file_open(filepath):
|
|
||||||
for proc in os.listdir('/proc'):
|
|
||||||
if proc.isdigit():
|
|
||||||
for fd in glob.glob(f'/proc/{proc}/fd/*'):
|
|
||||||
try:
|
|
||||||
if os.path.samefile(fd, filepath):
|
|
||||||
return True
|
|
||||||
except FileNotFoundError:
|
|
||||||
# Klipper has already released the CSV file
|
|
||||||
pass
|
|
||||||
except PermissionError:
|
|
||||||
# Unable to check for this particular process due to permissions
|
|
||||||
pass
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def create_belts_graph(keep_csv):
|
|
||||||
current_date = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
||||||
lognames = []
|
|
||||||
|
|
||||||
globbed_files = glob.glob('/tmp/raw_data_axis*.csv')
|
|
||||||
if not globbed_files:
|
|
||||||
print("No CSV files found in the /tmp folder to create the belt graphs!")
|
|
||||||
sys.exit(1)
|
|
||||||
if len(globbed_files) < 2:
|
|
||||||
print("Not enough CSV files found in the /tmp folder. Two files are required for the belt graphs!")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
sorted_files = sorted(globbed_files, key=os.path.getmtime, reverse=True)
|
|
||||||
|
|
||||||
for filename in sorted_files[:2]:
|
|
||||||
# Wait for the file handler to be released by Klipper
|
|
||||||
while is_file_open(filename):
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
# Extract the tested belt from the filename and rename/move the CSV file to the result folder
|
|
||||||
belt = os.path.basename(filename).split('_')[3].split('.')[0].upper()
|
|
||||||
new_file = os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[0], f'belt_{current_date}_{belt}.csv')
|
|
||||||
shutil.move(filename, new_file)
|
|
||||||
os.sync() # Sync filesystem to avoid problems
|
|
||||||
|
|
||||||
# Save the file path for later
|
|
||||||
lognames.append(new_file)
|
|
||||||
|
|
||||||
# Wait for the file handler to be released by the move command
|
|
||||||
while is_file_open(new_file):
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
# Generate the belts graph and its name
|
|
||||||
fig = belts_calibration(lognames, KLIPPER_FOLDER)
|
|
||||||
png_filename = os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[0], f'belts_{current_date}.png')
|
|
||||||
fig.savefig(png_filename, dpi=150)
|
|
||||||
|
|
||||||
# Remove the CSV files if the user don't want to keep them
|
|
||||||
if not keep_csv:
|
|
||||||
for csv in lognames:
|
|
||||||
if os.path.exists(csv):
|
|
||||||
os.remove(csv)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def create_shaper_graph(keep_csv, max_smoothing, scv):
|
|
||||||
current_date = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
||||||
|
|
||||||
# Get all the files and sort them based on last modified time to select the most recent one
|
|
||||||
globbed_files = glob.glob('/tmp/raw_data*.csv')
|
|
||||||
if not globbed_files:
|
|
||||||
print("No CSV files found in the /tmp folder to create the input shaper graphs!")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
sorted_files = sorted(globbed_files, key=os.path.getmtime, reverse=True)
|
|
||||||
filename = sorted_files[0]
|
|
||||||
|
|
||||||
# Wait for the file handler to be released by Klipper
|
|
||||||
while is_file_open(filename):
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
# Extract the tested axis from the filename and rename/move the CSV file to the result folder
|
|
||||||
axis = os.path.basename(filename).split('_')[3].split('.')[0].upper()
|
|
||||||
new_file = os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[1], f'resonances_{current_date}_{axis}.csv')
|
|
||||||
shutil.move(filename, new_file)
|
|
||||||
os.sync() # Sync filesystem to avoid problems
|
|
||||||
|
|
||||||
# Wait for the file handler to be released by the move command
|
|
||||||
while is_file_open(new_file):
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
# Generate the shaper graph and its name
|
|
||||||
fig = shaper_calibration([new_file], KLIPPER_FOLDER, max_smoothing=max_smoothing, scv=scv)
|
|
||||||
png_filename = os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[1], f'resonances_{current_date}_{axis}.png')
|
|
||||||
fig.savefig(png_filename, dpi=150)
|
|
||||||
|
|
||||||
# Remove the CSV file if the user don't want to keep it
|
|
||||||
if not keep_csv:
|
|
||||||
if os.path.exists(new_file):
|
|
||||||
os.remove(new_file)
|
|
||||||
|
|
||||||
return axis
|
|
||||||
|
|
||||||
|
|
||||||
def create_vibrations_graph(accel, kinematics, chip_name, keep_csv):
|
|
||||||
current_date = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
||||||
lognames = []
|
|
||||||
|
|
||||||
globbed_files = glob.glob(f'/tmp/{chip_name}-*.csv')
|
|
||||||
if not globbed_files:
|
|
||||||
print("No CSV files found in the /tmp folder to create the vibration graphs!")
|
|
||||||
sys.exit(1)
|
|
||||||
if len(globbed_files) < 3:
|
|
||||||
print("Not enough CSV files found in the /tmp folder. At least 3 files are required for the vibration graphs!")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
for filename in globbed_files:
|
|
||||||
# Wait for the file handler to be released by Klipper
|
|
||||||
while is_file_open(filename):
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
# Cleanup of the filename and moving it in the result folder
|
|
||||||
cleanfilename = os.path.basename(filename).replace(chip_name, f'vibr_{current_date}')
|
|
||||||
new_file = os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[2], cleanfilename)
|
|
||||||
shutil.move(filename, new_file)
|
|
||||||
|
|
||||||
# Save the file path for later
|
|
||||||
lognames.append(new_file)
|
|
||||||
|
|
||||||
# Sync filesystem to avoid problems as there is a lot of file copied
|
|
||||||
os.sync()
|
|
||||||
time.sleep(5)
|
|
||||||
|
|
||||||
# Generate the vibration graph and its name
|
|
||||||
fig = vibrations_profile(lognames, KLIPPER_FOLDER, kinematics, accel)
|
|
||||||
png_filename = os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[2], f'vibrations_{current_date}.png')
|
|
||||||
fig.savefig(png_filename, dpi=150)
|
|
||||||
|
|
||||||
# Archive all the csv files in a tarball in case the user want to keep them
|
|
||||||
if keep_csv:
|
|
||||||
with tarfile.open(os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[2], f'vibrations_{current_date}.tar.gz'), 'w:gz') as tar:
|
|
||||||
for csv_file in lognames:
|
|
||||||
tar.add(csv_file, arcname=os.path.basename(csv_file), recursive=False)
|
|
||||||
|
|
||||||
# Remove the remaining CSV files not needed anymore (tarball is safe if it was created)
|
|
||||||
for csv_file in lognames:
|
|
||||||
if os.path.exists(csv_file):
|
|
||||||
os.remove(csv_file)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def find_axesmap(accel, chip_name):
|
|
||||||
current_date = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
||||||
result_filename = os.path.join(RESULTS_FOLDER, f'axes_map_{current_date}.txt')
|
|
||||||
lognames = []
|
|
||||||
|
|
||||||
globbed_files = glob.glob(f'/tmp/{chip_name}-*.csv')
|
|
||||||
if not globbed_files:
|
|
||||||
print("No CSV files found in the /tmp folder to analyze and find the axes_map!")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
sorted_files = sorted(globbed_files, key=os.path.getmtime, reverse=True)
|
|
||||||
filename = sorted_files[0]
|
|
||||||
|
|
||||||
# Wait for the file handler to be released by Klipper
|
|
||||||
while is_file_open(filename):
|
|
||||||
time.sleep(2)
|
|
||||||
|
|
||||||
# Analyze the CSV to find the axes_map parameter
|
|
||||||
lognames.append(filename)
|
|
||||||
results = axesmap_calibration(lognames, accel)
|
|
||||||
|
|
||||||
with open(result_filename, 'w') as f:
|
|
||||||
f.write(results)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
# Utility function to get old files based on their modification time
|
|
||||||
def get_old_files(folder, extension, limit):
|
|
||||||
files = [os.path.join(folder, f) for f in os.listdir(folder) if f.endswith(extension)]
|
|
||||||
files.sort(key=lambda x: os.path.getmtime(x), reverse=True)
|
|
||||||
return files[limit:]
|
|
||||||
|
|
||||||
def clean_files(keep_results):
|
|
||||||
# Define limits based on STORE_RESULTS
|
|
||||||
keep1 = keep_results + 1
|
|
||||||
keep2 = 2 * keep_results + 1
|
|
||||||
|
|
||||||
# Find old files in each directory
|
|
||||||
old_belts_files = get_old_files(os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[0]), '.png', keep1)
|
|
||||||
old_inputshaper_files = get_old_files(os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[1]), '.png', keep2)
|
|
||||||
old_speed_vibr_files = get_old_files(os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[2]), '.png', keep1)
|
|
||||||
|
|
||||||
# Remove the old belt files
|
|
||||||
for old_file in old_belts_files:
|
|
||||||
file_date = "_".join(os.path.splitext(os.path.basename(old_file))[0].split('_')[1:3])
|
|
||||||
for suffix in ['A', 'B']:
|
|
||||||
csv_file = os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[0], f'belt_{file_date}_{suffix}.csv')
|
|
||||||
if os.path.exists(csv_file):
|
|
||||||
os.remove(csv_file)
|
|
||||||
os.remove(old_file)
|
|
||||||
|
|
||||||
# Remove the old shaper files
|
|
||||||
for old_file in old_inputshaper_files:
|
|
||||||
csv_file = os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[1], os.path.splitext(os.path.basename(old_file))[0] + ".csv")
|
|
||||||
if os.path.exists(csv_file):
|
|
||||||
os.remove(csv_file)
|
|
||||||
os.remove(old_file)
|
|
||||||
|
|
||||||
# Remove the old vibrations files
|
|
||||||
for old_file in old_speed_vibr_files:
|
|
||||||
os.remove(old_file)
|
|
||||||
tar_file = os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[2], os.path.splitext(os.path.basename(old_file))[0] + ".tar.gz")
|
|
||||||
if os.path.exists(tar_file):
|
|
||||||
os.remove(tar_file)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Parse command-line arguments
|
|
||||||
usage = "%prog [options] <logs>"
|
|
||||||
opts = optparse.OptionParser(usage)
|
|
||||||
opts.add_option("-t", "--type", type="string", dest="type",
|
|
||||||
default=None, help="type of output graph to produce")
|
|
||||||
opts.add_option("--accel", type="int", default=None, dest="accel_used",
|
|
||||||
help="acceleration used during the vibration macro or axesmap macro")
|
|
||||||
opts.add_option("--axis_name", type="string", default=None, dest="axis_name",
|
|
||||||
help="axis tested during the vibration macro")
|
|
||||||
opts.add_option("--chip_name", type="string", default="adxl345", dest="chip_name",
|
|
||||||
help="accelerometer chip name in klipper used during the vibration macro or the axesmap macro")
|
|
||||||
opts.add_option("-n", "--keep_results", type="int", default=3, dest="keep_results",
|
|
||||||
help="number of results to keep in the result folder after each run of the script")
|
|
||||||
opts.add_option("-c", "--keep_csv", action="store_true", default=False, dest="keep_csv",
|
|
||||||
help="weither or not to keep the CSV files alongside the PNG graphs image results")
|
|
||||||
opts.add_option("--scv", "--square_corner_velocity", type="float", dest="scv", default=5.,
|
|
||||||
help="square corner velocity used to compute max accel for axis shapers graphs")
|
|
||||||
opts.add_option("--max_smoothing", type="float", dest="max_smoothing", default=None,
|
|
||||||
help="maximum shaper smoothing to allow")
|
|
||||||
opts.add_option("-m", "--kinematics", type="string", dest="kinematics",
|
|
||||||
default="cartesian", help="machine kinematics configuration used for the vibrations graphs")
|
|
||||||
options, args = opts.parse_args()
|
|
||||||
|
|
||||||
if options.type is None:
|
|
||||||
opts.error("You must specify the type of output graph you want to produce (option -t)")
|
|
||||||
elif options.type.lower() is None or options.type.lower() not in ['belts', 'shaper', 'vibrations', 'axesmap', 'clean']:
|
|
||||||
opts.error("Type of output graph need to be in the list of 'belts', 'shaper', 'vibrations', 'axesmap' or 'clean'")
|
|
||||||
else:
|
|
||||||
graph_mode = options.type
|
|
||||||
|
|
||||||
if graph_mode.lower() == "vibrations" and options.kinematics not in ["cartesian", "corexy"]:
|
|
||||||
opts.error("Only Cartesian and CoreXY kinematics are supported by this tool at the moment!")
|
|
||||||
|
|
||||||
# Check if results folders are there or create them before doing anything else
|
|
||||||
for result_subfolder in RESULTS_SUBFOLDERS:
|
|
||||||
folder = os.path.join(RESULTS_FOLDER, result_subfolder)
|
|
||||||
if not os.path.exists(folder):
|
|
||||||
os.makedirs(folder)
|
|
||||||
|
|
||||||
if graph_mode.lower() == 'belts':
|
|
||||||
create_belts_graph(keep_csv=options.keep_csv)
|
|
||||||
print(f"Belt graph created. You will find the results in {RESULTS_FOLDER}/{RESULTS_SUBFOLDERS[0]}")
|
|
||||||
elif graph_mode.lower() == 'shaper':
|
|
||||||
axis = create_shaper_graph(keep_csv=options.keep_csv, max_smoothing=options.max_smoothing, scv=options.scv)
|
|
||||||
print(f"{axis} input shaper graph created. You will find the results in {RESULTS_FOLDER}/{RESULTS_SUBFOLDERS[1]}")
|
|
||||||
elif graph_mode.lower() == 'vibrations':
|
|
||||||
create_vibrations_graph(accel=options.accel_used, kinematics=options.kinematics, chip_name=options.chip_name, keep_csv=options.keep_csv)
|
|
||||||
print(f"Vibrations graph created. You will find the results in {RESULTS_FOLDER}/{RESULTS_SUBFOLDERS[2]}")
|
|
||||||
elif graph_mode.lower() == 'axesmap':
|
|
||||||
print(f"WARNING: AXES_MAP_CALIBRATION is currently very experimental and may produce incorrect results... Please validate the output!")
|
|
||||||
find_axesmap(accel=options.accel_used, chip_name=options.chip_name)
|
|
||||||
elif graph_mode.lower() == 'clean':
|
|
||||||
print(f"Cleaning output folder to keep only the last {options.keep_results} results...")
|
|
||||||
clean_files(keep_results=options.keep_results)
|
|
||||||
|
|
||||||
if options.keep_csv is False and graph_mode.lower() != 'clean':
|
|
||||||
print(f"Deleting raw CSV files... If you want to keep them, use the --keep_csv option!")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
source ~/klippain_shaketune-env/bin/activate
|
|
||||||
python ~/klippain_shaketune/K-ShakeTune/scripts/is_workflow.py "$@"
|
|
||||||
deactivate
|
|
||||||
10
K-ShakeTune/shaketune.sh
Executable file
10
K-ShakeTune/shaketune.sh
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# This script is used to run the Shake&Tune Python scripts as a module
|
||||||
|
# from the project root directory using its virtual environment
|
||||||
|
# Usage: ./shaketune.sh <args>
|
||||||
|
|
||||||
|
source ~/klippain_shaketune-env/bin/activate
|
||||||
|
cd ~/klippain_shaketune
|
||||||
|
python -m src.is_workflow "$@"
|
||||||
|
deactivate
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
[gcode_shell_command shaketune]
|
[gcode_shell_command shaketune]
|
||||||
command: ~/printer_data/config/K-ShakeTune/scripts/shaketune.sh
|
command: ~/printer_data/config/K-ShakeTune/shaketune.sh
|
||||||
timeout: 600.0
|
timeout: 600.0
|
||||||
verbose: True
|
verbose: True
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Then, call the `AXES_SHAPER_CALIBRATION` macro and look for the graphs in the re
|
|||||||
|SCV|printer square corner velocity|Square corner velocity you want to use to calculate shaper recommendations. Using higher SCV values usually results in more smoothing and lower maximum accelerations|
|
|SCV|printer square corner velocity|Square corner velocity you want to use to calculate shaper recommendations. Using higher SCV values usually results in more smoothing and lower maximum accelerations|
|
||||||
|MAX_SMOOTHING|None|Max smoothing allowed when calculating shaper recommendations|
|
|MAX_SMOOTHING|None|Max smoothing allowed when calculating shaper recommendations|
|
||||||
|KEEP_N_RESULTS|3|Total number of results to keep in the result folder after running the test. The older results are automatically cleaned up|
|
|KEEP_N_RESULTS|3|Total number of results to keep in the result folder after running the test. The older results are automatically cleaned up|
|
||||||
|KEEP_CSV|True|Weither or not to keep the CSV data file alonside the PNG graphs|
|
|KEEP_CSV|0|Weither or not to keep the CSV data file alonside the PNG graphs|
|
||||||
|
|
||||||
|
|
||||||
## Graphs description
|
## Graphs description
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Then, call the `COMPARE_BELTS_RESPONSES` macro and look for the graphs in the re
|
|||||||
|FREQ_END|133|Maximum excitation frequency|
|
|FREQ_END|133|Maximum excitation frequency|
|
||||||
|HZ_PER_SEC|1|Number of Hz per seconds for the test|
|
|HZ_PER_SEC|1|Number of Hz per seconds for the test|
|
||||||
|KEEP_N_RESULTS|3|Total number of results to keep in the result folder after running the test. The older results are automatically cleaned up|
|
|KEEP_N_RESULTS|3|Total number of results to keep in the result folder after running the test. The older results are automatically cleaned up|
|
||||||
|KEEP_CSV|True|Weither or not to keep the CSV data files alonside the PNG graphs|
|
|KEEP_CSV|0|Weither or not to keep the CSV data files alonside the PNG graphs|
|
||||||
|
|
||||||
|
|
||||||
## Graphs description
|
## Graphs description
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ Call the `CREATE_VIBRATIONS_PROFILE` macro with the speed range you want to meas
|
|||||||
|TRAVEL_SPEED|200|speed in mm/s used for all the travels moves|
|
|TRAVEL_SPEED|200|speed in mm/s used for all the travels moves|
|
||||||
|ACCEL_CHIP|"adxl345"|accelerometer chip name in the config|
|
|ACCEL_CHIP|"adxl345"|accelerometer chip name in the config|
|
||||||
|KEEP_N_RESULTS|3|Total number of results to keep in the result folder after running the test. The older results are automatically cleaned up|
|
|KEEP_N_RESULTS|3|Total number of results to keep in the result folder after running the test. The older results are automatically cleaned up|
|
||||||
|KEEP_CSV|True|Weither or not to keep the CSV data files alonside the PNG graphs (archived in a tarball)|
|
|KEEP_CSV|0|Weither or not to keep the CSV data files alonside the PNG graphs (archived in a tarball)|
|
||||||
|
|
||||||
|
|
||||||
## Graphs description
|
## Graphs description
|
||||||
|
|||||||
29
pyproject.toml
Normal file
29
pyproject.toml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
[project]
|
||||||
|
name = "Shake&Tune"
|
||||||
|
description = "Klipper streamlined input shaper workflow and calibration tools"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">= 3.9"
|
||||||
|
authors = [
|
||||||
|
{name = "Félix Boisselier", email = "felix@fboisselier.fr"}
|
||||||
|
]
|
||||||
|
keywords = ["klipper", "input shaper", "calibration", "3d printer"]
|
||||||
|
license = {file = "LICENSE"}
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Repository = "https://github.com/Frix-x/klippain-shaketune"
|
||||||
|
Documentation = "https://github.com/Frix-x/klippain-shaketune/tree/main/docs"
|
||||||
|
Issues = "https://github.com/Frix-x/klippain-shaketune/issues"
|
||||||
|
Changelog = "https://github.com/Frix-x/klippain-shaketune/releases"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 120 # We all have modern screens now and I believe this should be brought in line with current technology
|
||||||
|
indent-width = 4
|
||||||
|
target-version = "py39"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E4", "E7", "E9", "F", "B"]
|
||||||
|
unfixable = ["B"]
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
quote-style = "single"
|
||||||
|
skip-magic-trailing-comma = false
|
||||||
0
src/graph_creators/__init.py__
Normal file
0
src/graph_creators/__init.py__
Normal file
53
K-ShakeTune/scripts/analyze_axesmap.py → src/graph_creators/analyze_axesmap.py
Executable file → Normal file
53
K-ShakeTune/scripts/analyze_axesmap.py → src/graph_creators/analyze_axesmap.py
Executable file → Normal file
@@ -5,17 +5,12 @@
|
|||||||
######################################
|
######################################
|
||||||
# Written by Frix_x#0161 #
|
# Written by Frix_x#0161 #
|
||||||
|
|
||||||
# Be sure to make this script executable using SSH: type 'chmod +x ./analyze_axesmap.py' when in the folder !
|
|
||||||
|
|
||||||
#####################################################################
|
|
||||||
################ !!! DO NOT EDIT BELOW THIS LINE !!! ################
|
|
||||||
#####################################################################
|
|
||||||
|
|
||||||
import optparse
|
import optparse
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from locale_utils import print_with_c_locale
|
|
||||||
from scipy.signal import butter, filtfilt
|
from scipy.signal import butter, filtfilt
|
||||||
|
|
||||||
|
from ..helpers.locale_utils import print_with_c_locale
|
||||||
|
|
||||||
NUM_POINTS = 500
|
NUM_POINTS = 500
|
||||||
|
|
||||||
@@ -24,6 +19,7 @@ NUM_POINTS = 500
|
|||||||
# Computation
|
# Computation
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
|
||||||
def accel_signal_filter(data, cutoff=2, fs=100, order=5):
|
def accel_signal_filter(data, cutoff=2, fs=100, order=5):
|
||||||
nyq = 0.5 * fs
|
nyq = 0.5 * fs
|
||||||
normal_cutoff = cutoff / nyq
|
normal_cutoff = cutoff / nyq
|
||||||
@@ -32,10 +28,12 @@ def accel_signal_filter(data, cutoff=2, fs=100, order=5):
|
|||||||
filtered_data -= np.mean(filtered_data)
|
filtered_data -= np.mean(filtered_data)
|
||||||
return filtered_data
|
return filtered_data
|
||||||
|
|
||||||
|
|
||||||
def find_first_spike(data):
|
def find_first_spike(data):
|
||||||
min_index, max_index = np.argmin(data), np.argmax(data)
|
min_index, max_index = np.argmin(data), np.argmax(data)
|
||||||
return ('-', min_index) if min_index < max_index else ('', max_index)
|
return ('-', min_index) if min_index < max_index else ('', max_index)
|
||||||
|
|
||||||
|
|
||||||
def get_movement_vector(data, start_idx, end_idx):
|
def get_movement_vector(data, start_idx, end_idx):
|
||||||
if start_idx < end_idx:
|
if start_idx < end_idx:
|
||||||
vector = []
|
vector = []
|
||||||
@@ -45,21 +43,19 @@ def get_movement_vector(data, start_idx, end_idx):
|
|||||||
else:
|
else:
|
||||||
return np.zeros(3)
|
return np.zeros(3)
|
||||||
|
|
||||||
|
|
||||||
def angle_between(v1, v2):
|
def angle_between(v1, v2):
|
||||||
v1_u = v1 / np.linalg.norm(v1)
|
v1_u = v1 / np.linalg.norm(v1)
|
||||||
v2_u = v2 / np.linalg.norm(v2)
|
v2_u = v2 / np.linalg.norm(v2)
|
||||||
return np.arccos(np.clip(np.dot(v1_u, v2_u), -1.0, 1.0))
|
return np.arccos(np.clip(np.dot(v1_u, v2_u), -1.0, 1.0))
|
||||||
|
|
||||||
|
|
||||||
def compute_errors(filtered_data, spikes_sorted, accel_value, num_points):
|
def compute_errors(filtered_data, spikes_sorted, accel_value, num_points):
|
||||||
# Get the movement start points in the correct order from the sorted bag of spikes
|
# Get the movement start points in the correct order from the sorted bag of spikes
|
||||||
movement_starts = [spike[0][1] for spike in spikes_sorted]
|
movement_starts = [spike[0][1] for spike in spikes_sorted]
|
||||||
|
|
||||||
# Theoretical unit vectors for X, Y, Z printer axes
|
# Theoretical unit vectors for X, Y, Z printer axes
|
||||||
printer_axes = {
|
printer_axes = {'x': np.array([1, 0, 0]), 'y': np.array([0, 1, 0]), 'z': np.array([0, 0, 1])}
|
||||||
'x': np.array([1, 0, 0]),
|
|
||||||
'y': np.array([0, 1, 0]),
|
|
||||||
'z': np.array([0, 0, 1])
|
|
||||||
}
|
|
||||||
|
|
||||||
alignment_errors = {}
|
alignment_errors = {}
|
||||||
sensitivity_errors = {}
|
sensitivity_errors = {}
|
||||||
@@ -82,6 +78,7 @@ def compute_errors(filtered_data, spikes_sorted, accel_value, num_points):
|
|||||||
# Startup and main routines
|
# Startup and main routines
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
|
||||||
def parse_log(logname):
|
def parse_log(logname):
|
||||||
with open(logname) as f:
|
with open(logname) as f:
|
||||||
for header in f:
|
for header in f:
|
||||||
@@ -91,26 +88,28 @@ def parse_log(logname):
|
|||||||
# Raw accelerometer data
|
# Raw accelerometer data
|
||||||
return np.loadtxt(logname, comments='#', delimiter=',')
|
return np.loadtxt(logname, comments='#', delimiter=',')
|
||||||
# Power spectral density data or shaper calibration data
|
# Power spectral density data or shaper calibration data
|
||||||
raise ValueError("File %s does not contain raw accelerometer data and therefore "
|
raise ValueError(
|
||||||
"is not supported by this script. Please use the official Klipper "
|
'File %s does not contain raw accelerometer data and therefore '
|
||||||
"calibrate_shaper.py script to process it instead." % (logname,))
|
'is not supported by this script. Please use the official Klipper '
|
||||||
|
'calibrate_shaper.py script to process it instead.' % (logname,)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def axesmap_calibration(lognames, accel=None):
|
def axesmap_calibration(lognames, accel=None):
|
||||||
# Parse the raw data and get them ready for analysis
|
# Parse the raw data and get them ready for analysis
|
||||||
raw_datas = [parse_log(filename) for filename in lognames]
|
raw_datas = [parse_log(filename) for filename in lognames]
|
||||||
if len(raw_datas) > 1:
|
if len(raw_datas) > 1:
|
||||||
raise ValueError("Analysis of multiple CSV files at once is not possible with this script")
|
raise ValueError('Analysis of multiple CSV files at once is not possible with this script')
|
||||||
|
|
||||||
filtered_data = [accel_signal_filter(raw_datas[0][:, i+1]) for i in range(3)]
|
filtered_data = [accel_signal_filter(raw_datas[0][:, i + 1]) for i in range(3)]
|
||||||
spikes = [find_first_spike(filtered_data[i]) for i in range(3)]
|
spikes = [find_first_spike(filtered_data[i]) for i in range(3)]
|
||||||
spikes_sorted = sorted([(spikes[0], 'x'), (spikes[1], 'y'), (spikes[2], 'z')], key=lambda x: x[0][1])
|
spikes_sorted = sorted([(spikes[0], 'x'), (spikes[1], 'y'), (spikes[2], 'z')], key=lambda x: x[0][1])
|
||||||
|
|
||||||
# Using the previous variables to get the axes_map and errors
|
# Using the previous variables to get the axes_map and errors
|
||||||
axes_map = ','.join([f"{spike[0][0]}{spike[1]}" for spike in spikes_sorted])
|
axes_map = ','.join([f'{spike[0][0]}{spike[1]}' for spike in spikes_sorted])
|
||||||
# alignment_error, sensitivity_error = compute_errors(filtered_data, spikes_sorted, accel, NUM_POINTS)
|
# alignment_error, sensitivity_error = compute_errors(filtered_data, spikes_sorted, accel, NUM_POINTS)
|
||||||
|
|
||||||
results = f"Detected axes_map:\n {axes_map}\n"
|
results = f'Detected axes_map:\n {axes_map}\n'
|
||||||
|
|
||||||
# TODO: work on this function that is currently not giving good results...
|
# TODO: work on this function that is currently not giving good results...
|
||||||
# results += "Accelerometer angle deviation:\n"
|
# results += "Accelerometer angle deviation:\n"
|
||||||
@@ -127,21 +126,21 @@ def axesmap_calibration(lognames, accel=None):
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
# Parse command-line arguments
|
# Parse command-line arguments
|
||||||
usage = "%prog [options] <raw logs>"
|
usage = '%prog [options] <raw logs>'
|
||||||
opts = optparse.OptionParser(usage)
|
opts = optparse.OptionParser(usage)
|
||||||
opts.add_option("-o", "--output", type="string", dest="output",
|
opts.add_option('-o', '--output', type='string', dest='output', default=None, help='filename of output graph')
|
||||||
default=None, help="filename of output graph")
|
opts.add_option(
|
||||||
opts.add_option("-a", "--accel", type="string", dest="accel",
|
'-a', '--accel', type='string', dest='accel', default=None, help='acceleration value used to do the movements'
|
||||||
default=None, help="acceleration value used to do the movements")
|
)
|
||||||
options, args = opts.parse_args()
|
options, args = opts.parse_args()
|
||||||
if len(args) < 1:
|
if len(args) < 1:
|
||||||
opts.error("No CSV file(s) to analyse")
|
opts.error('No CSV file(s) to analyse')
|
||||||
if options.accel is None:
|
if options.accel is None:
|
||||||
opts.error("You must specify the acceleration value used when generating the CSV file (option -a)")
|
opts.error('You must specify the acceleration value used when generating the CSV file (option -a)')
|
||||||
try:
|
try:
|
||||||
accel_value = float(options.accel)
|
accel_value = float(options.accel)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
opts.error("Invalid acceleration value. It should be a numeric value.")
|
opts.error('Invalid acceleration value. It should be a numeric value.')
|
||||||
|
|
||||||
results = axesmap_calibration(args, accel_value)
|
results = axesmap_calibration(args, accel_value)
|
||||||
print_with_c_locale(results)
|
print_with_c_locale(results)
|
||||||
327
K-ShakeTune/scripts/graph_belts.py → src/graph_creators/graph_belts.py
Executable file → Normal file
327
K-ShakeTune/scripts/graph_belts.py → src/graph_creators/graph_belts.py
Executable file → Normal file
@@ -5,27 +5,31 @@
|
|||||||
#################################################
|
#################################################
|
||||||
# Written by Frix_x#0161 #
|
# Written by Frix_x#0161 #
|
||||||
|
|
||||||
# Be sure to make this script executable using SSH: type 'chmod +x ./graph_belts.py' when in the folder!
|
import optparse
|
||||||
|
import os
|
||||||
#####################################################################
|
|
||||||
################ !!! DO NOT EDIT BELOW THIS LINE !!! ################
|
|
||||||
#####################################################################
|
|
||||||
|
|
||||||
import optparse, matplotlib, os
|
|
||||||
from datetime import datetime
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
import numpy as np
|
from datetime import datetime
|
||||||
|
|
||||||
|
import matplotlib
|
||||||
|
import matplotlib.colors
|
||||||
|
import matplotlib.font_manager
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
import matplotlib.font_manager, matplotlib.ticker, matplotlib.colors
|
import matplotlib.ticker
|
||||||
|
import numpy as np
|
||||||
from scipy.interpolate import griddata
|
from scipy.interpolate import griddata
|
||||||
|
|
||||||
matplotlib.use('Agg')
|
matplotlib.use('Agg')
|
||||||
|
|
||||||
from locale_utils import set_locale, print_with_c_locale
|
from ..helpers.common_func import (
|
||||||
from common_func import compute_spectrogram, detect_peaks, get_git_version, parse_log, setup_klipper_import, compute_curve_similarity_factor
|
compute_curve_similarity_factor,
|
||||||
|
compute_spectrogram,
|
||||||
|
detect_peaks,
|
||||||
|
parse_log,
|
||||||
|
setup_klipper_import,
|
||||||
|
)
|
||||||
|
from ..helpers.locale_utils import print_with_c_locale, set_locale
|
||||||
|
|
||||||
|
ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' # For paired peaks names
|
||||||
ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # For paired peaks names
|
|
||||||
|
|
||||||
PEAKS_DETECTION_THRESHOLD = 0.20
|
PEAKS_DETECTION_THRESHOLD = 0.20
|
||||||
CURVE_SIMILARITY_SIGMOID_K = 0.6
|
CURVE_SIMILARITY_SIGMOID_K = 0.6
|
||||||
@@ -37,11 +41,11 @@ DC_MAX_UNPAIRED_PEAKS_ALLOWED = 4
|
|||||||
SignalData = namedtuple('CalibrationData', ['freqs', 'psd', 'peaks', 'paired_peaks', 'unpaired_peaks'])
|
SignalData = namedtuple('CalibrationData', ['freqs', 'psd', 'peaks', 'paired_peaks', 'unpaired_peaks'])
|
||||||
|
|
||||||
KLIPPAIN_COLORS = {
|
KLIPPAIN_COLORS = {
|
||||||
"purple": "#70088C",
|
'purple': '#70088C',
|
||||||
"orange": "#FF8D32",
|
'orange': '#FF8D32',
|
||||||
"dark_purple": "#150140",
|
'dark_purple': '#150140',
|
||||||
"dark_orange": "#F24130",
|
'dark_orange': '#F24130',
|
||||||
"red_pink": "#F2055C"
|
'red_pink': '#F2055C',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -49,6 +53,7 @@ KLIPPAIN_COLORS = {
|
|||||||
# Computation of the PSD graph
|
# Computation of the PSD graph
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
|
||||||
# This function create pairs of peaks that are close in frequency on two curves (that are known
|
# This function create pairs of peaks that are close in frequency on two curves (that are known
|
||||||
# to be resonances points and must be similar on both belts on a CoreXY kinematic)
|
# to be resonances points and must be similar on both belts on a CoreXY kinematic)
|
||||||
def pair_peaks(peaks1, freqs1, psd1, peaks2, freqs2, psd2):
|
def pair_peaks(peaks1, freqs1, psd1, peaks2, freqs2, psd2):
|
||||||
@@ -82,7 +87,7 @@ def pair_peaks(peaks1, freqs1, psd1, peaks2, freqs2, psd2):
|
|||||||
min_distance = distance
|
min_distance = distance
|
||||||
pair = (p1, p2)
|
pair = (p1, p2)
|
||||||
|
|
||||||
if pair is None: # No more pairs below the threshold
|
if pair is None: # No more pairs below the threshold
|
||||||
break
|
break
|
||||||
|
|
||||||
p1, p2 = pair
|
p1, p2 = pair
|
||||||
@@ -97,6 +102,7 @@ def pair_peaks(peaks1, freqs1, psd1, peaks2, freqs2, psd2):
|
|||||||
# Computation of the differential spectrogram
|
# Computation of the differential spectrogram
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
|
||||||
# Interpolate source_data (2D) to match target_x and target_y in order to
|
# Interpolate source_data (2D) to match target_x and target_y in order to
|
||||||
# get similar time and frequency dimensions for the differential spectrogram
|
# get similar time and frequency dimensions for the differential spectrogram
|
||||||
def interpolate_2d(target_x, target_y, source_x, source_y, source_data):
|
def interpolate_2d(target_x, target_y, source_x, source_y, source_data):
|
||||||
@@ -163,41 +169,44 @@ def compute_mhi(combined_data, similarity_coefficient, num_unpaired_peaks):
|
|||||||
# LUT to transform the MHI into a textual value easy to understand for the users of the script
|
# LUT to transform the MHI into a textual value easy to understand for the users of the script
|
||||||
def mhi_lut(mhi):
|
def mhi_lut(mhi):
|
||||||
ranges = [
|
ranges = [
|
||||||
(0, 30, "Excellent mechanical health"),
|
(0, 30, 'Excellent mechanical health'),
|
||||||
(30, 45, "Good mechanical health"),
|
(30, 45, 'Good mechanical health'),
|
||||||
(45, 55, "Acceptable mechanical health"),
|
(45, 55, 'Acceptable mechanical health'),
|
||||||
(55, 70, "Potential signs of a mechanical issue"),
|
(55, 70, 'Potential signs of a mechanical issue'),
|
||||||
(70, 85, "Likely a mechanical issue"),
|
(70, 85, 'Likely a mechanical issue'),
|
||||||
(85, 100, "Mechanical issue detected")
|
(85, 100, 'Mechanical issue detected'),
|
||||||
]
|
]
|
||||||
for lower, upper, message in ranges:
|
for lower, upper, message in ranges:
|
||||||
if lower < mhi <= upper:
|
if lower < mhi <= upper:
|
||||||
return message
|
return message
|
||||||
|
|
||||||
return "Error computing MHI value"
|
return 'Error computing MHI value'
|
||||||
|
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
# Graphing
|
# Graphing
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
|
||||||
def plot_compare_frequency(ax, lognames, signal1, signal2, similarity_factor, max_freq):
|
def plot_compare_frequency(ax, lognames, signal1, signal2, similarity_factor, max_freq):
|
||||||
# Get the belt name for the legend to avoid putting the full file name
|
# Get the belt name for the legend to avoid putting the full file name
|
||||||
signal1_belt = (lognames[0].split('/')[-1]).split('_')[-1][0]
|
signal1_belt = (lognames[0].split('/')[-1]).split('_')[-1][0]
|
||||||
signal2_belt = (lognames[1].split('/')[-1]).split('_')[-1][0]
|
signal2_belt = (lognames[1].split('/')[-1]).split('_')[-1][0]
|
||||||
|
|
||||||
if signal1_belt == 'A' and signal2_belt == 'B':
|
if signal1_belt == 'A' and signal2_belt == 'B':
|
||||||
signal1_belt += " (axis 1,-1)"
|
signal1_belt += ' (axis 1,-1)'
|
||||||
signal2_belt += " (axis 1, 1)"
|
signal2_belt += ' (axis 1, 1)'
|
||||||
elif signal1_belt == 'B' and signal2_belt == 'A':
|
elif signal1_belt == 'B' and signal2_belt == 'A':
|
||||||
signal1_belt += " (axis 1, 1)"
|
signal1_belt += ' (axis 1, 1)'
|
||||||
signal2_belt += " (axis 1,-1)"
|
signal2_belt += ' (axis 1,-1)'
|
||||||
else:
|
else:
|
||||||
print_with_c_locale("Warning: belts doesn't seem to have the correct name A and B (extracted from the filename.csv)")
|
print_with_c_locale(
|
||||||
|
"Warning: belts doesn't seem to have the correct name A and B (extracted from the filename.csv)"
|
||||||
|
)
|
||||||
|
|
||||||
# Plot the two belts PSD signals
|
# Plot the two belts PSD signals
|
||||||
ax.plot(signal1.freqs, signal1.psd, label="Belt " + signal1_belt, color=KLIPPAIN_COLORS['purple'])
|
ax.plot(signal1.freqs, signal1.psd, label='Belt ' + signal1_belt, color=KLIPPAIN_COLORS['purple'])
|
||||||
ax.plot(signal2.freqs, signal2.psd, label="Belt " + signal2_belt, color=KLIPPAIN_COLORS['orange'])
|
ax.plot(signal2.freqs, signal2.psd, label='Belt ' + signal2_belt, color=KLIPPAIN_COLORS['orange'])
|
||||||
|
|
||||||
# Trace the "relax region" (also used as a threshold to filter and detect the peaks)
|
# Trace the "relax region" (also used as a threshold to filter and detect the peaks)
|
||||||
psd_lowest_max = min(signal1.psd.max(), signal2.psd.max())
|
psd_lowest_max = min(signal1.psd.max(), signal2.psd.max())
|
||||||
@@ -212,38 +221,71 @@ def plot_compare_frequency(ax, lognames, signal1, signal2, similarity_factor, ma
|
|||||||
|
|
||||||
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]]) / max(signal1.psd[peak1[0]], signal2.psd[peak2[0]])) * 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} %'])
|
||||||
|
|
||||||
ax.plot(signal1.freqs[peak1[0]], signal1.psd[peak1[0]], "x", color='black')
|
ax.plot(signal1.freqs[peak1[0]], signal1.psd[peak1[0]], 'x', color='black')
|
||||||
ax.plot(signal2.freqs[peak2[0]], signal2.psd[peak2[0]], "x", color='black')
|
ax.plot(signal2.freqs[peak2[0]], signal2.psd[peak2[0]], 'x', color='black')
|
||||||
ax.plot([signal1.freqs[peak1[0]], signal2.freqs[peak2[0]]], [signal1.psd[peak1[0]], signal2.psd[peak2[0]]], ":", color='gray')
|
ax.plot(
|
||||||
|
[signal1.freqs[peak1[0]], signal2.freqs[peak2[0]]],
|
||||||
|
[signal1.psd[peak1[0]], signal2.psd[peak2[0]]],
|
||||||
|
':',
|
||||||
|
color='gray',
|
||||||
|
)
|
||||||
|
|
||||||
ax.annotate(label + "1", (signal1.freqs[peak1[0]], signal1.psd[peak1[0]]),
|
ax.annotate(
|
||||||
textcoords="offset points", xytext=(8, 5),
|
label + '1',
|
||||||
ha='left', fontsize=13, color='black')
|
(signal1.freqs[peak1[0]], signal1.psd[peak1[0]]),
|
||||||
ax.annotate(label + "2", (signal2.freqs[peak2[0]], signal2.psd[peak2[0]]),
|
textcoords='offset points',
|
||||||
textcoords="offset points", xytext=(8, 5),
|
xytext=(8, 5),
|
||||||
ha='left', fontsize=13, color='black')
|
ha='left',
|
||||||
|
fontsize=13,
|
||||||
|
color='black',
|
||||||
|
)
|
||||||
|
ax.annotate(
|
||||||
|
label + '2',
|
||||||
|
(signal2.freqs[peak2[0]], signal2.psd[peak2[0]]),
|
||||||
|
textcoords='offset points',
|
||||||
|
xytext=(8, 5),
|
||||||
|
ha='left',
|
||||||
|
fontsize=13,
|
||||||
|
color='black',
|
||||||
|
)
|
||||||
paired_peak_count += 1
|
paired_peak_count += 1
|
||||||
|
|
||||||
for peak in signal1.unpaired_peaks:
|
for peak in signal1.unpaired_peaks:
|
||||||
ax.plot(signal1.freqs[peak], signal1.psd[peak], "x", color='black')
|
ax.plot(signal1.freqs[peak], signal1.psd[peak], 'x', color='black')
|
||||||
ax.annotate(str(unpaired_peak_count + 1), (signal1.freqs[peak], signal1.psd[peak]),
|
ax.annotate(
|
||||||
textcoords="offset points", xytext=(8, 5),
|
str(unpaired_peak_count + 1),
|
||||||
ha='left', fontsize=13, color='red', weight='bold')
|
(signal1.freqs[peak], signal1.psd[peak]),
|
||||||
|
textcoords='offset points',
|
||||||
|
xytext=(8, 5),
|
||||||
|
ha='left',
|
||||||
|
fontsize=13,
|
||||||
|
color='red',
|
||||||
|
weight='bold',
|
||||||
|
)
|
||||||
unpaired_peak_count += 1
|
unpaired_peak_count += 1
|
||||||
|
|
||||||
for peak in signal2.unpaired_peaks:
|
for peak in signal2.unpaired_peaks:
|
||||||
ax.plot(signal2.freqs[peak], signal2.psd[peak], "x", color='black')
|
ax.plot(signal2.freqs[peak], signal2.psd[peak], 'x', color='black')
|
||||||
ax.annotate(str(unpaired_peak_count + 1), (signal2.freqs[peak], signal2.psd[peak]),
|
ax.annotate(
|
||||||
textcoords="offset points", xytext=(8, 5),
|
str(unpaired_peak_count + 1),
|
||||||
ha='left', fontsize=13, color='red', weight='bold')
|
(signal2.freqs[peak], signal2.psd[peak]),
|
||||||
|
textcoords='offset points',
|
||||||
|
xytext=(8, 5),
|
||||||
|
ha='left',
|
||||||
|
fontsize=13,
|
||||||
|
color='red',
|
||||||
|
weight='bold',
|
||||||
|
)
|
||||||
unpaired_peak_count += 1
|
unpaired_peak_count += 1
|
||||||
|
|
||||||
# Add estimated similarity to the graph
|
# Add estimated similarity to the graph
|
||||||
ax2 = ax.twinx() # To split the legends in two box
|
ax2 = ax.twinx() # To split the legends in two box
|
||||||
ax2.yaxis.set_visible(False)
|
ax2.yaxis.set_visible(False)
|
||||||
ax2.plot([], [], ' ', label=f'Estimated similarity: {similarity_factor:.1f}%')
|
ax2.plot([], [], ' ', label=f'Estimated similarity: {similarity_factor:.1f}%')
|
||||||
ax2.plot([], [], ' ', label=f'Number of unpaired peaks: {unpaired_peak_count}')
|
ax2.plot([], [], ' ', label=f'Number of unpaired peaks: {unpaired_peak_count}')
|
||||||
@@ -257,17 +299,32 @@ def plot_compare_frequency(ax, lognames, signal1, signal2, similarity_factor, ma
|
|||||||
|
|
||||||
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='y', 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()
|
||||||
fontP.set_size('small')
|
fontP.set_size('small')
|
||||||
ax.set_title('Belts Frequency Profiles (estimated similarity: {:.1f}%)'.format(similarity_factor), fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
|
ax.set_title(
|
||||||
|
'Belts Frequency Profiles (estimated similarity: {:.1f}%)'.format(similarity_factor),
|
||||||
|
fontsize=14,
|
||||||
|
color=KLIPPAIN_COLORS['dark_orange'],
|
||||||
|
weight='bold',
|
||||||
|
)
|
||||||
|
|
||||||
# Print the table of offsets ontop of the graph below the original legend (upper right)
|
# Print the table of offsets ontop of the graph below the original legend (upper right)
|
||||||
if len(offsets_table_data) > 0:
|
if len(offsets_table_data) > 0:
|
||||||
columns = ["", "Frequency delta", "Amplitude delta", ]
|
columns = [
|
||||||
offset_table = ax.table(cellText=offsets_table_data, colLabels=columns, bbox=[0.66, 0.75, 0.33, 0.15], loc='upper right', cellLoc='center')
|
'',
|
||||||
|
'Frequency delta',
|
||||||
|
'Amplitude delta',
|
||||||
|
]
|
||||||
|
offset_table = ax.table(
|
||||||
|
cellText=offsets_table_data,
|
||||||
|
colLabels=columns,
|
||||||
|
bbox=[0.66, 0.75, 0.33, 0.15],
|
||||||
|
loc='upper right',
|
||||||
|
cellLoc='center',
|
||||||
|
)
|
||||||
offset_table.auto_set_font_size(False)
|
offset_table.auto_set_font_size(False)
|
||||||
offset_table.set_fontsize(8)
|
offset_table.set_fontsize(8)
|
||||||
offset_table.auto_set_column_width([0, 1, 2])
|
offset_table.auto_set_column_width([0, 1, 2])
|
||||||
@@ -284,19 +341,35 @@ def plot_compare_frequency(ax, lognames, signal1, signal2, similarity_factor, ma
|
|||||||
|
|
||||||
|
|
||||||
def plot_difference_spectrogram(ax, signal1, signal2, t, bins, combined_divergent, textual_mhi, max_freq):
|
def plot_difference_spectrogram(ax, signal1, signal2, t, bins, combined_divergent, textual_mhi, max_freq):
|
||||||
ax.set_title(f"Differential Spectrogram", fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
|
ax.set_title('Differential Spectrogram', fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
|
||||||
ax.plot([], [], ' ', label=f'{textual_mhi} (experimental)')
|
ax.plot([], [], ' ', label=f'{textual_mhi} (experimental)')
|
||||||
|
|
||||||
# Draw the differential spectrogram with a specific custom norm to get orange or purple values where there is signal or white near zeros
|
# Draw the differential spectrogram with a specific custom norm to get orange or purple values where there is signal or white near zeros
|
||||||
# imgshow is better suited here than pcolormesh since its result is already rasterized and we doesn't need to keep vector graphics
|
# imgshow is better suited here than pcolormesh since its result is already rasterized and we doesn't need to keep vector graphics
|
||||||
# when saving to a final .png file. Using it also allow to save ~150-200MB of RAM during the "fig.savefig" operation.
|
# when saving to a final .png file. Using it also allow to save ~150-200MB of RAM during the "fig.savefig" operation.
|
||||||
colors = [KLIPPAIN_COLORS['dark_orange'], KLIPPAIN_COLORS['orange'], 'white', KLIPPAIN_COLORS['purple'], KLIPPAIN_COLORS['dark_purple']]
|
colors = [
|
||||||
cm = matplotlib.colors.LinearSegmentedColormap.from_list('klippain_divergent', list(zip([0, 0.25, 0.5, 0.75, 1], colors)))
|
KLIPPAIN_COLORS['dark_orange'],
|
||||||
|
KLIPPAIN_COLORS['orange'],
|
||||||
|
'white',
|
||||||
|
KLIPPAIN_COLORS['purple'],
|
||||||
|
KLIPPAIN_COLORS['dark_purple'],
|
||||||
|
]
|
||||||
|
cm = matplotlib.colors.LinearSegmentedColormap.from_list(
|
||||||
|
'klippain_divergent', list(zip([0, 0.25, 0.5, 0.75, 1], colors))
|
||||||
|
)
|
||||||
norm = matplotlib.colors.TwoSlopeNorm(vmin=np.min(combined_divergent), vcenter=0, vmax=np.max(combined_divergent))
|
norm = matplotlib.colors.TwoSlopeNorm(vmin=np.min(combined_divergent), vcenter=0, vmax=np.max(combined_divergent))
|
||||||
ax.imshow(combined_divergent.T, cmap=cm, norm=norm, aspect='auto', extent=[t[0], t[-1], bins[0], bins[-1]], interpolation='bilinear', origin='lower')
|
ax.imshow(
|
||||||
|
combined_divergent.T,
|
||||||
|
cmap=cm,
|
||||||
|
norm=norm,
|
||||||
|
aspect='auto',
|
||||||
|
extent=[t[0], t[-1], bins[0], bins[-1]],
|
||||||
|
interpolation='bilinear',
|
||||||
|
origin='lower',
|
||||||
|
)
|
||||||
|
|
||||||
ax.set_xlabel('Frequency (hz)')
|
ax.set_xlabel('Frequency (hz)')
|
||||||
ax.set_xlim([0., max_freq])
|
ax.set_xlim([0.0, max_freq])
|
||||||
ax.set_ylabel('Time (s)')
|
ax.set_ylabel('Time (s)')
|
||||||
ax.set_ylim([0, bins[-1]])
|
ax.set_ylim([0, bins[-1]])
|
||||||
|
|
||||||
@@ -308,17 +381,31 @@ def plot_difference_spectrogram(ax, signal1, signal2, t, bins, combined_divergen
|
|||||||
unpaired_peak_count = 0
|
unpaired_peak_count = 0
|
||||||
for _, peak in enumerate(signal1.unpaired_peaks):
|
for _, peak in enumerate(signal1.unpaired_peaks):
|
||||||
ax.axvline(signal1.freqs[peak], color=KLIPPAIN_COLORS['red_pink'], linestyle='dotted', linewidth=1.5)
|
ax.axvline(signal1.freqs[peak], color=KLIPPAIN_COLORS['red_pink'], linestyle='dotted', linewidth=1.5)
|
||||||
ax.annotate(f"Peak {unpaired_peak_count + 1}", (signal1.freqs[peak], t[-1]*0.05),
|
ax.annotate(
|
||||||
textcoords="data", color=KLIPPAIN_COLORS['red_pink'], rotation=90, fontsize=10,
|
f'Peak {unpaired_peak_count + 1}',
|
||||||
verticalalignment='bottom', horizontalalignment='right')
|
(signal1.freqs[peak], t[-1] * 0.05),
|
||||||
unpaired_peak_count +=1
|
textcoords='data',
|
||||||
|
color=KLIPPAIN_COLORS['red_pink'],
|
||||||
|
rotation=90,
|
||||||
|
fontsize=10,
|
||||||
|
verticalalignment='bottom',
|
||||||
|
horizontalalignment='right',
|
||||||
|
)
|
||||||
|
unpaired_peak_count += 1
|
||||||
|
|
||||||
for _, peak in enumerate(signal2.unpaired_peaks):
|
for _, peak in enumerate(signal2.unpaired_peaks):
|
||||||
ax.axvline(signal2.freqs[peak], color=KLIPPAIN_COLORS['red_pink'], linestyle='dotted', linewidth=1.5)
|
ax.axvline(signal2.freqs[peak], color=KLIPPAIN_COLORS['red_pink'], linestyle='dotted', linewidth=1.5)
|
||||||
ax.annotate(f"Peak {unpaired_peak_count + 1}", (signal2.freqs[peak], t[-1]*0.05),
|
ax.annotate(
|
||||||
textcoords="data", color=KLIPPAIN_COLORS['red_pink'], rotation=90, fontsize=10,
|
f'Peak {unpaired_peak_count + 1}',
|
||||||
verticalalignment='bottom', horizontalalignment='right')
|
(signal2.freqs[peak], t[-1] * 0.05),
|
||||||
unpaired_peak_count +=1
|
textcoords='data',
|
||||||
|
color=KLIPPAIN_COLORS['red_pink'],
|
||||||
|
rotation=90,
|
||||||
|
fontsize=10,
|
||||||
|
verticalalignment='bottom',
|
||||||
|
horizontalalignment='right',
|
||||||
|
)
|
||||||
|
unpaired_peak_count += 1
|
||||||
|
|
||||||
# Plot vertical lines and zones for paired peaks
|
# Plot vertical lines and zones for paired peaks
|
||||||
for idx, (peak1, peak2) in enumerate(signal1.paired_peaks):
|
for idx, (peak1, peak2) in enumerate(signal1.paired_peaks):
|
||||||
@@ -328,9 +415,16 @@ def plot_difference_spectrogram(ax, signal1, signal2, t, bins, combined_divergen
|
|||||||
ax.axvline(x_min, color=KLIPPAIN_COLORS['dark_purple'], linestyle='dotted', linewidth=1.5)
|
ax.axvline(x_min, color=KLIPPAIN_COLORS['dark_purple'], linestyle='dotted', linewidth=1.5)
|
||||||
ax.axvline(x_max, color=KLIPPAIN_COLORS['dark_purple'], linestyle='dotted', linewidth=1.5)
|
ax.axvline(x_max, color=KLIPPAIN_COLORS['dark_purple'], linestyle='dotted', linewidth=1.5)
|
||||||
ax.fill_between([x_min, x_max], 0, np.max(combined_divergent), color=KLIPPAIN_COLORS['dark_purple'], alpha=0.3)
|
ax.fill_between([x_min, x_max], 0, np.max(combined_divergent), color=KLIPPAIN_COLORS['dark_purple'], alpha=0.3)
|
||||||
ax.annotate(f"Peaks {label}", (x_min, t[-1]*0.05),
|
ax.annotate(
|
||||||
textcoords="data", color=KLIPPAIN_COLORS['dark_purple'], rotation=90, fontsize=10,
|
f'Peaks {label}',
|
||||||
verticalalignment='bottom', horizontalalignment='right')
|
(x_min, t[-1] * 0.05),
|
||||||
|
textcoords='data',
|
||||||
|
color=KLIPPAIN_COLORS['dark_purple'],
|
||||||
|
rotation=90,
|
||||||
|
fontsize=10,
|
||||||
|
verticalalignment='bottom',
|
||||||
|
horizontalalignment='right',
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -339,6 +433,7 @@ def plot_difference_spectrogram(ax, signal1, signal2, t, bins, combined_divergen
|
|||||||
# Custom tools
|
# Custom tools
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
|
||||||
# Original Klipper function to get the PSD data of a raw accelerometer signal
|
# Original Klipper function to get the PSD data of a raw accelerometer signal
|
||||||
def compute_signal_data(data, max_freq):
|
def compute_signal_data(data, max_freq):
|
||||||
helper = shaper_calibrate.ShaperCalibrate(printer=None)
|
helper = shaper_calibrate.ShaperCalibrate(printer=None)
|
||||||
@@ -356,7 +451,8 @@ def compute_signal_data(data, max_freq):
|
|||||||
# Startup and main routines
|
# Startup and main routines
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def belts_calibration(lognames, klipperdir="~/klipper", max_freq=200.):
|
|
||||||
|
def belts_calibration(lognames, klipperdir='~/klipper', max_freq=200.0, st_version=None):
|
||||||
set_locale()
|
set_locale()
|
||||||
global shaper_calibrate
|
global shaper_calibrate
|
||||||
shaper_calibrate = setup_klipper_import(klipperdir)
|
shaper_calibrate = setup_klipper_import(klipperdir)
|
||||||
@@ -364,7 +460,7 @@ def belts_calibration(lognames, klipperdir="~/klipper", max_freq=200.):
|
|||||||
# Parse data
|
# Parse data
|
||||||
datas = [parse_log(fn) for fn in lognames]
|
datas = [parse_log(fn) for fn in lognames]
|
||||||
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)!')
|
||||||
|
|
||||||
# Compute calibration data for the two datasets with automatic peaks detection
|
# Compute calibration data for the two datasets with automatic peaks detection
|
||||||
signal1 = compute_signal_data(datas[0], max_freq)
|
signal1 = compute_signal_data(datas[0], max_freq)
|
||||||
@@ -373,41 +469,54 @@ def belts_calibration(lognames, klipperdir="~/klipper", max_freq=200.):
|
|||||||
del datas
|
del datas
|
||||||
|
|
||||||
# Pair the peaks across the two datasets
|
# Pair the peaks across the two datasets
|
||||||
paired_peaks, unpaired_peaks1, unpaired_peaks2 = pair_peaks(signal1.peaks, signal1.freqs, signal1.psd,
|
paired_peaks, unpaired_peaks1, unpaired_peaks2 = pair_peaks(
|
||||||
signal2.peaks, signal2.freqs, signal2.psd)
|
signal1.peaks, signal1.freqs, signal1.psd, signal2.peaks, signal2.freqs, signal2.psd
|
||||||
signal1 = signal1._replace(paired_peaks = paired_peaks, unpaired_peaks = unpaired_peaks1)
|
)
|
||||||
signal2 = signal2._replace(paired_peaks = paired_peaks, unpaired_peaks = unpaired_peaks2)
|
signal1 = signal1._replace(paired_peaks=paired_peaks, unpaired_peaks=unpaired_peaks1)
|
||||||
|
signal2 = signal2._replace(paired_peaks=paired_peaks, unpaired_peaks=unpaired_peaks2)
|
||||||
|
|
||||||
# Compute the similarity (using cross-correlation of the PSD signals)
|
# Compute the similarity (using cross-correlation of the PSD signals)
|
||||||
similarity_factor = compute_curve_similarity_factor(signal1.freqs, signal1.psd, signal2.freqs, signal2.psd, CURVE_SIMILARITY_SIGMOID_K)
|
similarity_factor = compute_curve_similarity_factor(
|
||||||
print_with_c_locale(f"Belts estimated similarity: {similarity_factor:.1f}%")
|
signal1.freqs, signal1.psd, signal2.freqs, signal2.psd, CURVE_SIMILARITY_SIGMOID_K
|
||||||
|
)
|
||||||
|
print_with_c_locale(f'Belts estimated similarity: {similarity_factor:.1f}%')
|
||||||
# Compute the MHI value from the differential spectrogram sum of gradient, salted with the similarity factor and the number of
|
# Compute the MHI value from the differential spectrogram sum of gradient, salted with the similarity factor and the number of
|
||||||
# unpaired peaks from the belts frequency profile. Be careful, this value is highly opinionated and is pretty experimental!
|
# unpaired peaks from the belts frequency profile. Be careful, this value is highly opinionated and is pretty experimental!
|
||||||
mhi, textual_mhi = compute_mhi(combined_sum, similarity_factor, len(signal1.unpaired_peaks) + len(signal2.unpaired_peaks))
|
mhi, textual_mhi = compute_mhi(
|
||||||
print_with_c_locale(f"[experimental] Mechanical Health Indicator: {textual_mhi.lower()} ({mhi:.1f}%)")
|
combined_sum, similarity_factor, len(signal1.unpaired_peaks) + len(signal2.unpaired_peaks)
|
||||||
|
)
|
||||||
|
print_with_c_locale(f'[experimental] Mechanical Health Indicator: {textual_mhi.lower()} ({mhi:.1f}%)')
|
||||||
|
|
||||||
# Create graph layout
|
# Create graph layout
|
||||||
fig, (ax1, ax2) = plt.subplots(2, 1, gridspec_kw={
|
fig, (ax1, ax2) = plt.subplots(
|
||||||
'height_ratios':[4, 3],
|
2,
|
||||||
'bottom':0.050,
|
1,
|
||||||
'top':0.890,
|
gridspec_kw={
|
||||||
'left':0.085,
|
'height_ratios': [4, 3],
|
||||||
'right':0.966,
|
'bottom': 0.050,
|
||||||
'hspace':0.169,
|
'top': 0.890,
|
||||||
'wspace':0.200
|
'left': 0.085,
|
||||||
})
|
'right': 0.966,
|
||||||
|
'hspace': 0.169,
|
||||||
|
'wspace': 0.200,
|
||||||
|
},
|
||||||
|
)
|
||||||
fig.set_size_inches(8.3, 11.6)
|
fig.set_size_inches(8.3, 11.6)
|
||||||
|
|
||||||
# Add title
|
# Add title
|
||||||
title_line1 = "RELATIVE BELTS CALIBRATION TOOL"
|
title_line1 = 'RELATIVE BELTS CALIBRATION TOOL'
|
||||||
fig.text(0.12, 0.965, title_line1, ha='left', va='bottom', fontsize=20, color=KLIPPAIN_COLORS['purple'], weight='bold')
|
fig.text(
|
||||||
|
0.12, 0.965, title_line1, ha='left', va='bottom', fontsize=20, color=KLIPPAIN_COLORS['purple'], weight='bold'
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
filename = lognames[0].split('/')[-1]
|
filename = lognames[0].split('/')[-1]
|
||||||
dt = datetime.strptime(f"{filename.split('_')[1]} {filename.split('_')[2]}", "%Y%m%d %H%M%S")
|
dt = datetime.strptime(f"{filename.split('_')[1]} {filename.split('_')[2]}", '%Y%m%d %H%M%S')
|
||||||
title_line2 = dt.strftime('%x %X')
|
title_line2 = dt.strftime('%x %X')
|
||||||
except:
|
except Exception:
|
||||||
print_with_c_locale("Warning: CSV filenames look to be different than expected (%s , %s)" % (lognames[0], lognames[1]))
|
print_with_c_locale(
|
||||||
title_line2 = lognames[0].split('/')[-1] + " / " + lognames[1].split('/')[-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]
|
||||||
fig.text(0.12, 0.957, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple'])
|
fig.text(0.12, 0.957, title_line2, ha='left', va='top', fontsize=16, color=KLIPPAIN_COLORS['dark_purple'])
|
||||||
|
|
||||||
# Plot the graphs
|
# Plot the graphs
|
||||||
@@ -420,8 +529,7 @@ def belts_calibration(lognames, klipperdir="~/klipper", max_freq=200.):
|
|||||||
ax_logo.axis('off')
|
ax_logo.axis('off')
|
||||||
|
|
||||||
# Adding Shake&Tune version in the top right corner
|
# Adding Shake&Tune version in the top right corner
|
||||||
st_version = get_git_version()
|
if st_version != 'unknown':
|
||||||
if st_version is not None:
|
|
||||||
fig.text(0.995, 0.985, st_version, ha='right', va='bottom', fontsize=8, color=KLIPPAIN_COLORS['purple'])
|
fig.text(0.995, 0.985, st_version, ha='right', va='bottom', fontsize=8, color=KLIPPAIN_COLORS['purple'])
|
||||||
|
|
||||||
return fig
|
return fig
|
||||||
@@ -429,19 +537,18 @@ def belts_calibration(lognames, klipperdir="~/klipper", max_freq=200.):
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
# Parse command-line arguments
|
# Parse command-line arguments
|
||||||
usage = "%prog [options] <raw logs>"
|
usage = '%prog [options] <raw logs>'
|
||||||
opts = optparse.OptionParser(usage)
|
opts = optparse.OptionParser(usage)
|
||||||
opts.add_option("-o", "--output", type="string", dest="output",
|
opts.add_option('-o', '--output', type='string', dest='output', default=None, help='filename of output graph')
|
||||||
default=None, help="filename of output graph")
|
opts.add_option('-f', '--max_freq', type='float', default=200.0, help='maximum frequency to graph')
|
||||||
opts.add_option("-f", "--max_freq", type="float", default=200.,
|
opts.add_option(
|
||||||
help="maximum frequency to graph")
|
'-k', '--klipper_dir', type='string', dest='klipperdir', default='~/klipper', help='main klipper directory'
|
||||||
opts.add_option("-k", "--klipper_dir", type="string", dest="klipperdir",
|
)
|
||||||
default="~/klipper", help="main klipper directory")
|
|
||||||
options, args = opts.parse_args()
|
options, args = opts.parse_args()
|
||||||
if len(args) < 1:
|
if len(args) < 1:
|
||||||
opts.error("Incorrect number of arguments")
|
opts.error('Incorrect number of arguments')
|
||||||
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)')
|
||||||
|
|
||||||
fig = belts_calibration(args, options.klipperdir, options.max_freq)
|
fig = belts_calibration(args, options.klipperdir, options.max_freq)
|
||||||
fig.savefig(options.output, dpi=150)
|
fig.savefig(options.output, dpi=150)
|
||||||
274
K-ShakeTune/scripts/graph_shaper.py → src/graph_creators/graph_shaper.py
Executable file → Normal file
274
K-ShakeTune/scripts/graph_shaper.py → src/graph_creators/graph_shaper.py
Executable file → Normal file
@@ -6,25 +6,28 @@
|
|||||||
# Derived from the calibrate_shaper.py official Klipper script
|
# Derived from the calibrate_shaper.py official Klipper script
|
||||||
# Copyright (C) 2020 Dmitry Butyugin <dmbutyugin@google.com>
|
# Copyright (C) 2020 Dmitry Butyugin <dmbutyugin@google.com>
|
||||||
# Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>
|
# Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>
|
||||||
# Written by Frix_x#0161 #
|
# Highly modified and improved by Frix_x#0161 #
|
||||||
|
|
||||||
# Be sure to make this script executable using SSH: type 'chmod +x ./graph_shaper.py' when in the folder!
|
import optparse
|
||||||
|
import os
|
||||||
#####################################################################
|
|
||||||
################ !!! DO NOT EDIT BELOW THIS LINE !!! ################
|
|
||||||
#####################################################################
|
|
||||||
|
|
||||||
import optparse, matplotlib, os
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import numpy as np
|
|
||||||
|
import matplotlib
|
||||||
|
import matplotlib.font_manager
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
import matplotlib.font_manager, matplotlib.ticker
|
import matplotlib.ticker
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
matplotlib.use('Agg')
|
matplotlib.use('Agg')
|
||||||
|
|
||||||
from locale_utils import set_locale, print_with_c_locale
|
from ..helpers.common_func import (
|
||||||
from common_func import compute_mechanical_parameters, compute_spectrogram, detect_peaks, get_git_version, parse_log, setup_klipper_import
|
compute_mechanical_parameters,
|
||||||
|
compute_spectrogram,
|
||||||
|
detect_peaks,
|
||||||
|
parse_log,
|
||||||
|
setup_klipper_import,
|
||||||
|
)
|
||||||
|
from ..helpers.locale_utils import print_with_c_locale, set_locale
|
||||||
|
|
||||||
PEAKS_DETECTION_THRESHOLD = 0.05
|
PEAKS_DETECTION_THRESHOLD = 0.05
|
||||||
PEAKS_EFFECT_THRESHOLD = 0.12
|
PEAKS_EFFECT_THRESHOLD = 0.12
|
||||||
@@ -32,11 +35,11 @@ SPECTROGRAM_LOW_PERCENTILE_FILTER = 5
|
|||||||
MAX_SMOOTHING = 0.1
|
MAX_SMOOTHING = 0.1
|
||||||
|
|
||||||
KLIPPAIN_COLORS = {
|
KLIPPAIN_COLORS = {
|
||||||
"purple": "#70088C",
|
'purple': '#70088C',
|
||||||
"orange": "#FF8D32",
|
'orange': '#FF8D32',
|
||||||
"dark_purple": "#150140",
|
'dark_purple': '#150140',
|
||||||
"dark_orange": "#F24130",
|
'dark_orange': '#F24130',
|
||||||
"red_pink": "#F2055C"
|
'red_pink': '#F2055C',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -44,6 +47,7 @@ KLIPPAIN_COLORS = {
|
|||||||
# Computation
|
# Computation
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
|
||||||
# Find the best shaper parameters using Klipper's official algorithm selection with
|
# Find the best shaper parameters using Klipper's official algorithm selection with
|
||||||
# a proper precomputed damping ratio (zeta) and using the configured printer SQV value
|
# a proper precomputed damping ratio (zeta) and using the configured printer SQV value
|
||||||
def calibrate_shaper(datas, max_smoothing, scv, max_freq):
|
def calibrate_shaper(datas, max_smoothing, scv, max_freq):
|
||||||
@@ -54,22 +58,36 @@ def calibrate_shaper(datas, max_smoothing, scv, max_freq):
|
|||||||
fr, zeta, _, _ = compute_mechanical_parameters(calibration_data.psd_sum, calibration_data.freq_bins)
|
fr, zeta, _, _ = compute_mechanical_parameters(calibration_data.psd_sum, calibration_data.freq_bins)
|
||||||
|
|
||||||
# If the damping ratio computation fail, we use Klipper default value instead
|
# If the damping ratio computation fail, we use Klipper default value instead
|
||||||
if zeta is None: zeta = 0.1
|
if zeta is None:
|
||||||
|
zeta = 0.1
|
||||||
|
|
||||||
compat = False
|
compat = False
|
||||||
try:
|
try:
|
||||||
shaper, all_shapers = helper.find_best_shaper(
|
shaper, all_shapers = helper.find_best_shaper(
|
||||||
calibration_data, shapers=None, damping_ratio=zeta,
|
calibration_data,
|
||||||
scv=scv, shaper_freqs=None, max_smoothing=max_smoothing,
|
shapers=None,
|
||||||
test_damping_ratios=None, max_freq=max_freq,
|
damping_ratio=zeta,
|
||||||
logger=print_with_c_locale)
|
scv=scv,
|
||||||
|
shaper_freqs=None,
|
||||||
|
max_smoothing=max_smoothing,
|
||||||
|
test_damping_ratios=None,
|
||||||
|
max_freq=max_freq,
|
||||||
|
logger=print_with_c_locale,
|
||||||
|
)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
print_with_c_locale("[WARNING] You seem to be using an older version of Klipper that is not compatible with all the latest Shake&Tune features!")
|
print_with_c_locale(
|
||||||
print_with_c_locale("Shake&Tune now runs in compatibility mode: be aware that the results may be slightly off, since the real damping ratio cannot be used to create the filter recommendations")
|
'[WARNING] You seem to be using an older version of Klipper that is not compatible with all the latest Shake&Tune features!'
|
||||||
|
)
|
||||||
|
print_with_c_locale(
|
||||||
|
'Shake&Tune now runs in compatibility mode: be aware that the results may be slightly off, since the real damping ratio cannot be used to create the filter recommendations'
|
||||||
|
)
|
||||||
compat = True
|
compat = True
|
||||||
shaper, all_shapers = helper.find_best_shaper(calibration_data, max_smoothing, print_with_c_locale)
|
shaper, all_shapers = helper.find_best_shaper(calibration_data, max_smoothing, print_with_c_locale)
|
||||||
|
|
||||||
print_with_c_locale("\n-> Recommended shaper is %s @ %.1f Hz (when using a square corner velocity of %.1f and a damping ratio of %.3f)" % (shaper.name.upper(), shaper.freq, scv, zeta))
|
print_with_c_locale(
|
||||||
|
'\n-> Recommended shaper is %s @ %.1f Hz (when using a square corner velocity of %.1f and a damping ratio of %.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
|
||||||
|
|
||||||
@@ -78,7 +96,10 @@ def calibrate_shaper(datas, max_smoothing, scv, max_freq):
|
|||||||
# Graphing
|
# Graphing
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def plot_freq_response(ax, calibration_data, shapers, performance_shaper, peaks, peaks_freqs, peaks_threshold, fr, zeta, max_freq):
|
|
||||||
|
def plot_freq_response(
|
||||||
|
ax, calibration_data, shapers, performance_shaper, peaks, peaks_freqs, peaks_threshold, fr, zeta, max_freq
|
||||||
|
):
|
||||||
freqs = calibration_data.freqs
|
freqs = calibration_data.freqs
|
||||||
psd = calibration_data.psd_sum
|
psd = calibration_data.psd_sum
|
||||||
px = calibration_data.psd_x
|
px = calibration_data.psd_x
|
||||||
@@ -100,7 +121,7 @@ def plot_freq_response(ax, calibration_data, shapers, performance_shaper, peaks,
|
|||||||
|
|
||||||
ax.xaxis.set_minor_locator(matplotlib.ticker.MultipleLocator(5))
|
ax.xaxis.set_minor_locator(matplotlib.ticker.MultipleLocator(5))
|
||||||
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='y', 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')
|
||||||
|
|
||||||
@@ -115,21 +136,27 @@ def plot_freq_response(ax, calibration_data, shapers, performance_shaper, peaks,
|
|||||||
# Draw the shappers curves and add their specific parameters in the legend
|
# Draw the shappers curves and add their specific parameters in the legend
|
||||||
# This adds also a way to find the best shaper with a low level of vibrations (with a resonable level of smoothing)
|
# This adds also a way to find the best shaper with a low level of vibrations (with a resonable level of smoothing)
|
||||||
for shaper in shapers:
|
for shaper in shapers:
|
||||||
shaper_max_accel = round(shaper.max_accel / 100.) * 100.
|
shaper_max_accel = round(shaper.max_accel / 100.0) * 100.0
|
||||||
label = "%s (%.1f Hz, vibr=%.1f%%, sm~=%.2f, accel<=%.f)" % (
|
label = '%s (%.1f Hz, vibr=%.1f%%, sm~=%.2f, accel<=%.f)' % (
|
||||||
shaper.name.upper(), shaper.freq,
|
shaper.name.upper(),
|
||||||
shaper.vibrs * 100., shaper.smoothing,
|
shaper.freq,
|
||||||
shaper_max_accel)
|
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 performance shaper
|
# Get the performance shaper
|
||||||
if shaper.name == performance_shaper:
|
if shaper.name == performance_shaper:
|
||||||
performance_shaper_freq = shaper.freq
|
performance_shaper_freq = shaper.freq
|
||||||
performance_shaper_vibr = shaper.vibrs * 100.
|
performance_shaper_vibr = shaper.vibrs * 100.0
|
||||||
performance_shaper_vals = shaper.vals
|
performance_shaper_vals = shaper.vals
|
||||||
|
|
||||||
# Get the low vibration shaper
|
# Get the low vibration shaper
|
||||||
if (shaper.vibrs * 100 < lowvib_shaper_vibrs or (shaper.vibrs * 100 == lowvib_shaper_vibrs and shaper_max_accel > lowvib_shaper_accel)) and shaper.smoothing < MAX_SMOOTHING:
|
if (
|
||||||
|
shaper.vibrs * 100 < lowvib_shaper_vibrs
|
||||||
|
or (shaper.vibrs * 100 == lowvib_shaper_vibrs and shaper_max_accel > lowvib_shaper_accel)
|
||||||
|
) and shaper.smoothing < MAX_SMOOTHING:
|
||||||
lowvib_shaper_accel = shaper_max_accel
|
lowvib_shaper_accel = shaper_max_accel
|
||||||
lowvib_shaper = shaper.name
|
lowvib_shaper = shaper.name
|
||||||
lowvib_shaper_freq = shaper.freq
|
lowvib_shaper_freq = shaper.freq
|
||||||
@@ -140,21 +167,45 @@ def plot_freq_response(ax, calibration_data, shapers, performance_shaper, peaks,
|
|||||||
# and the other one is the custom "low vibration" recommendation that looks for a suitable shaper that doesn't have excessive
|
# and the other one is the custom "low vibration" recommendation that looks for a suitable shaper that doesn't have excessive
|
||||||
# smoothing and that have a lower vibration level. If both recommendation are the same shaper, or if no suitable "low
|
# smoothing and that have a lower vibration level. If both recommendation are the same shaper, or if no suitable "low
|
||||||
# vibration" shaper is found, then only a single line as the "best shaper" recommendation is added to the legend
|
# vibration" shaper is found, then only a single line as the "best shaper" recommendation is added to the legend
|
||||||
if lowvib_shaper != None and lowvib_shaper != performance_shaper and lowvib_shaper_vibrs <= performance_shaper_vibr:
|
if (
|
||||||
ax2.plot([], [], ' ', label="Recommended performance shaper: %s @ %.1f Hz" % (performance_shaper.upper(), performance_shaper_freq))
|
lowvib_shaper is not None
|
||||||
ax.plot(freqs, psd * performance_shaper_vals, label='With %s applied' % (performance_shaper.upper()), color='cyan')
|
and lowvib_shaper != performance_shaper
|
||||||
ax2.plot([], [], ' ', label="Recommended low vibrations shaper: %s @ %.1f Hz" % (lowvib_shaper.upper(), lowvib_shaper_freq))
|
and lowvib_shaper_vibrs <= performance_shaper_vibr
|
||||||
|
):
|
||||||
|
ax2.plot(
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
' ',
|
||||||
|
label='Recommended performance shaper: %s @ %.1f Hz'
|
||||||
|
% (performance_shaper.upper(), performance_shaper_freq),
|
||||||
|
)
|
||||||
|
ax.plot(
|
||||||
|
freqs, psd * performance_shaper_vals, label='With %s applied' % (performance_shaper.upper()), color='cyan'
|
||||||
|
)
|
||||||
|
ax2.plot(
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
' ',
|
||||||
|
label='Recommended low vibrations shaper: %s @ %.1f Hz' % (lowvib_shaper.upper(), lowvib_shaper_freq),
|
||||||
|
)
|
||||||
ax.plot(freqs, psd * lowvib_shaper_vals, label='With %s applied' % (lowvib_shaper.upper()), color='lime')
|
ax.plot(freqs, psd * lowvib_shaper_vals, label='With %s applied' % (lowvib_shaper.upper()), color='lime')
|
||||||
else:
|
else:
|
||||||
ax2.plot([], [], ' ', label="Recommended best shaper: %s @ %.1f Hz" % (performance_shaper.upper(), performance_shaper_freq))
|
ax2.plot(
|
||||||
ax.plot(freqs, psd * performance_shaper_vals, label='With %s applied' % (performance_shaper.upper()), color='cyan')
|
[],
|
||||||
|
[],
|
||||||
|
' ',
|
||||||
|
label='Recommended best shaper: %s @ %.1f Hz' % (performance_shaper.upper(), performance_shaper_freq),
|
||||||
|
)
|
||||||
|
ax.plot(
|
||||||
|
freqs, psd * performance_shaper_vals, label='With %s applied' % (performance_shaper.upper()), 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='Estimated damping ratio (ζ): %.3f' % (zeta))
|
||||||
|
|
||||||
# Draw the detected peaks and name them
|
# Draw the detected peaks and name them
|
||||||
# This also draw the detection threshold and warning threshold (aka "effect zone")
|
# This also draw the detection threshold and warning threshold (aka "effect zone")
|
||||||
ax.plot(peaks_freqs, psd[peaks], "x", color='black', markersize=8)
|
ax.plot(peaks_freqs, psd[peaks], 'x', color='black', markersize=8)
|
||||||
for idx, peak in enumerate(peaks):
|
for idx, peak in enumerate(peaks):
|
||||||
if psd[peak] > peaks_threshold[1]:
|
if psd[peak] > peaks_threshold[1]:
|
||||||
fontcolor = 'red'
|
fontcolor = 'red'
|
||||||
@@ -162,16 +213,28 @@ def plot_freq_response(ax, calibration_data, shapers, performance_shaper, peaks,
|
|||||||
else:
|
else:
|
||||||
fontcolor = 'black'
|
fontcolor = 'black'
|
||||||
fontweight = 'normal'
|
fontweight = 'normal'
|
||||||
ax.annotate(f"{idx+1}", (freqs[peak], psd[peak]),
|
ax.annotate(
|
||||||
textcoords="offset points", xytext=(8, 5),
|
f'{idx+1}',
|
||||||
ha='left', fontsize=13, color=fontcolor, weight=fontweight)
|
(freqs[peak], psd[peak]),
|
||||||
|
textcoords='offset points',
|
||||||
|
xytext=(8, 5),
|
||||||
|
ha='left',
|
||||||
|
fontsize=13,
|
||||||
|
color=fontcolor,
|
||||||
|
weight=fontweight,
|
||||||
|
)
|
||||||
ax.axhline(y=peaks_threshold[0], color='black', linestyle='--', linewidth=0.5)
|
ax.axhline(y=peaks_threshold[0], color='black', linestyle='--', linewidth=0.5)
|
||||||
ax.axhline(y=peaks_threshold[1], color='black', linestyle='--', linewidth=0.5)
|
ax.axhline(y=peaks_threshold[1], color='black', linestyle='--', linewidth=0.5)
|
||||||
ax.fill_between(freqs, 0, peaks_threshold[0], color='green', alpha=0.15, label='Relax Region')
|
ax.fill_between(freqs, 0, peaks_threshold[0], color='green', alpha=0.15, label='Relax Region')
|
||||||
ax.fill_between(freqs, peaks_threshold[0], peaks_threshold[1], color='orange', alpha=0.2, label='Warning Region')
|
ax.fill_between(freqs, peaks_threshold[0], peaks_threshold[1], color='orange', alpha=0.2, label='Warning Region')
|
||||||
|
|
||||||
# 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("Axis Frequency Profile (ω0=%.1fHz, ζ=%.3f)" % (fr, zeta), fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
|
ax.set_title(
|
||||||
|
'Axis Frequency Profile (ω0=%.1fHz, ζ=%.3f)' % (fr, zeta),
|
||||||
|
fontsize=14,
|
||||||
|
color=KLIPPAIN_COLORS['dark_orange'],
|
||||||
|
weight='bold',
|
||||||
|
)
|
||||||
ax.legend(loc='upper left', prop=fontP)
|
ax.legend(loc='upper left', prop=fontP)
|
||||||
ax2.legend(loc='upper right', prop=fontP)
|
ax2.legend(loc='upper right', prop=fontP)
|
||||||
|
|
||||||
@@ -181,7 +244,7 @@ def plot_freq_response(ax, calibration_data, shapers, performance_shaper, peaks,
|
|||||||
# Plot a time-frequency spectrogram to see how the system respond over time during the
|
# Plot a time-frequency spectrogram to see how the system respond over time during the
|
||||||
# resonnance test. This can highlight hidden spots from the standard PSD graph from other harmonics
|
# resonnance test. This can highlight hidden spots from the standard PSD graph from other harmonics
|
||||||
def plot_spectrogram(ax, t, bins, pdata, peaks, max_freq):
|
def plot_spectrogram(ax, t, bins, pdata, peaks, max_freq):
|
||||||
ax.set_title("Time-Frequency Spectrogram", fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
|
ax.set_title('Time-Frequency Spectrogram', fontsize=14, color=KLIPPAIN_COLORS['dark_orange'], weight='bold')
|
||||||
|
|
||||||
# We need to normalize the data to get a proper signal on the spectrogram
|
# We need to normalize the data to get a proper signal on the spectrogram
|
||||||
# However, while using "LogNorm" provide too much background noise, using
|
# However, while using "LogNorm" provide too much background noise, using
|
||||||
@@ -194,9 +257,17 @@ def plot_spectrogram(ax, t, bins, pdata, peaks, max_freq):
|
|||||||
# save ~150-200MB of RAM during the "fig.savefig" operation.
|
# save ~150-200MB of RAM during the "fig.savefig" operation.
|
||||||
cm = 'inferno'
|
cm = 'inferno'
|
||||||
norm = matplotlib.colors.LogNorm(vmin=vmin_value)
|
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.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., max_freq])
|
ax.set_xlim([0.0, max_freq])
|
||||||
ax.set_ylabel('Time (s)')
|
ax.set_ylabel('Time (s)')
|
||||||
ax.set_xlabel('Frequency (Hz)')
|
ax.set_xlabel('Frequency (Hz)')
|
||||||
|
|
||||||
@@ -204,9 +275,16 @@ def plot_spectrogram(ax, t, bins, pdata, peaks, max_freq):
|
|||||||
if peaks is not None:
|
if peaks is not None:
|
||||||
for idx, peak in enumerate(peaks):
|
for idx, peak in enumerate(peaks):
|
||||||
ax.axvline(peak, color='cyan', linestyle='dotted', linewidth=1)
|
ax.axvline(peak, color='cyan', linestyle='dotted', linewidth=1)
|
||||||
ax.annotate(f"Peak {idx+1}", (peak, bins[-1]*0.9),
|
ax.annotate(
|
||||||
textcoords="data", color='cyan', rotation=90, fontsize=10,
|
f'Peak {idx+1}',
|
||||||
verticalalignment='top', horizontalalignment='right')
|
(peak, bins[-1] * 0.9),
|
||||||
|
textcoords='data',
|
||||||
|
color='cyan',
|
||||||
|
rotation=90,
|
||||||
|
fontsize=10,
|
||||||
|
verticalalignment='top',
|
||||||
|
horizontalalignment='right',
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -215,7 +293,8 @@ def plot_spectrogram(ax, t, bins, pdata, peaks, max_freq):
|
|||||||
# Startup and main routines
|
# Startup and main routines
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def shaper_calibration(lognames, klipperdir="~/klipper", max_smoothing=None, scv=5. , max_freq=200.):
|
|
||||||
|
def shaper_calibration(lognames, klipperdir='~/klipper', max_smoothing=None, scv=5.0, max_freq=200.0, st_version=None):
|
||||||
set_locale()
|
set_locale()
|
||||||
global shaper_calibrate
|
global shaper_calibrate
|
||||||
shaper_calibrate = setup_klipper_import(klipperdir)
|
shaper_calibrate = setup_klipper_import(klipperdir)
|
||||||
@@ -223,10 +302,12 @@ def shaper_calibration(lognames, klipperdir="~/klipper", max_smoothing=None, scv
|
|||||||
# Parse data
|
# Parse data
|
||||||
datas = [parse_log(fn) for fn in lognames]
|
datas = [parse_log(fn) for fn in lognames]
|
||||||
if len(datas) > 1:
|
if len(datas) > 1:
|
||||||
print_with_c_locale("Warning: incorrect number of .csv files detected. Only the first one will be used!")
|
print_with_c_locale('Warning: incorrect number of .csv files detected. Only the first one will be used!')
|
||||||
|
|
||||||
# Compute shapers, PSD outputs and spectrogram
|
# Compute shapers, PSD outputs and spectrogram
|
||||||
performance_shaper, shapers, calibration_data, fr, zeta, compat = calibrate_shaper(datas[0], max_smoothing, scv, max_freq)
|
performance_shaper, shapers, calibration_data, fr, zeta, compat = calibrate_shaper(
|
||||||
|
datas[0], max_smoothing, scv, max_freq
|
||||||
|
)
|
||||||
pdata, bins, t = compute_spectrogram(datas[0])
|
pdata, bins, t = compute_spectrogram(datas[0])
|
||||||
del datas
|
del datas
|
||||||
|
|
||||||
@@ -241,42 +322,51 @@ def shaper_calibration(lognames, klipperdir="~/klipper", max_smoothing=None, scv
|
|||||||
# Peak detection algorithm
|
# Peak detection algorithm
|
||||||
peaks_threshold = [
|
peaks_threshold = [
|
||||||
PEAKS_DETECTION_THRESHOLD * calibration_data.psd_sum.max(),
|
PEAKS_DETECTION_THRESHOLD * calibration_data.psd_sum.max(),
|
||||||
PEAKS_EFFECT_THRESHOLD * calibration_data.psd_sum.max()
|
PEAKS_EFFECT_THRESHOLD * calibration_data.psd_sum.max(),
|
||||||
]
|
]
|
||||||
num_peaks, peaks, peaks_freqs = detect_peaks(calibration_data.psd_sum, calibration_data.freqs, peaks_threshold[0])
|
num_peaks, peaks, peaks_freqs = detect_peaks(calibration_data.psd_sum, calibration_data.freqs, peaks_threshold[0])
|
||||||
|
|
||||||
# Print the peaks info in the console
|
# Print the peaks info in the console
|
||||||
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])
|
||||||
print_with_c_locale("\nPeaks detected on the graph: %d @ %s Hz (%d above effect threshold)" % (num_peaks, ", ".join(map(str, peak_freqs_formated)), num_peaks_above_effect_threshold))
|
print_with_c_locale(
|
||||||
|
'\nPeaks detected on the graph: %d @ %s Hz (%d above effect threshold)'
|
||||||
|
% (num_peaks, ', '.join(map(str, peak_freqs_formated)), num_peaks_above_effect_threshold)
|
||||||
|
)
|
||||||
|
|
||||||
# Create graph layout
|
# Create graph layout
|
||||||
fig, (ax1, ax2) = plt.subplots(2, 1, gridspec_kw={
|
fig, (ax1, ax2) = plt.subplots(
|
||||||
'height_ratios':[4, 3],
|
2,
|
||||||
'bottom':0.050,
|
1,
|
||||||
'top':0.890,
|
gridspec_kw={
|
||||||
'left':0.085,
|
'height_ratios': [4, 3],
|
||||||
'right':0.966,
|
'bottom': 0.050,
|
||||||
'hspace':0.169,
|
'top': 0.890,
|
||||||
'wspace':0.200
|
'left': 0.085,
|
||||||
})
|
'right': 0.966,
|
||||||
|
'hspace': 0.169,
|
||||||
|
'wspace': 0.200,
|
||||||
|
},
|
||||||
|
)
|
||||||
fig.set_size_inches(8.3, 11.6)
|
fig.set_size_inches(8.3, 11.6)
|
||||||
|
|
||||||
# Add a title with some test info
|
# Add a title with some test info
|
||||||
title_line1 = "INPUT SHAPER CALIBRATION TOOL"
|
title_line1 = 'INPUT SHAPER CALIBRATION TOOL'
|
||||||
fig.text(0.12, 0.965, title_line1, ha='left', va='bottom', fontsize=20, color=KLIPPAIN_COLORS['purple'], weight='bold')
|
fig.text(
|
||||||
|
0.12, 0.965, title_line1, ha='left', va='bottom', fontsize=20, color=KLIPPAIN_COLORS['purple'], weight='bold'
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
filename_parts = (lognames[0].split('/')[-1]).split('_')
|
filename_parts = (lognames[0].split('/')[-1]).split('_')
|
||||||
dt = datetime.strptime(f"{filename_parts[1]} {filename_parts[2]}", "%Y%m%d %H%M%S")
|
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_line2 = dt.strftime('%x %X') + ' -- ' + filename_parts[3].upper().split('.')[0] + ' axis'
|
||||||
if compat:
|
if compat:
|
||||||
title_line3: '| Compatibility mode with older Klipper,'
|
title_line3 = '| Compatibility mode with older Klipper,'
|
||||||
title_line4: '| and no custom S&T parameters are used!'
|
title_line4 = '| and no custom S&T parameters are used!'
|
||||||
else:
|
else:
|
||||||
title_line3 = '| Square corner velocity: ' + str(scv) + 'mm/s'
|
title_line3 = '| Square corner velocity: ' + str(scv) + 'mm/s'
|
||||||
title_line4 = '| Max allowed smoothing: ' + str(max_smoothing)
|
title_line4 = '| Max allowed smoothing: ' + str(max_smoothing)
|
||||||
except:
|
except Exception:
|
||||||
print_with_c_locale("Warning: CSV filename look to be different than expected (%s)" % (lognames[0]))
|
print_with_c_locale('Warning: CSV filename look to be different than expected (%s)' % (lognames[0]))
|
||||||
title_line2 = lognames[0].split('/')[-1]
|
title_line2 = lognames[0].split('/')[-1]
|
||||||
title_line3 = ''
|
title_line3 = ''
|
||||||
title_line4 = ''
|
title_line4 = ''
|
||||||
@@ -285,7 +375,9 @@ def shaper_calibration(lognames, klipperdir="~/klipper", max_smoothing=None, scv
|
|||||||
fig.text(0.58, 0.946, title_line4, ha='left', va='top', fontsize=10, color=KLIPPAIN_COLORS['dark_purple'])
|
fig.text(0.58, 0.946, title_line4, ha='left', va='top', fontsize=10, color=KLIPPAIN_COLORS['dark_purple'])
|
||||||
|
|
||||||
# Plot the graphs
|
# Plot the graphs
|
||||||
plot_freq_response(ax1, calibration_data, shapers, performance_shaper, peaks, peaks_freqs, peaks_threshold, fr, zeta, max_freq)
|
plot_freq_response(
|
||||||
|
ax1, calibration_data, shapers, performance_shaper, peaks, peaks_freqs, peaks_threshold, fr, zeta, max_freq
|
||||||
|
)
|
||||||
plot_spectrogram(ax2, t, bins, pdata, peaks_freqs, max_freq)
|
plot_spectrogram(ax2, t, bins, pdata, peaks_freqs, max_freq)
|
||||||
|
|
||||||
# Adding a small Klippain logo to the top left corner of the figure
|
# Adding a small Klippain logo to the top left corner of the figure
|
||||||
@@ -294,8 +386,7 @@ def shaper_calibration(lognames, klipperdir="~/klipper", max_smoothing=None, scv
|
|||||||
ax_logo.axis('off')
|
ax_logo.axis('off')
|
||||||
|
|
||||||
# Adding Shake&Tune version in the top right corner
|
# Adding Shake&Tune version in the top right corner
|
||||||
st_version = get_git_version()
|
if st_version != 'unknown':
|
||||||
if st_version is not None:
|
|
||||||
fig.text(0.995, 0.985, st_version, ha='right', va='bottom', fontsize=8, color=KLIPPAIN_COLORS['purple'])
|
fig.text(0.995, 0.985, st_version, ha='right', va='bottom', fontsize=8, color=KLIPPAIN_COLORS['purple'])
|
||||||
|
|
||||||
return fig
|
return fig
|
||||||
@@ -303,25 +394,24 @@ def shaper_calibration(lognames, klipperdir="~/klipper", max_smoothing=None, scv
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
# Parse command-line arguments
|
# Parse command-line arguments
|
||||||
usage = "%prog [options] <logs>"
|
usage = '%prog [options] <logs>'
|
||||||
opts = optparse.OptionParser(usage)
|
opts = optparse.OptionParser(usage)
|
||||||
opts.add_option("-o", "--output", type="string", dest="output",
|
opts.add_option('-o', '--output', type='string', dest='output', default=None, help='filename of output graph')
|
||||||
default=None, help="filename of output graph")
|
opts.add_option('-f', '--max_freq', type='float', default=200.0, help='maximum frequency to graph')
|
||||||
opts.add_option("-f", "--max_freq", type="float", default=200.,
|
opts.add_option('-s', '--max_smoothing', type='float', default=None, help='maximum shaper smoothing to allow')
|
||||||
help="maximum frequency to graph")
|
opts.add_option(
|
||||||
opts.add_option("-s", "--max_smoothing", type="float", default=None,
|
'--scv', '--square_corner_velocity', type='float', dest='scv', default=5.0, help='square corner velocity'
|
||||||
help="maximum shaper smoothing to allow")
|
)
|
||||||
opts.add_option("--scv", "--square_corner_velocity", type="float",
|
opts.add_option(
|
||||||
dest="scv", default=5., help="square corner velocity")
|
'-k', '--klipper_dir', type='string', dest='klipperdir', default='~/klipper', help='main klipper directory'
|
||||||
opts.add_option("-k", "--klipper_dir", type="string", dest="klipperdir",
|
)
|
||||||
default="~/klipper", help="main klipper directory")
|
|
||||||
options, args = opts.parse_args()
|
options, args = opts.parse_args()
|
||||||
if len(args) < 1:
|
if len(args) < 1:
|
||||||
opts.error("Incorrect number of arguments")
|
opts.error('Incorrect number of arguments')
|
||||||
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.max_smoothing is not None and options.max_smoothing < 0.05:
|
if options.max_smoothing is not None and options.max_smoothing < 0.05:
|
||||||
opts.error("Too small max_smoothing specified (must be at least 0.05)")
|
opts.error('Too small max_smoothing specified (must be at least 0.05)')
|
||||||
|
|
||||||
fig = shaper_calibration(args, options.klipperdir, options.max_smoothing, options.scv, options.max_freq)
|
fig = shaper_calibration(args, options.klipperdir, options.max_smoothing, options.scv, options.max_freq)
|
||||||
fig.savefig(options.output, dpi=150)
|
fig.savefig(options.output, dpi=150)
|
||||||
415
K-ShakeTune/scripts/graph_vibrations.py → src/graph_creators/graph_vibrations.py
Executable file → Normal file
415
K-ShakeTune/scripts/graph_vibrations.py → src/graph_creators/graph_vibrations.py
Executable file → Normal file
@@ -5,40 +5,44 @@
|
|||||||
##################################################
|
##################################################
|
||||||
# Written by Frix_x#0161 #
|
# 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 math
|
||||||
import optparse, matplotlib, re, os
|
import optparse
|
||||||
from datetime import datetime
|
import os
|
||||||
|
import re
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import numpy as np
|
from datetime import datetime
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import matplotlib.font_manager, matplotlib.ticker, matplotlib.gridspec
|
|
||||||
|
|
||||||
|
import matplotlib
|
||||||
|
import matplotlib.font_manager
|
||||||
|
import matplotlib.gridspec
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import matplotlib.ticker
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
matplotlib.use('Agg')
|
matplotlib.use('Agg')
|
||||||
|
|
||||||
from locale_utils import set_locale, print_with_c_locale
|
from ..helpers.common_func import (
|
||||||
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
|
compute_mechanical_parameters,
|
||||||
|
detect_peaks,
|
||||||
|
identify_low_energy_zones,
|
||||||
|
parse_log,
|
||||||
|
setup_klipper_import,
|
||||||
|
)
|
||||||
|
from ..helpers.locale_utils import print_with_c_locale, set_locale
|
||||||
|
|
||||||
PEAKS_DETECTION_THRESHOLD = 0.05
|
PEAKS_DETECTION_THRESHOLD = 0.05
|
||||||
PEAKS_RELATIVE_HEIGHT_THRESHOLD = 0.04
|
PEAKS_RELATIVE_HEIGHT_THRESHOLD = 0.04
|
||||||
CURVE_SIMILARITY_SIGMOID_K = 0.5
|
CURVE_SIMILARITY_SIGMOID_K = 0.5
|
||||||
SPEEDS_VALLEY_DETECTION_THRESHOLD = 0.7 # 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
|
SPEEDS_AROUND_PEAK_DELETION = 3 # to delete +-3mm/s around a peak
|
||||||
ANGLES_VALLEY_DETECTION_THRESHOLD = 1.1 # Lower is more sensitive
|
ANGLES_VALLEY_DETECTION_THRESHOLD = 1.1 # Lower is more sensitive
|
||||||
|
|
||||||
KLIPPAIN_COLORS = {
|
KLIPPAIN_COLORS = {
|
||||||
"purple": "#70088C",
|
'purple': '#70088C',
|
||||||
"orange": "#FF8D32",
|
'orange': '#FF8D32',
|
||||||
"dark_purple": "#150140",
|
'dark_purple': '#150140',
|
||||||
"dark_orange": "#F24130",
|
'dark_orange': '#F24130',
|
||||||
"red_pink": "#F2055C"
|
'red_pink': '#F2055C',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -46,6 +50,7 @@ KLIPPAIN_COLORS = {
|
|||||||
# Computation
|
# Computation
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
|
||||||
# Call to the official Klipper input shaper object to do the PSD computation
|
# Call to the official Klipper input shaper object to do the PSD computation
|
||||||
def calc_freq_response(data):
|
def calc_freq_response(data):
|
||||||
helper = shaper_calibrate.ShaperCalibrate(printer=None)
|
helper = shaper_calibrate.ShaperCalibrate(printer=None)
|
||||||
@@ -54,7 +59,10 @@ def calc_freq_response(data):
|
|||||||
|
|
||||||
# Calculate motor frequency profiles based on the measured Power Spectral Density (PSD) measurements for the machine kinematics
|
# 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
|
# 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 = {}
|
motor_profiles = {}
|
||||||
weighted_sum_profiles = np.zeros_like(freqs)
|
weighted_sum_profiles = np.zeros_like(freqs)
|
||||||
total_weight = 0
|
total_weight = 0
|
||||||
@@ -67,8 +75,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')
|
motor_profiles[angle] = np.convolve(sum_curve / len(psds[angle]), conv_filter, mode='same')
|
||||||
|
|
||||||
# Calculate weights
|
# 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
|
angle_energy = (
|
||||||
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
|
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
|
total_angle_weight = angle_energy * curve_area
|
||||||
|
|
||||||
# Update weighted sum profiles to get the global motor profile
|
# Update weighted sum profiles to get the global motor profile
|
||||||
@@ -85,19 +97,24 @@ 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
|
# 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
|
# 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.
|
# 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
|
# 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_speeds = np.linspace(min(measured_speeds), max(measured_speeds), len(measured_speeds) * 6)
|
||||||
spectrum_vibrations = np.zeros((len(spectrum_angles), len(spectrum_speeds)))
|
spectrum_vibrations = np.zeros((len(spectrum_angles), len(spectrum_speeds)))
|
||||||
|
|
||||||
def get_interpolated_vibrations(data, speed, 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]
|
lower_speed = speeds[idx - 1]
|
||||||
upper_speed = speeds[idx]
|
upper_speed = speeds[idx]
|
||||||
lower_vibrations = data.get(lower_speed, 0)
|
lower_vibrations = data.get(lower_speed, 0)
|
||||||
upper_vibrations = data.get(upper_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
|
# Precompute trigonometric values and constant before the loop
|
||||||
angle_radians = np.deg2rad(spectrum_angles)
|
angle_radians = np.deg2rad(spectrum_angles)
|
||||||
@@ -108,10 +125,10 @@ def compute_dir_speed_spectrogram(measured_speeds, data, kinematics="cartesian",
|
|||||||
# Compute the spectrum vibrations for each angle and speed combination
|
# 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)):
|
||||||
for target_speed_idx, target_speed in enumerate(spectrum_speeds):
|
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_1 = np.abs(target_speed * cos_val)
|
||||||
speed_2 = np.abs(target_speed * sin_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_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)
|
speed_2 = np.abs(target_speed * (cos_val - sin_val) * sqrt_2_inv)
|
||||||
|
|
||||||
@@ -129,7 +146,7 @@ def compute_angle_powers(spectrogram_data):
|
|||||||
# the array to start and end of it to smooth transitions when doing the convolution
|
# 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
|
# 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]])
|
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]
|
return convolved_extended[9:-9]
|
||||||
|
|
||||||
@@ -149,6 +166,7 @@ def compute_speed_powers(spectrogram_data, smoothing_window=15):
|
|||||||
# utility function to pad and smooth the data avoiding edge effects
|
# utility function to pad and smooth the data avoiding edge effects
|
||||||
conv_filter = np.ones(smoothing_window) / smoothing_window
|
conv_filter = np.ones(smoothing_window) / smoothing_window
|
||||||
window = int(smoothing_window / 2)
|
window = int(smoothing_window / 2)
|
||||||
|
|
||||||
def pad_and_smooth(data):
|
def pad_and_smooth(data):
|
||||||
data_padded = np.pad(data, (window,), mode='edge')
|
data_padded = np.pad(data, (window,), mode='edge')
|
||||||
smoothed_data = np.convolve(data_padded, conv_filter, mode='valid')
|
smoothed_data = np.convolve(data_padded, conv_filter, mode='valid')
|
||||||
@@ -207,7 +225,10 @@ def filter_and_split_ranges(all_speeds, good_speeds, peak_speed_indices, deletio
|
|||||||
|
|
||||||
# This function allow the computation of a symmetry score that reflect the spectrogram apparent symmetry between
|
# 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
|
# 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)
|
total_spectrogram_angles = len(all_angles)
|
||||||
half_spectrogram_angles = total_spectrogram_angles // 2
|
half_spectrogram_angles = total_spectrogram_angles // 2
|
||||||
|
|
||||||
@@ -220,8 +241,8 @@ def compute_symmetry_analysis(all_angles, spectrogram_data, measured_angles=[0,
|
|||||||
half_segment_length = half_spectrogram_angles // 2
|
half_segment_length = half_spectrogram_angles // 2
|
||||||
|
|
||||||
# Slice out the two segments of the spectrogram and flatten them for comparison
|
# 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_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_2_flattened = extended_spectrogram[split_index : split_index + half_segment_length].flatten()
|
||||||
|
|
||||||
# Compute the correlation coefficient between the two segments of spectrogram
|
# Compute the correlation coefficient between the two segments of spectrogram
|
||||||
correlation = np.corrcoef(segment_1_flattened, segment_2_flattened)[0, 1]
|
correlation = np.corrcoef(segment_1_flattened, segment_2_flattened)[0, 1]
|
||||||
@@ -234,10 +255,11 @@ def compute_symmetry_analysis(all_angles, spectrogram_data, measured_angles=[0,
|
|||||||
# Graphing
|
# Graphing
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
|
||||||
def plot_angle_profile_polar(ax, angles, angles_powers, low_energy_zones, symmetry_factor):
|
def plot_angle_profile_polar(ax, angles, angles_powers, low_energy_zones, symmetry_factor):
|
||||||
angles_radians = np.deg2rad(angles)
|
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_zero_location('E')
|
||||||
ax.set_theta_direction(1)
|
ax.set_theta_direction(1)
|
||||||
|
|
||||||
@@ -246,14 +268,38 @@ def plot_angle_profile_polar(ax, angles, angles_powers, low_energy_zones, symmet
|
|||||||
ax.set_xlim([0, np.deg2rad(360)])
|
ax.set_xlim([0, np.deg2rad(360)])
|
||||||
ymax = angles_powers.max() * 1.05
|
ymax = angles_powers.max() * 1.05
|
||||||
ax.set_ylim([0, ymax])
|
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):
|
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(
|
||||||
ax.axvline(angles_radians[end], angles_powers[end]/ymax, color=KLIPPAIN_COLORS['red_pink'], linestyle='dotted', linewidth=1.5)
|
angles_radians[start],
|
||||||
ax.fill_between(angles_radians[start:end], angles_powers[start:end], angles_powers.max() * 1.05, color='green', alpha=0.2)
|
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.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||||
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||||
@@ -267,8 +313,19 @@ def plot_angle_profile_polar(ax, angles, angles_powers, low_energy_zones, symmet
|
|||||||
|
|
||||||
return
|
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_xlabel('Speed (mm/s)')
|
||||||
ax.set_ylabel('Energy')
|
ax.set_ylabel('Energy')
|
||||||
ax2 = ax.twinx()
|
ax2 = ax.twinx()
|
||||||
@@ -277,7 +334,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_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_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='--')
|
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_xlim([all_speeds.min(), all_speeds.max()])
|
||||||
ax.set_ylim([0, sp_max_energy.max() * 1.15])
|
ax.set_ylim([0, sp_max_energy.max() * 1.15])
|
||||||
@@ -286,17 +349,32 @@ def plot_global_speed_profile(ax, all_speeds, sp_min_energy, sp_max_energy, sp_v
|
|||||||
y2max = vibration_metric.max() * 1.07
|
y2max = vibration_metric.max() * 1.07
|
||||||
ax2.set_ylim([y2min, y2max])
|
ax2.set_ylim([y2min, y2max])
|
||||||
|
|
||||||
if peaks is not None:
|
if peaks is not None and len(peaks) > 0:
|
||||||
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):
|
for idx, peak in enumerate(peaks):
|
||||||
ax2.annotate(f"{idx+1}", (all_speeds[peak], vibration_metric[peak]),
|
ax2.annotate(
|
||||||
textcoords="offset points", xytext=(5, 5), fontweight='bold',
|
f'{idx+1}',
|
||||||
ha='left', fontsize=13, color=KLIPPAIN_COLORS['red_pink'], zorder=10)
|
(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):
|
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[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.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.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||||
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||||
@@ -310,8 +388,9 @@ def plot_global_speed_profile(ax, all_speeds, sp_min_energy, sp_max_energy, sp_v
|
|||||||
|
|
||||||
return
|
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_xlabel('Speed (mm/s)')
|
||||||
ax.set_ylabel('Energy')
|
ax.set_ylabel('Energy')
|
||||||
|
|
||||||
@@ -319,13 +398,13 @@ def plot_angular_speed_profiles(ax, speeds, angles, spectrogram_data, kinematics
|
|||||||
angle_settings = {
|
angle_settings = {
|
||||||
0: ('X (0 deg)', 'purple', 10),
|
0: ('X (0 deg)', 'purple', 10),
|
||||||
90: ('Y (90 deg)', 'dark_purple', 5),
|
90: ('Y (90 deg)', 'dark_purple', 5),
|
||||||
45: ('A (45 deg)' if kinematics == "corexy" else '45 deg', 'orange', 10),
|
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),
|
135: ('B (135 deg)' if kinematics == 'corexy' else '135 deg', 'dark_orange', 5),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Plot each angle using settings from the dictionary
|
# Plot each angle using settings from the dictionary
|
||||||
for angle, (label, color, zorder) in angle_settings.items():
|
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.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()])
|
||||||
@@ -343,8 +422,9 @@ def plot_angular_speed_profiles(ax, speeds, angles, spectrogram_data, kinematics
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
def plot_motor_profiles(ax, freqs, main_angles, motor_profiles, global_motor_profile, max_freq):
|
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_ylabel('Energy')
|
||||||
ax.set_xlabel('Frequency (Hz)')
|
ax.set_xlabel('Frequency (Hz)')
|
||||||
|
|
||||||
@@ -352,49 +432,61 @@ def plot_motor_profiles(ax, freqs, main_angles, motor_profiles, global_motor_pro
|
|||||||
ax2.yaxis.set_visible(False)
|
ax2.yaxis.set_visible(False)
|
||||||
|
|
||||||
# Global weighted average motor profile
|
# 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()
|
max_value = global_motor_profile.max()
|
||||||
|
|
||||||
# Mapping of angles to axis names
|
# Mapping of angles to axis names
|
||||||
angle_settings = {
|
angle_settings = {0: 'X', 90: 'Y', 45: 'A', 135: 'B'}
|
||||||
0: "X",
|
|
||||||
90: "Y",
|
|
||||||
45: "A",
|
|
||||||
135: "B"
|
|
||||||
}
|
|
||||||
|
|
||||||
# And then plot the motor profiles at each measured angles
|
# And then plot the motor profiles at each measured angles
|
||||||
for angle in main_angles:
|
for angle in main_angles:
|
||||||
profile_max = motor_profiles[angle].max()
|
profile_max = motor_profiles[angle].max()
|
||||||
if profile_max > max_value:
|
if profile_max > max_value:
|
||||||
max_value = profile_max
|
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.plot(freqs, motor_profiles[angle], linestyle='--', label=label, zorder=2)
|
||||||
|
|
||||||
ax.set_xlim([0, max_freq])
|
ax.set_xlim([0, max_freq])
|
||||||
ax.set_ylim([0, max_value * 1.1])
|
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
|
# 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)
|
motor_fr, motor_zeta, motor_res_idx, lowfreq_max = compute_mechanical_parameters(global_motor_profile, freqs, 30)
|
||||||
if lowfreq_max:
|
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(
|
||||||
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")
|
'[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:
|
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:
|
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.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]),
|
ax.annotate(
|
||||||
textcoords="offset points", xytext=(15, 5),
|
'R',
|
||||||
ha='right', fontsize=14, color=KLIPPAIN_COLORS['red_pink'], weight='bold')
|
(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:
|
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:
|
else:
|
||||||
ax2.plot([], [], ' ', label="No damping ratio computed")
|
ax2.plot([], [], ' ', label='No damping ratio computed')
|
||||||
|
|
||||||
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())
|
||||||
@@ -408,6 +500,7 @@ def plot_motor_profiles(ax, freqs, main_angles, motor_profiles, global_motor_pro
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
def plot_vibration_spectrogram_polar(ax, angles, speeds, spectrogram_data):
|
def plot_vibration_spectrogram_polar(ax, angles, speeds, spectrogram_data):
|
||||||
angles_radians = np.radians(angles)
|
angles_radians = np.radians(angles)
|
||||||
|
|
||||||
@@ -415,12 +508,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
|
# for both angles and speeds to map the spectrogram data onto a polar plot correctly
|
||||||
radius, theta = np.meshgrid(speeds, angles_radians)
|
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_title(
|
||||||
ax.set_theta_zero_location("E")
|
'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.set_theta_direction(1)
|
||||||
|
|
||||||
ax.pcolormesh(theta, radius, spectrogram_data, norm=matplotlib.colors.LogNorm(), cmap='inferno', shading='auto')
|
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.tick_params(axis='y', which='both', colors='white', labelsize='medium')
|
||||||
ax.set_ylim([0, max(speeds)])
|
ax.set_ylim([0, max(speeds)])
|
||||||
|
|
||||||
@@ -431,22 +526,36 @@ def plot_vibration_spectrogram_polar(ax, angles, speeds, spectrogram_data):
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
def plot_vibration_spectrogram(ax, angles, speeds, spectrogram_data, peaks):
|
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_xlabel('Speed (mm/s)')
|
||||||
ax.set_ylabel('Angle (deg)')
|
ax.set_ylabel('Angle (deg)')
|
||||||
|
|
||||||
ax.imshow(spectrogram_data, norm=matplotlib.colors.LogNorm(), cmap='inferno',
|
ax.imshow(
|
||||||
aspect='auto', extent=[speeds[0], speeds[-1], angles[0], angles[-1]],
|
spectrogram_data,
|
||||||
origin='lower', interpolation='antialiased')
|
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
|
# Add peaks lines in the spectrogram to get hint from peaks found in the first graph
|
||||||
if peaks is not None:
|
if peaks is not None and len(peaks) > 0:
|
||||||
for idx, peak in enumerate(peaks):
|
for idx, peak in enumerate(peaks):
|
||||||
ax.axvline(speeds[peak], color='cyan', linewidth=0.75)
|
ax.axvline(speeds[peak], color='cyan', linewidth=0.75)
|
||||||
ax.annotate(f"Peak {idx+1}", (speeds[peak], angles[-1]*0.9),
|
ax.annotate(
|
||||||
textcoords="data", color='cyan', rotation=90, fontsize=10,
|
f'Peak {idx+1}',
|
||||||
verticalalignment='top', horizontalalignment='right')
|
(speeds[peak], angles[-1] * 0.9),
|
||||||
|
textcoords='data',
|
||||||
|
color='cyan',
|
||||||
|
rotation=90,
|
||||||
|
fontsize=10,
|
||||||
|
verticalalignment='top',
|
||||||
|
horizontalalignment='right',
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -455,26 +564,31 @@ def plot_vibration_spectrogram(ax, angles, speeds, spectrogram_data, peaks):
|
|||||||
# Startup and main routines
|
# Startup and main routines
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
|
||||||
def extract_angle_and_speed(logname):
|
def extract_angle_and_speed(logname):
|
||||||
try:
|
try:
|
||||||
match = re.search(r'an(\d+)_\d+sp(\d+)_\d+', os.path.basename(logname))
|
match = re.search(r'an(\d+)_\d+sp(\d+)_\d+', os.path.basename(logname))
|
||||||
if match:
|
if match:
|
||||||
angle = match.group(1)
|
angle = match.group(1)
|
||||||
speed = match.group(2)
|
speed = match.group(2)
|
||||||
except AttributeError:
|
except AttributeError as err:
|
||||||
raise ValueError(f"File {logname} does not match expected format.")
|
raise ValueError(f'File {logname} does not match expected format.') from err
|
||||||
return float(angle), float(speed)
|
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, st_version=None
|
||||||
|
):
|
||||||
set_locale()
|
set_locale()
|
||||||
global shaper_calibrate
|
global shaper_calibrate
|
||||||
shaper_calibrate = setup_klipper_import(klipperdir)
|
shaper_calibrate = setup_klipper_import(klipperdir)
|
||||||
|
|
||||||
if kinematics == "cartesian": main_angles = [0, 90]
|
if kinematics == 'cartesian':
|
||||||
elif kinematics == "corexy": main_angles = [45, 135]
|
main_angles = [0, 90]
|
||||||
|
elif kinematics == 'corexy':
|
||||||
|
main_angles = [45, 135]
|
||||||
else:
|
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 = defaultdict(lambda: defaultdict(list))
|
||||||
psds_sum = defaultdict(lambda: defaultdict(list))
|
psds_sum = defaultdict(lambda: defaultdict(list))
|
||||||
@@ -503,27 +617,35 @@ def vibrations_profile(lognames, klipperdir="~/klipper", kinematics="cartesian",
|
|||||||
|
|
||||||
for main_angle in main_angles:
|
for main_angle in main_angles:
|
||||||
if main_angle not in measured_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
|
# 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)
|
all_angles_energy = compute_angle_powers(spectrogram_data)
|
||||||
sp_min_energy, sp_max_energy, sp_variance_energy, vibration_metric = compute_speed_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)
|
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, all_angles_energy)
|
||||||
symmetry_factor = compute_symmetry_analysis(all_angles, spectrogram_data, main_angles)
|
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
|
# 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
|
# and highlight them. Also find the peaks to identify speeds to avoid due to high resonances
|
||||||
num_peaks, vibration_peaks, peaks_speeds = detect_peaks(
|
num_peaks, vibration_peaks, peaks_speeds = detect_peaks(
|
||||||
vibration_metric, all_speeds,
|
vibration_metric,
|
||||||
|
all_speeds,
|
||||||
PEAKS_DETECTION_THRESHOLD * vibration_metric.max(),
|
PEAKS_DETECTION_THRESHOLD * vibration_metric.max(),
|
||||||
PEAKS_RELATIVE_HEIGHT_THRESHOLD, 10, 10
|
PEAKS_RELATIVE_HEIGHT_THRESHOLD,
|
||||||
)
|
10,
|
||||||
formated_peaks_speeds = ["{:.1f}".format(pspeed) for pspeed in peaks_speeds]
|
10,
|
||||||
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))))
|
)
|
||||||
|
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)
|
good_speeds = identify_low_energy_zones(vibration_metric, SPEEDS_VALLEY_DETECTION_THRESHOLD)
|
||||||
if good_speeds is not None:
|
if good_speeds is not None:
|
||||||
@@ -543,19 +665,25 @@ def vibrations_profile(lognames, klipperdir="~/klipper", kinematics="cartesian",
|
|||||||
if good_angles is not None:
|
if good_angles is not None:
|
||||||
print_with_c_locale(f'Lowest vibrations angles ({len(good_angles)} ranges sorted from best to worse):')
|
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):
|
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
|
# Create graph layout
|
||||||
fig, ((ax1, ax2, ax3), (ax4, ax5, ax6)) = plt.subplots(2, 3, gridspec_kw={
|
fig, ((ax1, ax2, ax3), (ax4, ax5, ax6)) = plt.subplots(
|
||||||
'height_ratios':[1, 1],
|
2,
|
||||||
'width_ratios':[4, 8, 6],
|
3,
|
||||||
'bottom':0.050,
|
gridspec_kw={
|
||||||
'top':0.890,
|
'height_ratios': [1, 1],
|
||||||
'left':0.040,
|
'width_ratios': [4, 8, 6],
|
||||||
'right':0.985,
|
'bottom': 0.050,
|
||||||
'hspace':0.166,
|
'top': 0.890,
|
||||||
'wspace':0.138
|
'left': 0.040,
|
||||||
})
|
'right': 0.985,
|
||||||
|
'hspace': 0.166,
|
||||||
|
'wspace': 0.138,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# Transform ax3 and ax4 to polar plots
|
# Transform ax3 and ax4 to polar plots
|
||||||
ax1.remove()
|
ax1.remove()
|
||||||
@@ -567,16 +695,18 @@ def vibrations_profile(lognames, klipperdir="~/klipper", kinematics="cartesian",
|
|||||||
fig.set_size_inches(20, 11.5)
|
fig.set_size_inches(20, 11.5)
|
||||||
|
|
||||||
# Add title
|
# Add title
|
||||||
title_line1 = "MACHINE VIBRATIONS ANALYSIS TOOL"
|
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')
|
fig.text(
|
||||||
|
0.060, 0.965, title_line1, ha='left', va='bottom', fontsize=20, color=KLIPPAIN_COLORS['purple'], weight='bold'
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
filename_parts = (lognames[0].split('/')[-1]).split('_')
|
filename_parts = (lognames[0].split('/')[-1]).split('_')
|
||||||
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')
|
title_line2 = dt.strftime('%x %X')
|
||||||
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:
|
except Exception:
|
||||||
print_with_c_locale("Warning: CSV filenames appear to be different than expected (%s)" % (lognames[0]))
|
print_with_c_locale('Warning: CSV filenames appear to be different than expected (%s)' % (lognames[0]))
|
||||||
title_line2 = lognames[0].split('/')[-1]
|
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'])
|
||||||
|
|
||||||
@@ -584,7 +714,17 @@ def vibrations_profile(lognames, klipperdir="~/klipper", kinematics="cartesian",
|
|||||||
plot_angle_profile_polar(ax1, all_angles, all_angles_energy, good_angles, symmetry_factor)
|
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_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_angular_speed_profiles(ax3, all_speeds, all_angles, spectrogram_data, kinematics)
|
||||||
plot_vibration_spectrogram(ax5, all_angles, all_speeds, spectrogram_data, vibration_peaks)
|
plot_vibration_spectrogram(ax5, all_angles, all_speeds, spectrogram_data, vibration_peaks)
|
||||||
|
|
||||||
@@ -596,8 +736,7 @@ def vibrations_profile(lognames, klipperdir="~/klipper", kinematics="cartesian",
|
|||||||
ax_logo.axis('off')
|
ax_logo.axis('off')
|
||||||
|
|
||||||
# Adding Shake&Tune version in the top right corner
|
# Adding Shake&Tune version in the top right corner
|
||||||
st_version = get_git_version()
|
if st_version != 'unknown':
|
||||||
if st_version is not None:
|
|
||||||
fig.text(0.995, 0.985, st_version, ha='right', va='bottom', fontsize=8, color=KLIPPAIN_COLORS['purple'])
|
fig.text(0.995, 0.985, st_version, ha='right', va='bottom', fontsize=8, color=KLIPPAIN_COLORS['purple'])
|
||||||
|
|
||||||
return fig
|
return fig
|
||||||
@@ -605,25 +744,31 @@ def vibrations_profile(lognames, klipperdir="~/klipper", kinematics="cartesian",
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
# Parse command-line arguments
|
# Parse command-line arguments
|
||||||
usage = "%prog [options] <raw logs>"
|
usage = '%prog [options] <raw logs>'
|
||||||
opts = optparse.OptionParser(usage)
|
opts = optparse.OptionParser(usage)
|
||||||
opts.add_option("-o", "--output", type="string", dest="output",
|
opts.add_option('-o', '--output', type='string', dest='output', default=None, help='filename of output graph')
|
||||||
default=None, help="filename of output graph")
|
opts.add_option(
|
||||||
opts.add_option("-c", "--accel", type="int", dest="accel",
|
'-c', '--accel', type='int', dest='accel', default=None, help='accel value to be printed on the graph'
|
||||||
default=None, help="accel value to be printed on the graph")
|
)
|
||||||
opts.add_option("-f", "--max_freq", type="float", default=1000.,
|
opts.add_option('-f', '--max_freq', type='float', default=1000.0, help='maximum frequency to graph')
|
||||||
help="maximum frequency to graph")
|
opts.add_option(
|
||||||
opts.add_option("-k", "--klipper_dir", type="string", dest="klipperdir",
|
'-k', '--klipper_dir', type='string', dest='klipperdir', default='~/klipper', help='main klipper directory'
|
||||||
default="~/klipper", help="main klipper directory")
|
)
|
||||||
opts.add_option("-m", "--kinematics", type="string", dest="kinematics",
|
opts.add_option(
|
||||||
default="cartesian", help="machine kinematics configuration")
|
'-m',
|
||||||
|
'--kinematics',
|
||||||
|
type='string',
|
||||||
|
dest='kinematics',
|
||||||
|
default='cartesian',
|
||||||
|
help='machine kinematics configuration',
|
||||||
|
)
|
||||||
options, args = opts.parse_args()
|
options, args = opts.parse_args()
|
||||||
if len(args) < 1:
|
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:
|
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"]:
|
if options.kinematics not in ['cartesian', 'corexy']:
|
||||||
opts.error("Only cartesian and corexy kinematics are supported by this tool at the moment!")
|
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 = vibrations_profile(args, options.klipperdir, options.kinematics, options.accel, options.max_freq)
|
||||||
fig.savefig(options.output, dpi=150)
|
fig.savefig(options.output, dpi=150)
|
||||||
|
Before Width: | Height: | Size: 607 KiB After Width: | Height: | Size: 607 KiB |
0
src/helpers/__init__.py
Normal file
0
src/helpers/__init__.py
Normal file
58
K-ShakeTune/scripts/common_func.py → src/helpers/common_func.py
Executable file → Normal file
58
K-ShakeTune/scripts/common_func.py → src/helpers/common_func.py
Executable file → Normal file
@@ -4,12 +4,14 @@
|
|||||||
# Written by Frix_x#0161 #
|
# Written by Frix_x#0161 #
|
||||||
|
|
||||||
import math
|
import math
|
||||||
import os, sys
|
import os
|
||||||
|
import sys
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from scipy.signal import spectrogram
|
|
||||||
from git import GitCommandError, Repo
|
from git import GitCommandError, Repo
|
||||||
|
from scipy.signal import spectrogram
|
||||||
|
|
||||||
|
|
||||||
def parse_log(logname):
|
def parse_log(logname):
|
||||||
@@ -21,9 +23,11 @@ def parse_log(logname):
|
|||||||
# Raw accelerometer data
|
# Raw accelerometer data
|
||||||
return np.loadtxt(logname, comments='#', delimiter=',')
|
return np.loadtxt(logname, comments='#', delimiter=',')
|
||||||
# Power spectral density data or shaper calibration data
|
# Power spectral density data or shaper calibration data
|
||||||
raise ValueError("File %s does not contain raw accelerometer data and therefore "
|
raise ValueError(
|
||||||
"is not supported by Shake&Tune. Please use the official Klipper "
|
'File %s does not contain raw accelerometer data and therefore '
|
||||||
"script to process it instead." % (logname,))
|
'is not supported by Shake&Tune. Please use the official Klipper '
|
||||||
|
'script to process it instead.' % (logname,)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def setup_klipper_import(kdir):
|
def setup_klipper_import(kdir):
|
||||||
@@ -38,7 +42,7 @@ def get_git_version():
|
|||||||
# Get the absolute path of the script, resolving any symlinks
|
# Get the absolute path of the script, resolving any symlinks
|
||||||
# Then get 2 times to parent dir to be at the git root folder
|
# Then get 2 times to parent dir to be at the git root folder
|
||||||
script_path = Path(__file__).resolve()
|
script_path = Path(__file__).resolve()
|
||||||
repo_path = script_path.parents[2]
|
repo_path = script_path.parents[1]
|
||||||
repo = Repo(repo_path)
|
repo = Repo(repo_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -48,7 +52,7 @@ def get_git_version():
|
|||||||
version = repo.head.commit.hexsha[:7]
|
version = repo.head.commit.hexsha[:7]
|
||||||
return version
|
return version
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -57,12 +61,13 @@ def compute_spectrogram(data):
|
|||||||
N = data.shape[0]
|
N = data.shape[0]
|
||||||
Fs = N / (data[-1, 0] - data[0, 0])
|
Fs = N / (data[-1, 0] - data[0, 0])
|
||||||
# Round up to a power of 2 for faster FFT
|
# Round up to a power of 2 for faster FFT
|
||||||
M = 1 << int(.5 * Fs - 1).bit_length()
|
M = 1 << int(0.5 * Fs - 1).bit_length()
|
||||||
window = np.kaiser(M, 6.)
|
window = np.kaiser(M, 6.0)
|
||||||
|
|
||||||
def _specgram(x):
|
def _specgram(x):
|
||||||
return spectrogram(x, fs=Fs, window=window, nperseg=M, noverlap=M//2,
|
return spectrogram(
|
||||||
detrend='constant', scaling='density', mode='psd')
|
x, fs=Fs, window=window, nperseg=M, noverlap=M // 2, detrend='constant', scaling='density', mode='psd'
|
||||||
|
)
|
||||||
|
|
||||||
d = {'x': data[:, 1], 'y': data[:, 2], 'z': data[:, 3]}
|
d = {'x': data[:, 1], 'y': data[:, 2], 'z': data[:, 3]}
|
||||||
f, t, pdata = _specgram(d['x'])
|
f, t, pdata = _specgram(d['x'])
|
||||||
@@ -104,17 +109,26 @@ def compute_mechanical_parameters(psd, freqs, min_freq=None):
|
|||||||
|
|
||||||
idx_below = indices_below[-1]
|
idx_below = indices_below[-1]
|
||||||
idx_above = indices_above[0] + max_power_index
|
idx_above = indices_above[0] + max_power_index
|
||||||
freq_below_half_power = freqs[idx_below] + (half_power - psd[idx_below]) * (freqs[idx_below + 1] - freqs[idx_below]) / (psd[idx_below + 1] - psd[idx_below])
|
freq_below_half_power = freqs[idx_below] + (half_power - psd[idx_below]) * (
|
||||||
freq_above_half_power = freqs[idx_above - 1] + (half_power - psd[idx_above - 1]) * (freqs[idx_above] - freqs[idx_above - 1]) / (psd[idx_above] - psd[idx_above - 1])
|
freqs[idx_below + 1] - freqs[idx_below]
|
||||||
|
) / (psd[idx_below + 1] - psd[idx_below])
|
||||||
|
freq_above_half_power = freqs[idx_above - 1] + (half_power - psd[idx_above - 1]) * (
|
||||||
|
freqs[idx_above] - freqs[idx_above - 1]
|
||||||
|
) / (psd[idx_above] - psd[idx_above - 1])
|
||||||
|
|
||||||
bandwidth = freq_above_half_power - freq_below_half_power
|
bandwidth = freq_above_half_power - freq_below_half_power
|
||||||
bw1 = math.pow(bandwidth/fr, 2)
|
bw1 = math.pow(bandwidth / fr, 2)
|
||||||
bw2 = math.pow(bandwidth/fr, 4)
|
bw2 = math.pow(bandwidth / fr, 4)
|
||||||
|
|
||||||
zeta = math.sqrt(0.5 - math.sqrt(1 / (4 + 4 * bw1 - bw2)))
|
try:
|
||||||
|
zeta = math.sqrt(0.5 - math.sqrt(1 / (4 + 4 * bw1 - bw2)))
|
||||||
|
except ValueError:
|
||||||
|
# If a math problem arise such as a negative sqrt term, we also return None instead for damping ratio
|
||||||
|
return fr, None, max_power_index, max_under_min_freq
|
||||||
|
|
||||||
return fr, zeta, max_power_index, max_under_min_freq
|
return fr, zeta, max_power_index, max_under_min_freq
|
||||||
|
|
||||||
|
|
||||||
# This find all the peaks in a curve by looking at when the derivative term goes from positive to negative
|
# This find all the peaks in a curve by looking at when the derivative term goes from positive to negative
|
||||||
# Then only the peaks found above a threshold are kept to avoid capturing peaks in the low amplitude noise of a signal
|
# Then only the peaks found above a threshold are kept to avoid capturing peaks in the low amplitude noise of a signal
|
||||||
def detect_peaks(data, indices, detection_threshold, relative_height_threshold=None, window_size=5, vicinity=3):
|
def detect_peaks(data, indices, detection_threshold, relative_height_threshold=None, window_size=5, vicinity=3):
|
||||||
@@ -125,7 +139,9 @@ def detect_peaks(data, indices, detection_threshold, relative_height_threshold=N
|
|||||||
smoothed_data = np.concatenate((mean_pad, smoothed_data))
|
smoothed_data = np.concatenate((mean_pad, smoothed_data))
|
||||||
|
|
||||||
# Find peaks on the smoothed curve
|
# Find peaks on the smoothed curve
|
||||||
smoothed_peaks = np.where((smoothed_data[:-2] < smoothed_data[1:-1]) & (smoothed_data[1:-1] > smoothed_data[2:]))[0] + 1
|
smoothed_peaks = (
|
||||||
|
np.where((smoothed_data[:-2] < smoothed_data[1:-1]) & (smoothed_data[1:-1] > smoothed_data[2:]))[0] + 1
|
||||||
|
)
|
||||||
smoothed_peaks = smoothed_peaks[smoothed_data[smoothed_peaks] > detection_threshold]
|
smoothed_peaks = smoothed_peaks[smoothed_data[smoothed_peaks] > detection_threshold]
|
||||||
|
|
||||||
# Additional validation for peaks based on relative height
|
# Additional validation for peaks based on relative height
|
||||||
@@ -133,14 +149,16 @@ def detect_peaks(data, indices, detection_threshold, relative_height_threshold=N
|
|||||||
if relative_height_threshold is not None:
|
if relative_height_threshold is not None:
|
||||||
valid_peaks = []
|
valid_peaks = []
|
||||||
for peak in smoothed_peaks:
|
for peak in smoothed_peaks:
|
||||||
peak_height = smoothed_data[peak] - np.min(smoothed_data[max(0, peak-vicinity):min(len(smoothed_data), peak+vicinity+1)])
|
peak_height = smoothed_data[peak] - np.min(
|
||||||
|
smoothed_data[max(0, peak - vicinity) : min(len(smoothed_data), peak + vicinity + 1)]
|
||||||
|
)
|
||||||
if peak_height > relative_height_threshold * smoothed_data[peak]:
|
if peak_height > relative_height_threshold * smoothed_data[peak]:
|
||||||
valid_peaks.append(peak)
|
valid_peaks.append(peak)
|
||||||
|
|
||||||
# Refine peak positions on the original curve
|
# Refine peak positions on the original curve
|
||||||
refined_peaks = []
|
refined_peaks = []
|
||||||
for peak in valid_peaks:
|
for peak in valid_peaks:
|
||||||
local_max = peak + np.argmax(data[max(0, peak-vicinity):min(len(data), peak+vicinity+1)]) - vicinity
|
local_max = peak + np.argmax(data[max(0, peak - vicinity) : min(len(data), peak + vicinity + 1)]) - vicinity
|
||||||
refined_peaks.append(local_max)
|
refined_peaks.append(local_max)
|
||||||
|
|
||||||
num_peaks = len(refined_peaks)
|
num_peaks = len(refined_peaks)
|
||||||
@@ -153,7 +171,7 @@ def identify_low_energy_zones(power_total, detection_threshold=0.1):
|
|||||||
valleys = []
|
valleys = []
|
||||||
|
|
||||||
# Calculate the a "mean + 1/4" and standard deviation of the entire power_total
|
# Calculate the a "mean + 1/4" and standard deviation of the entire power_total
|
||||||
mean_energy = np.mean(power_total) + (np.max(power_total) - np.min(power_total))/4
|
mean_energy = np.mean(power_total) + (np.max(power_total) - np.min(power_total)) / 4
|
||||||
std_energy = np.std(power_total)
|
std_energy = np.std(power_total)
|
||||||
|
|
||||||
# Define a threshold value as "mean + 1/4" minus a certain number of standard deviations
|
# Define a threshold value as "mean + 1/4" minus a certain number of standard deviations
|
||||||
38
src/helpers/filemanager.py
Normal file
38
src/helpers/filemanager.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# Common file management functions for the Shake&Tune package
|
||||||
|
# Written by Frix_x#0161 #
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def wait_file_ready(filepath: Path, timeout: int = 60) -> None:
|
||||||
|
file_busy = True
|
||||||
|
loop_count = 0
|
||||||
|
|
||||||
|
while file_busy:
|
||||||
|
if loop_count >= timeout:
|
||||||
|
raise TimeoutError(f'Klipper is taking too long to release the CSV file ({filepath})!')
|
||||||
|
|
||||||
|
# Try to open the file in write-only mode to check if it is in use
|
||||||
|
# If we successfully open and close the file, it is not in use
|
||||||
|
try:
|
||||||
|
fd = os.open(filepath, os.O_WRONLY)
|
||||||
|
os.close(fd)
|
||||||
|
file_busy = False
|
||||||
|
except OSError:
|
||||||
|
# If OSError is caught, it indicates the file is still being used
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
# If another exception is raised, it's not a problem, we just loop again
|
||||||
|
pass
|
||||||
|
|
||||||
|
loop_count += 1
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_folders_exist(folders: list[Path]) -> None:
|
||||||
|
for folder in folders:
|
||||||
|
folder.mkdir(parents=True, exist_ok=True)
|
||||||
10
K-ShakeTune/scripts/locale_utils.py → src/helpers/locale_utils.py
Executable file → Normal file
10
K-ShakeTune/scripts/locale_utils.py → src/helpers/locale_utils.py
Executable file → Normal file
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import locale
|
import locale
|
||||||
|
|
||||||
|
|
||||||
# Set the best locale for time and date formating (generation of the titles)
|
# Set the best locale for time and date formating (generation of the titles)
|
||||||
def set_locale():
|
def set_locale():
|
||||||
try:
|
try:
|
||||||
@@ -15,16 +16,19 @@ def set_locale():
|
|||||||
except locale.Error:
|
except locale.Error:
|
||||||
locale.setlocale(locale.LC_TIME, 'C')
|
locale.setlocale(locale.LC_TIME, 'C')
|
||||||
|
|
||||||
|
|
||||||
# Print function to avoid problem in Klipper console (that doesn't support special characters) due to locale settings
|
# Print function to avoid problem in Klipper console (that doesn't support special characters) due to locale settings
|
||||||
def print_with_c_locale(*args, **kwargs):
|
def print_with_c_locale(*args, **kwargs):
|
||||||
try:
|
try:
|
||||||
original_locale = locale.getlocale()
|
original_locale = locale.getlocale()
|
||||||
locale.setlocale(locale.LC_ALL, 'C')
|
locale.setlocale(locale.LC_ALL, 'C')
|
||||||
except locale.Error as e:
|
except locale.Error as e:
|
||||||
print("Warning: Failed to set a basic locale. Special characters may not display correctly in Klipper console:", e)
|
print(
|
||||||
|
'Warning: Failed to set a basic locale. Special characters may not display correctly in Klipper console:', e
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
print(*args, **kwargs) # Proceed with printing regardless of locale setting success
|
print(*args, **kwargs) # Proceed with printing regardless of locale setting success
|
||||||
try:
|
try:
|
||||||
locale.setlocale(locale.LC_ALL, original_locale)
|
locale.setlocale(locale.LC_ALL, original_locale)
|
||||||
except locale.Error as e:
|
except locale.Error as e:
|
||||||
print("Warning: Failed to restore the original locale setting:", e)
|
print('Warning: Failed to restore the original locale setting:', e)
|
||||||
410
src/is_workflow.py
Executable file
410
src/is_workflow.py
Executable file
@@ -0,0 +1,410 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
############################################
|
||||||
|
###### INPUT SHAPER KLIPPAIN WORKFLOW ######
|
||||||
|
############################################
|
||||||
|
# Written by Frix_x#0161 #
|
||||||
|
|
||||||
|
# This script is designed to be used with gcode_shell_commands directly from Klipper
|
||||||
|
# Use the provided Shake&Tune macros instead!
|
||||||
|
|
||||||
|
|
||||||
|
import abc
|
||||||
|
import argparse
|
||||||
|
import tarfile
|
||||||
|
import traceback
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
from git import GitCommandError, Repo
|
||||||
|
from matplotlib.figure import Figure
|
||||||
|
|
||||||
|
import src.helpers.filemanager as fm
|
||||||
|
from src.graph_creators.analyze_axesmap import axesmap_calibration
|
||||||
|
from src.graph_creators.graph_belts import belts_calibration
|
||||||
|
from src.graph_creators.graph_shaper import shaper_calibration
|
||||||
|
from src.graph_creators.graph_vibrations import vibrations_profile
|
||||||
|
from src.helpers.locale_utils import print_with_c_locale
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
KLIPPER_FOLDER = Path.home() / 'klipper'
|
||||||
|
RESULTS_BASE_FOLDER = Path.home() / 'printer_data/config/K-ShakeTune_results'
|
||||||
|
RESULTS_SUBFOLDERS = {'belts': 'belts', 'shaper': 'inputshaper', 'vibrations': 'vibrations'}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_results_folder(type: str) -> Path:
|
||||||
|
return Config.RESULTS_BASE_FOLDER / Config.RESULTS_SUBFOLDERS[type]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_git_version() -> str:
|
||||||
|
try:
|
||||||
|
# Get the absolute path of the script, resolving any symlinks
|
||||||
|
# Then get 1 times to parent dir to be at the git root folder
|
||||||
|
script_path = Path(__file__).resolve()
|
||||||
|
repo_path = script_path.parents[1]
|
||||||
|
repo = Repo(repo_path)
|
||||||
|
try:
|
||||||
|
version = repo.git.describe('--tags')
|
||||||
|
except GitCommandError:
|
||||||
|
version = repo.head.commit.hexsha[:7] # If no tag is found, use the simplified commit SHA instead
|
||||||
|
return version
|
||||||
|
except Exception as e:
|
||||||
|
print_with_c_locale(f'Warning: unable to retrieve Shake&Tune version number: {e}')
|
||||||
|
return 'unknown'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def parse_arguments() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description='Shake&Tune graphs generation script')
|
||||||
|
parser.add_argument(
|
||||||
|
'-t',
|
||||||
|
'--type',
|
||||||
|
dest='type',
|
||||||
|
choices=['belts', 'shaper', 'vibrations', 'axesmap'],
|
||||||
|
required=True,
|
||||||
|
help='Type of output graph to produce',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--accel',
|
||||||
|
type=int,
|
||||||
|
default=None,
|
||||||
|
dest='accel_used',
|
||||||
|
help='Accelerometion used for vibrations profile creation or axes map calibration',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--chip_name',
|
||||||
|
type=str,
|
||||||
|
default='adxl345',
|
||||||
|
dest='chip_name',
|
||||||
|
help='Accelerometer chip name used for vibrations profile creation or axes map calibration',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--max_smoothing',
|
||||||
|
type=float,
|
||||||
|
default=None,
|
||||||
|
dest='max_smoothing',
|
||||||
|
help='Maximum smoothing to allow for input shaper filter recommendations',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--scv',
|
||||||
|
'--square_corner_velocity',
|
||||||
|
type=float,
|
||||||
|
default=5.0,
|
||||||
|
dest='scv',
|
||||||
|
help='Square corner velocity used to compute max accel for input shapers filter recommendations',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-m',
|
||||||
|
'--kinematics',
|
||||||
|
dest='kinematics',
|
||||||
|
default='cartesian',
|
||||||
|
choices=['cartesian', 'corexy'],
|
||||||
|
help='Machine kinematics configuration used for the vibrations profile creation',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-c',
|
||||||
|
'--keep_csv',
|
||||||
|
action='store_true',
|
||||||
|
default=False,
|
||||||
|
dest='keep_csv',
|
||||||
|
help='Whether to keep the raw CSV files after processing in addition to the PNG graphs',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-n',
|
||||||
|
'--keep_results',
|
||||||
|
type=int,
|
||||||
|
default=3,
|
||||||
|
dest='keep_results',
|
||||||
|
help='Number of results to keep in the result folder after each run of the script',
|
||||||
|
)
|
||||||
|
parser.add_argument('--dpi', type=int, default=150, dest='dpi', help='DPI of the output PNG files')
|
||||||
|
parser.add_argument('-v', '--version', action='version', version=f'Shake&Tune {Config.get_git_version()}')
|
||||||
|
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
class GraphCreator(abc.ABC):
|
||||||
|
def __init__(self, keep_csv: bool, dpi: int):
|
||||||
|
self._keep_csv = keep_csv
|
||||||
|
self._dpi = dpi
|
||||||
|
|
||||||
|
self._graph_date = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
|
self._version = Config.get_git_version()
|
||||||
|
|
||||||
|
self._type = None
|
||||||
|
self._folder = None
|
||||||
|
|
||||||
|
def _setup_folder(self, graph_type: str) -> None:
|
||||||
|
self._type = graph_type
|
||||||
|
self._folder = Config.get_results_folder(graph_type)
|
||||||
|
|
||||||
|
def _move_and_prepare_files(
|
||||||
|
self,
|
||||||
|
glob_pattern: str,
|
||||||
|
min_files_required: Optional[int] = None,
|
||||||
|
custom_name_func: Optional[Callable[[Path], str]] = None,
|
||||||
|
) -> list[Path]:
|
||||||
|
tmp_path = Path('/tmp')
|
||||||
|
globbed_files = list(tmp_path.glob(glob_pattern))
|
||||||
|
|
||||||
|
# If min_files_required is not set, use the number of globbed files as the minimum
|
||||||
|
min_files_required = min_files_required or len(globbed_files)
|
||||||
|
|
||||||
|
if not globbed_files:
|
||||||
|
raise FileNotFoundError(f'no CSV files found in the /tmp folder to create the {self._type} graphs!')
|
||||||
|
if len(globbed_files) < min_files_required:
|
||||||
|
raise FileNotFoundError(f'{min_files_required} CSV files are needed to create the {self._type} graphs!')
|
||||||
|
|
||||||
|
lognames = []
|
||||||
|
for filename in sorted(globbed_files, key=lambda f: f.stat().st_mtime, reverse=True)[:min_files_required]:
|
||||||
|
fm.wait_file_ready(filename)
|
||||||
|
custom_name = custom_name_func(filename) if custom_name_func else filename.name
|
||||||
|
new_file = self._folder / f'{self._type}_{self._graph_date}_{custom_name}.csv'
|
||||||
|
filename.rename(new_file)
|
||||||
|
fm.wait_file_ready(new_file)
|
||||||
|
lognames.append(new_file)
|
||||||
|
return lognames
|
||||||
|
|
||||||
|
def _save_figure_and_cleanup(self, fig: Figure, lognames: list[Path], axis_label: Optional[str] = None) -> None:
|
||||||
|
axis_suffix = f'_{axis_label}' if axis_label else ''
|
||||||
|
png_filename = self._folder / f'{self._type}_{self._graph_date}{axis_suffix}.png'
|
||||||
|
fig.savefig(png_filename, dpi=self._dpi)
|
||||||
|
|
||||||
|
if self._keep_csv:
|
||||||
|
self._archive_files(lognames)
|
||||||
|
else:
|
||||||
|
self._remove_files(lognames)
|
||||||
|
|
||||||
|
def _archive_files(self, _: list[Path]) -> None:
|
||||||
|
return
|
||||||
|
|
||||||
|
def _remove_files(self, lognames: list[Path]) -> None:
|
||||||
|
for csv in lognames:
|
||||||
|
csv.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def create_graph(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def clean_old_files(self, keep_results: int) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BeltsGraphCreator(GraphCreator):
|
||||||
|
def __init__(self, keep_csv: bool = False, dpi: int = 150):
|
||||||
|
super().__init__(keep_csv, dpi)
|
||||||
|
|
||||||
|
self._setup_folder('belts')
|
||||||
|
|
||||||
|
def create_graph(self) -> None:
|
||||||
|
lognames = self._move_and_prepare_files(
|
||||||
|
glob_pattern='raw_data_axis*.csv',
|
||||||
|
min_files_required=2,
|
||||||
|
custom_name_func=lambda f: f.stem.split('_')[3].upper(),
|
||||||
|
)
|
||||||
|
fig = belts_calibration(
|
||||||
|
lognames=[str(path) for path in lognames],
|
||||||
|
klipperdir=str(Config.KLIPPER_FOLDER),
|
||||||
|
st_version=self._version,
|
||||||
|
)
|
||||||
|
self._save_figure_and_cleanup(fig, lognames)
|
||||||
|
|
||||||
|
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:]:
|
||||||
|
file_date = '_'.join(old_file.stem.split('_')[1:3])
|
||||||
|
for suffix in ['A', 'B']:
|
||||||
|
csv_file = self._folder / f'belts_{file_date}_{suffix}.csv'
|
||||||
|
csv_file.unlink(missing_ok=True)
|
||||||
|
old_file.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
class ShaperGraphCreator(GraphCreator):
|
||||||
|
def __init__(self, keep_csv: bool = False, dpi: int = 150):
|
||||||
|
super().__init__(keep_csv, dpi)
|
||||||
|
|
||||||
|
self._max_smoothing = None
|
||||||
|
self._scv = None
|
||||||
|
|
||||||
|
self._setup_folder('shaper')
|
||||||
|
|
||||||
|
def configure(self, scv: float, max_smoothing: float = None) -> None:
|
||||||
|
self._scv = scv
|
||||||
|
self._max_smoothing = max_smoothing
|
||||||
|
|
||||||
|
def create_graph(self) -> None:
|
||||||
|
if not self._scv:
|
||||||
|
raise ValueError('scv must be set to create the input shaper graph!')
|
||||||
|
|
||||||
|
lognames = self._move_and_prepare_files(
|
||||||
|
glob_pattern='raw_data*.csv',
|
||||||
|
min_files_required=1,
|
||||||
|
custom_name_func=lambda f: f.stem.split('_')[3].upper(),
|
||||||
|
)
|
||||||
|
fig = shaper_calibration(
|
||||||
|
lognames=[str(path) for path in lognames],
|
||||||
|
klipperdir=str(Config.KLIPPER_FOLDER),
|
||||||
|
max_smoothing=self._max_smoothing,
|
||||||
|
scv=self._scv,
|
||||||
|
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) <= 2 * keep_results:
|
||||||
|
return # No need to delete any files
|
||||||
|
|
||||||
|
# Delete the older files
|
||||||
|
for old_file in files[2 * keep_results :]:
|
||||||
|
csv_file = old_file.with_suffix('.csv')
|
||||||
|
csv_file.unlink(missing_ok=True)
|
||||||
|
old_file.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
class VibrationsGraphCreator(GraphCreator):
|
||||||
|
def __init__(self, keep_csv: bool = False, dpi: int = 150):
|
||||||
|
super().__init__(keep_csv, dpi)
|
||||||
|
|
||||||
|
self._kinematics = None
|
||||||
|
self._accel = None
|
||||||
|
self._chip_name = None
|
||||||
|
|
||||||
|
self._setup_folder('vibrations')
|
||||||
|
|
||||||
|
def configure(self, kinematics: str, accel: float, chip_name: str) -> None:
|
||||||
|
self._kinematics = kinematics
|
||||||
|
self._accel = accel
|
||||||
|
self._chip_name = chip_name
|
||||||
|
|
||||||
|
def _archive_files(self, lognames: list[Path]) -> None:
|
||||||
|
tar_path = self._folder / f'{self._type}_{self._graph_date}.tar.gz'
|
||||||
|
with tarfile.open(tar_path, 'w:gz') as tar:
|
||||||
|
for csv_file in lognames:
|
||||||
|
tar.add(csv_file, arcname=csv_file.name, recursive=False)
|
||||||
|
|
||||||
|
def create_graph(self) -> None:
|
||||||
|
if not self._accel or not self._chip_name or not self._kinematics:
|
||||||
|
raise ValueError('accel, chip_name and kinematics must be set to create the vibrations profile graph!')
|
||||||
|
|
||||||
|
lognames = self._move_and_prepare_files(
|
||||||
|
glob_pattern=f'{self._chip_name}-*.csv',
|
||||||
|
min_files_required=None,
|
||||||
|
custom_name_func=lambda f: f.name.replace(self._chip_name, self._type),
|
||||||
|
)
|
||||||
|
fig = vibrations_profile(
|
||||||
|
lognames=[str(path) for path in lognames],
|
||||||
|
klipperdir=str(Config.KLIPPER_FOLDER),
|
||||||
|
kinematics=self._kinematics,
|
||||||
|
accel=self._accel,
|
||||||
|
st_version=self._version,
|
||||||
|
)
|
||||||
|
self._save_figure_and_cleanup(fig, lognames)
|
||||||
|
|
||||||
|
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:]:
|
||||||
|
old_file.unlink()
|
||||||
|
tar_file = old_file.with_suffix('.tar.gz')
|
||||||
|
tar_file.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
class AxesMapFinder:
|
||||||
|
def __init__(self, accel: float, chip_name: str):
|
||||||
|
self._accel = accel
|
||||||
|
self._chip_name = chip_name
|
||||||
|
|
||||||
|
self._graph_date = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
|
|
||||||
|
self._type = 'axesmap'
|
||||||
|
self._folder = Config.RESULTS_BASE_FOLDER
|
||||||
|
|
||||||
|
def find_axesmap(self) -> None:
|
||||||
|
tmp_folder = Path('/tmp')
|
||||||
|
globbed_files = list(tmp_folder.glob(f'{self._chip_name}-*.csv'))
|
||||||
|
|
||||||
|
if not globbed_files:
|
||||||
|
raise FileNotFoundError('no CSV files found in the /tmp folder to find the axes map!')
|
||||||
|
|
||||||
|
# Find the CSV files with the latest timestamp and wait for it to be released by Klipper
|
||||||
|
logname = sorted(globbed_files, key=lambda f: f.stat().st_mtime, reverse=True)[0]
|
||||||
|
fm.wait_file_ready(logname)
|
||||||
|
|
||||||
|
results = axesmap_calibration(
|
||||||
|
lognames=[str(logname)],
|
||||||
|
accel=self._accel,
|
||||||
|
)
|
||||||
|
|
||||||
|
result_filename = self._folder / f'{self._type}_{self._graph_date}.txt'
|
||||||
|
with result_filename.open('w') as f:
|
||||||
|
f.write(results)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
options = Config.parse_arguments()
|
||||||
|
fm.ensure_folders_exist(
|
||||||
|
folders=[Config.RESULTS_BASE_FOLDER / subfolder for subfolder in Config.RESULTS_SUBFOLDERS.values()]
|
||||||
|
)
|
||||||
|
|
||||||
|
print_with_c_locale(f'Shake&Tune version: {Config.get_git_version()}')
|
||||||
|
|
||||||
|
graph_creators = {
|
||||||
|
'belts': (BeltsGraphCreator, None),
|
||||||
|
'shaper': (ShaperGraphCreator, lambda gc: gc.configure(options.scv, options.max_smoothing)),
|
||||||
|
'vibrations': (
|
||||||
|
VibrationsGraphCreator,
|
||||||
|
lambda gc: gc.configure(options.kinematics, options.accel_used, options.chip_name),
|
||||||
|
),
|
||||||
|
'axesmap': (AxesMapFinder, None),
|
||||||
|
}
|
||||||
|
|
||||||
|
creator_info = graph_creators.get(options.type)
|
||||||
|
if not creator_info:
|
||||||
|
print_with_c_locale('Error: invalid graph type specified!')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Instantiate the graph creator
|
||||||
|
graph_creator_class, configure_func = creator_info
|
||||||
|
graph_creator = graph_creator_class(options.keep_csv, options.dpi)
|
||||||
|
|
||||||
|
# Configure it if needed
|
||||||
|
if configure_func:
|
||||||
|
configure_func(graph_creator)
|
||||||
|
|
||||||
|
# And then run it
|
||||||
|
try:
|
||||||
|
graph_creator.create_graph()
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
print_with_c_locale(f'FileNotFound error: {e}')
|
||||||
|
return
|
||||||
|
except TimeoutError as e:
|
||||||
|
print_with_c_locale(f'Timeout error: {e}')
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
print_with_c_locale(f'Error while generating the graphs: {e}')
|
||||||
|
traceback.print_exc()
|
||||||
|
return
|
||||||
|
|
||||||
|
print_with_c_locale(f'{options.type} graphs created successfully!')
|
||||||
|
graph_creator.clean_old_files(options.keep_results)
|
||||||
|
print_with_c_locale(f'Cleaned output folder to keep only the last {options.keep_results} results!')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user