added axes_map computation

This commit is contained in:
Félix Boisselier
2023-12-01 00:08:32 +01:00
parent 7ba692954f
commit 8721488d8c
13 changed files with 298 additions and 47 deletions

View File

@@ -0,0 +1,171 @@
#!/usr/bin/env python3
######################################
###### AXE_MAP DETECTION SCRIPT ######
######################################
# 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 numpy as np
import locale
from scipy.signal import butter, filtfilt
NUM_POINTS = 500
# Set the best locale for time and date formating (generation of the titles)
try:
locale.setlocale(locale.LC_TIME, locale.getdefaultlocale())
except locale.Error:
locale.setlocale(locale.LC_TIME, 'C')
# Override the built-in print function to avoid problem in Klipper due to locale settings
original_print = print
def print_with_c_locale(*args, **kwargs):
original_locale = locale.setlocale(locale.LC_ALL, None)
locale.setlocale(locale.LC_ALL, 'C')
original_print(*args, **kwargs)
locale.setlocale(locale.LC_ALL, original_locale)
print = print_with_c_locale
######################################################################
# Computation
######################################################################
def accel_signal_filter(data, cutoff=2, fs=100, order=5):
nyq = 0.5 * fs
normal_cutoff = cutoff / nyq
b, a = butter(order, normal_cutoff, btype='low', analog=False)
filtered_data = filtfilt(b, a, data)
filtered_data -= np.mean(filtered_data)
return filtered_data
def find_first_spike(data):
min_index, max_index = np.argmin(data), np.argmax(data)
return ('-', min_index) if min_index < max_index else ('', max_index)
def get_movement_vector(data, start_idx, end_idx):
if start_idx < end_idx:
vector = []
for i in range(3):
vector.append(np.mean(data[i][start_idx:end_idx], axis=0))
return vector
else:
return np.zeros(3)
def angle_between(v1, v2):
v1_u = v1 / np.linalg.norm(v1)
v2_u = v2 / np.linalg.norm(v2)
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):
# 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]
# Theoretical unit vectors for X, Y, Z printer axes
printer_axes = {
'x': np.array([1, 0, 0]),
'y': np.array([0, 1, 0]),
'z': np.array([0, 0, 1])
}
alignment_errors = {}
sensitivity_errors = {}
for i, axis in enumerate(['x', 'y', 'z']):
movement_start = movement_starts[i]
movement_end = movement_start + num_points
movement_vector = get_movement_vector(filtered_data, movement_start, movement_end)
alignment_errors[axis] = angle_between(movement_vector, printer_axes[axis])
measured_accel_magnitude = np.linalg.norm(movement_vector)
if accel_value != 0:
sensitivity_errors[axis] = abs(measured_accel_magnitude - accel_value) / accel_value * 100
else:
sensitivity_errors[axis] = None
return alignment_errors, sensitivity_errors
######################################################################
# Startup and main routines
######################################################################
def parse_log(logname):
with open(logname) as f:
for header in f:
if not header.startswith('#'):
break
if not header.startswith('freq,psd_x,psd_y,psd_z,psd_xyz'):
# Raw accelerometer data
return np.loadtxt(logname, comments='#', delimiter=',')
# Power spectral density data or shaper calibration data
raise ValueError("File %s does not contain raw accelerometer data and therefore "
"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):
# Parse the raw data and get them ready for analysis
raw_datas = [parse_log(filename) for filename in lognames]
if len(raw_datas) > 1:
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)]
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])
# 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])
# alignment_error, sensitivity_error = compute_errors(filtered_data, spikes_sorted, accel, NUM_POINTS)
results = f"Detected axes_map:\n {axes_map}\n"
# TODO: work on this function that is currently not giving good results...
# results += "Accelerometer angle deviation:\n"
# for axis, angle in alignment_error.items():
# angle_degrees = np.degrees(angle) # Convert radians to degrees
# results += f" {axis.upper()} axis: {angle_degrees:.2f} degrees\n"
# results += "Accelerometer sensitivity error:\n"
# for axis, error in sensitivity_error.items():
# results += f" {axis.upper()} axis: {error:.2f}%\n"
return results
def main():
# Parse command-line arguments
usage = "%prog [options] <raw logs>"
opts = optparse.OptionParser(usage)
opts.add_option("-o", "--output", type="string", dest="output",
default=None, help="filename of output graph")
opts.add_option("-a", "--accel", type="string", dest="accel",
default=None, help="acceleration value used to do the movements")
options, args = opts.parse_args()
if len(args) < 1:
opts.error("No CSV file(s) to analyse")
if options.accel is None:
opts.error("You must specify the acceleration value used when generating the CSV file (option -a)")
try:
accel_value = float(options.accel)
except ValueError:
opts.error("Invalid acceleration value. It should be a numeric value.")
results = axesmap_calibration(args, accel_value)
print(results)
if options.output is not None:
with open(options.output, 'w') as f:
f.write(results)
if __name__ == '__main__':
main()

View File

@@ -317,7 +317,7 @@ def parse_log(logname):
return np.loadtxt(logname, comments='#', delimiter=',')
# Power spectral density data or shaper calibration data
raise ValueError("File %s does not contain raw accelerometer data and therefore "
"is not supported by graph_vibrations.py script. Please use "
"is not supported by this script. Please use the official Klipper"
"calibrate_shaper.py script to process it instead." % (logname,))
@@ -326,7 +326,7 @@ def extract_speed(logname):
speed = re.search('sp(.+?)n', os.path.basename(logname)).group(1).replace('_','.')
except AttributeError:
raise ValueError("File %s does not contain speed in its name and therefore "
"is not supported by graph_vibrations.py script." % (logname,))
"is not supported by this script." % (logname,))
return float(speed)

View File

@@ -30,6 +30,7 @@ STORE_RESULTS = 3
from graph_belts import belts_calibration
from graph_shaper import shaper_calibration
from graph_vibrations import vibrations_calibration
from analyze_axesmap import axesmap_calibration
RESULTS_SUBFOLDERS = ['belts', 'inputshaper', 'vibrations']
@@ -50,7 +51,7 @@ def is_file_open(filepath):
return False
def get_belts_graph():
def create_belts_graph():
current_date = datetime.now().strftime('%Y%m%d_%H%M%S')
lognames = []
@@ -61,6 +62,7 @@ def get_belts_graph():
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]:
@@ -84,11 +86,12 @@ def get_belts_graph():
# 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')
return fig, png_filename
fig.savefig(png_filename)
return
def get_shaper_graph():
def create_shaper_graph():
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
@@ -96,6 +99,7 @@ def get_shaper_graph():
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]
@@ -117,10 +121,11 @@ def get_shaper_graph():
fig = shaper_calibration([new_file], KLIPPER_FOLDER)
png_filename = os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[1], f'resonances_{current_date}_{axis}.png')
return fig, png_filename
fig.savefig(png_filename)
return
def get_vibrations_graph(axis_name):
def create_vibrations_graph(axis_name):
current_date = datetime.now().strftime('%Y%m%d_%H%M%S')
lognames = []
@@ -159,7 +164,35 @@ def get_vibrations_graph(axis_name):
tar.add(csv_file, recursive=False)
os.remove(csv_file)
return fig, png_filename
fig.savefig(png_filename)
return
def find_axesmap(accel):
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('/tmp/adxl345-*.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
@@ -210,20 +243,21 @@ def main():
os.makedirs(folder)
if len(sys.argv) < 2:
print("Usage: is_workflow.py [SHAPER|BELTS|VIBRATIONS]")
print("Usage: is_workflow.py [BELTS|SHAPER|VIBRATIONS|AXESMAP]")
sys.exit(1)
if sys.argv[1].lower() == 'belts':
fig, png_filename = get_belts_graph()
create_belts_graph()
elif sys.argv[1].lower() == 'shaper':
fig, png_filename = get_shaper_graph()
create_shaper_graph()
elif sys.argv[1].lower() == 'vibrations':
fig, png_filename = get_vibrations_graph(axis_name=sys.argv[2])
create_vibrations_graph(axis_name=sys.argv[2])
elif sys.argv[1].lower() == 'axesmap':
find_axesmap(accel=sys.argv[2])
else:
print("Usage: is_workflow.py [SHAPER|BELTS|VIBRATIONS]")
print("Usage: is_workflow.py [BELTS|SHAPER|VIBRATIONS|AXESMAP]")
sys.exit(1)
fig.savefig(png_filename)
clean_files()
print(f"Graphs created. You will find the results in {RESULTS_FOLDER}")