mirror of
https://github.com/Klipper3d/klipper.git
synced 2025-12-15 12:49:55 +01:00
shaper_calibrate: Reworked multi-file shaper autocalibration
Signed-off-by: Dmitry Butyugin <dmbutyugin@google.com>
This commit is contained in:
committed by
KevinOConnor
parent
b4c7cf4a33
commit
2aff840f68
@@ -307,7 +307,7 @@ class ResonanceTester:
|
||||
"'%s' is not an accelerometer" % chip_name)
|
||||
self.accel_chips.append((chip_axis, chip))
|
||||
|
||||
def _run_test(self, gcmd, axes, helper, raw_name_suffix=None,
|
||||
def _run_test(self, gcmd, axes, helper, name_suffix, raw_name_suffix=None,
|
||||
accel_chips=None, test_point=None):
|
||||
toolhead = self.printer.lookup_object('toolhead')
|
||||
calibration_data = {axis: None for axis in axes}
|
||||
@@ -367,7 +367,12 @@ class ResonanceTester:
|
||||
raise gcmd.error(
|
||||
"accelerometer '%s' measured no data" % (
|
||||
chip_name,))
|
||||
new_data = helper.process_accelerometer_data(aclient)
|
||||
name = self.get_filename(
|
||||
'resonances', name_suffix, axis,
|
||||
point if len(test_points) > 1 else None,
|
||||
chip_name if (accel_chips is not None
|
||||
or len(raw_values) > 1) else None)
|
||||
new_data = helper.process_accelerometer_data(name, aclient)
|
||||
if calibration_data[axis] is None:
|
||||
calibration_data[axis] = new_data
|
||||
else:
|
||||
@@ -427,7 +432,7 @@ class ResonanceTester:
|
||||
helper = None
|
||||
|
||||
data = self._run_test(
|
||||
gcmd, [axis], helper,
|
||||
gcmd, [axis], helper, name_suffix,
|
||||
raw_name_suffix=name_suffix if raw_output else None,
|
||||
accel_chips=accel_chips, test_point=test_point)[axis]
|
||||
if csv_output:
|
||||
@@ -463,7 +468,7 @@ class ResonanceTester:
|
||||
helper = shaper_calibrate.ShaperCalibrate(self.printer)
|
||||
|
||||
calibration_data = self._run_test(gcmd, calibrate_axes, helper,
|
||||
accel_chips=accel_chips)
|
||||
name_suffix, accel_chips=accel_chips)
|
||||
|
||||
configfile = self.printer.lookup_object('configfile')
|
||||
for axis in calibrate_axes:
|
||||
@@ -512,7 +517,7 @@ class ResonanceTester:
|
||||
raise gcmd.error(
|
||||
"%s-axis accelerometer measured no data" % (
|
||||
chip_axis,))
|
||||
data = helper.process_accelerometer_data(aclient)
|
||||
data = helper.process_accelerometer_data(name=None, data=aclient)
|
||||
vx = data.psd_x.mean()
|
||||
vy = data.psd_y.mean()
|
||||
vz = data.psd_z.mean()
|
||||
|
||||
@@ -20,7 +20,8 @@ AUTOTUNE_SHAPERS = ['zv', 'mzv', 'ei', '2hump_ei', '3hump_ei']
|
||||
######################################################################
|
||||
|
||||
class CalibrationData:
|
||||
def __init__(self, freq_bins, psd_sum, psd_x, psd_y, psd_z):
|
||||
def __init__(self, name, freq_bins, psd_sum, psd_x, psd_y, psd_z):
|
||||
self.name = name
|
||||
self.freq_bins = freq_bins
|
||||
self.psd_sum = psd_sum
|
||||
self.psd_x = psd_x
|
||||
@@ -29,35 +30,37 @@ class CalibrationData:
|
||||
self._psd_list = [self.psd_sum, self.psd_x, self.psd_y, self.psd_z]
|
||||
self._psd_map = {'x': self.psd_x, 'y': self.psd_y, 'z': self.psd_z,
|
||||
'all': self.psd_sum}
|
||||
self.data_sets = 1
|
||||
self._normalized = False
|
||||
self.data_sets = []
|
||||
def add_data(self, other):
|
||||
np = self.numpy
|
||||
joined_data_sets = self.data_sets + other.data_sets
|
||||
for psd, other_psd in zip(self._psd_list, other._psd_list):
|
||||
# `other` data may be defined at different frequency bins,
|
||||
# interpolating to fix that.
|
||||
other_normalized = other.data_sets * np.interp(
|
||||
self.freq_bins, other.freq_bins, other_psd)
|
||||
psd *= self.data_sets
|
||||
psd[:] = (psd + other_normalized) * (1. / joined_data_sets)
|
||||
self.data_sets = joined_data_sets
|
||||
self.data_sets.extend(other.get_datasets())
|
||||
def set_numpy(self, numpy):
|
||||
self.numpy = numpy
|
||||
def normalize_to_frequencies(self):
|
||||
for psd in self._psd_list:
|
||||
# Avoid division by zero errors
|
||||
psd /= self.freq_bins + .1
|
||||
# Remove low-frequency noise
|
||||
low_freqs = self.freq_bins < 2. * MIN_FREQ
|
||||
psd[low_freqs] *= self.numpy.exp(
|
||||
-(2. * MIN_FREQ / (self.freq_bins[low_freqs] + .1))**2 + 1.)
|
||||
if not self._normalized:
|
||||
for psd in self._psd_list:
|
||||
if psd is None:
|
||||
continue
|
||||
# Avoid division by zero errors
|
||||
psd /= self.freq_bins + .1
|
||||
# Remove low-frequency noise
|
||||
low_freqs = self.freq_bins < 2. * MIN_FREQ
|
||||
psd[low_freqs] *= self.numpy.exp(
|
||||
-(2. * MIN_FREQ / (
|
||||
self.freq_bins[low_freqs] + .1))**2 + 1.)
|
||||
self._normalized = True
|
||||
for other in self.data_sets:
|
||||
other.normalize_to_frequencies()
|
||||
def get_psd(self, axis='all'):
|
||||
return self._psd_map[axis]
|
||||
def get_datasets(self):
|
||||
return [self] + self.data_sets
|
||||
|
||||
|
||||
CalibrationResult = collections.namedtuple(
|
||||
'CalibrationResult',
|
||||
('name', 'freq', 'vals', 'vibrs', 'smoothing', 'score', 'max_accel'))
|
||||
('name', 'freq', 'freq_bins', 'vals', 'vibrs',
|
||||
'smoothing', 'score', 'max_accel'))
|
||||
|
||||
class ShaperCalibrate:
|
||||
def __init__(self, printer):
|
||||
@@ -147,7 +150,7 @@ class ShaperCalibrate:
|
||||
freqs = np.fft.rfftfreq(nfft, 1. / fs)
|
||||
return freqs, psd
|
||||
|
||||
def calc_freq_response(self, raw_values):
|
||||
def calc_freq_response(self, name, raw_values):
|
||||
np = self.numpy
|
||||
if raw_values is None:
|
||||
return None
|
||||
@@ -172,11 +175,11 @@ class ShaperCalibrate:
|
||||
fx, px = self._psd(data[:,1], SAMPLING_FREQ, M)
|
||||
fy, py = self._psd(data[:,2], SAMPLING_FREQ, M)
|
||||
fz, pz = self._psd(data[:,3], SAMPLING_FREQ, M)
|
||||
return CalibrationData(fx, px+py+pz, px, py, pz)
|
||||
return CalibrationData(name, fx, px+py+pz, px, py, pz)
|
||||
|
||||
def process_accelerometer_data(self, data):
|
||||
def process_accelerometer_data(self, name, data):
|
||||
calibration_data = self.background_process_exec(
|
||||
self.calc_freq_response, (data,))
|
||||
self.calc_freq_response, (name, data))
|
||||
if calibration_data is None:
|
||||
raise self.error(
|
||||
"Internal error processing accelerometer data %s" % (data,))
|
||||
@@ -250,36 +253,50 @@ class ShaperCalibrate:
|
||||
|
||||
max_freq = max(max_freq or MAX_FREQ, test_freqs.max())
|
||||
|
||||
freq_bins = calibration_data.freq_bins
|
||||
psd = calibration_data.psd_sum[freq_bins <= max_freq]
|
||||
freq_bins = freq_bins[freq_bins <= max_freq]
|
||||
|
||||
best_res = None
|
||||
results = []
|
||||
min_freq = max_freq
|
||||
for data in calibration_data.get_datasets():
|
||||
min_freq = min(min_freq, data.freq_bins.min())
|
||||
for test_freq in test_freqs[::-1]:
|
||||
shaper_vibrations = 0.
|
||||
shaper_vals = np.zeros(shape=freq_bins.shape)
|
||||
shaper = shaper_cfg.init_func(test_freq, damping_ratio)
|
||||
shaper_smoothing = self._get_shaper_smoothing(shaper, scv=scv)
|
||||
if max_smoothing and shaper_smoothing > max_smoothing and best_res:
|
||||
return best_res
|
||||
# Exact damping ratio of the printer is unknown, pessimizing
|
||||
# remaining vibrations over possible damping values
|
||||
for dr in test_damping_ratios:
|
||||
vibrations, vals = self._estimate_remaining_vibrations(
|
||||
shaper, dr, freq_bins, psd)
|
||||
shaper_vals = np.maximum(shaper_vals, vals)
|
||||
if vibrations > shaper_vibrations:
|
||||
shaper_vibrations = vibrations
|
||||
max_accel = self.find_shaper_max_accel(shaper, scv)
|
||||
all_shaper_vals = []
|
||||
|
||||
for data in calibration_data.get_datasets():
|
||||
freq_bins = data.freq_bins
|
||||
psd = data.psd_sum[freq_bins <= max_freq]
|
||||
freq_bins = freq_bins[freq_bins <= max_freq]
|
||||
|
||||
shaper_vals = np.zeros(shape=freq_bins.shape)
|
||||
# Exact damping ratio of the printer is unknown, pessimizing
|
||||
# remaining vibrations over possible damping values
|
||||
for dr in test_damping_ratios:
|
||||
vibrations, vals = self._estimate_remaining_vibrations(
|
||||
shaper, dr, freq_bins, psd)
|
||||
shaper_vals = np.maximum(shaper_vals, vals)
|
||||
if vibrations > shaper_vibrations:
|
||||
shaper_vibrations = vibrations
|
||||
all_shaper_vals.append((freq_bins, shaper_vals))
|
||||
# The score trying to minimize vibrations, but also accounting
|
||||
# the growth of smoothing. The formula itself does not have any
|
||||
# special meaning, it simply shows good results on real user data
|
||||
shaper_score = shaper_smoothing * (shaper_vibrations**1.5 +
|
||||
shaper_vibrations * .2 + .01)
|
||||
shaper_freq_bins = np.arange(min_freq, max_freq, 0.2)
|
||||
shaper_vals = np.zeros(shape=shaper_freq_bins.shape)
|
||||
for freq_bins, vals in all_shaper_vals:
|
||||
shaper_vals = np.maximum(
|
||||
shaper_vals, np.interp(shaper_freq_bins,
|
||||
freq_bins, vals))
|
||||
results.append(
|
||||
CalibrationResult(
|
||||
name=shaper_cfg.name, freq=test_freq, vals=shaper_vals,
|
||||
name=shaper_cfg.name, freq=test_freq,
|
||||
freq_bins=shaper_freq_bins, vals=shaper_vals,
|
||||
vibrs=shaper_vibrations, smoothing=shaper_smoothing,
|
||||
score=shaper_score, max_accel=max_accel))
|
||||
if best_res is None or best_res.vibrs > results[-1].vibrs:
|
||||
@@ -370,27 +387,56 @@ class ShaperCalibrate:
|
||||
"SHAPER_TYPE_" + axis: shaper_name,
|
||||
"SHAPER_FREQ_" + axis: shaper_freq}))
|
||||
|
||||
def _escape_for_csv(self, name):
|
||||
name = name.replace('"', '""')
|
||||
if ',' in name:
|
||||
return '"' + name + '"'
|
||||
return name
|
||||
|
||||
def save_calibration_data(self, output, calibration_data, shapers=None,
|
||||
max_freq=None):
|
||||
try:
|
||||
np = calibration_data.numpy
|
||||
datasets = calibration_data.get_datasets()
|
||||
max_freq = max_freq or MAX_FREQ
|
||||
with open(output, "w") as csvfile:
|
||||
csvfile.write("freq,psd_x,psd_y,psd_z,psd_xyz")
|
||||
if len(datasets) > 1:
|
||||
if shapers:
|
||||
freq_bins = shapers[0].freq_bins
|
||||
else:
|
||||
min_freq = max_freq
|
||||
for data in datasets:
|
||||
min_freq = min(min_freq, data.freq_bins.min())
|
||||
freq_bins = np.arange(min_freq, max_freq, 0.2)
|
||||
psd_data_to_write = []
|
||||
for data in datasets:
|
||||
psd_data_to_write.append(np.interp(
|
||||
freq_bins, data.freq_bins, data.psd_sum))
|
||||
else:
|
||||
freq_bins = calibration_data.freq_bins
|
||||
psd_data_to_write = [
|
||||
calibration_data.psd_x, calibration_data.psd_y,
|
||||
calibration_data.psd_z, calibration_data.psd_sum]
|
||||
with open(output, "w") as csvfile:
|
||||
csvfile.write("freq,")
|
||||
if len(datasets) > 1:
|
||||
csvfile.write(','.join([self._escape_for_csv(d.name)
|
||||
for d in datasets]))
|
||||
else:
|
||||
csvfile.write("psd_x,psd_y,psd_z,psd_xyz")
|
||||
if shapers:
|
||||
csvfile.write(',shapers:')
|
||||
for shaper in shapers:
|
||||
csvfile.write(",%s(%.1f)" % (shaper.name, shaper.freq))
|
||||
csvfile.write("\n")
|
||||
num_freqs = calibration_data.freq_bins.shape[0]
|
||||
num_freqs = freq_bins.shape[0]
|
||||
for i in range(num_freqs):
|
||||
if calibration_data.freq_bins[i] >= max_freq:
|
||||
if freq_bins[i] >= max_freq:
|
||||
break
|
||||
csvfile.write("%.1f,%.3e,%.3e,%.3e,%.3e" % (
|
||||
calibration_data.freq_bins[i],
|
||||
calibration_data.psd_x[i],
|
||||
calibration_data.psd_y[i],
|
||||
calibration_data.psd_z[i],
|
||||
calibration_data.psd_sum[i]))
|
||||
csvfile.write("%.1f" % freq_bins[i])
|
||||
for psd in psd_data_to_write:
|
||||
csvfile.write(",%.3e" % psd[i])
|
||||
if shapers:
|
||||
csvfile.write(',')
|
||||
for shaper in shapers:
|
||||
csvfile.write(",%.3f" % (shaper.vals[i],))
|
||||
csvfile.write("\n")
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
# Shaper auto-calibration script
|
||||
#
|
||||
# Copyright (C) 2020-2024 Dmitry Butyugin <dmbutyugin@google.com>
|
||||
# Copyright (C) 2020-2025 Dmitry Butyugin <dmbutyugin@google.com>
|
||||
# Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
from __future__ import print_function
|
||||
import importlib, optparse, os, sys
|
||||
import csv, importlib, optparse, os, sys
|
||||
from textwrap import wrap
|
||||
import numpy as np, matplotlib
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)),
|
||||
@@ -20,18 +20,39 @@ def parse_log(logname):
|
||||
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=',')
|
||||
if not header.startswith('freq,'):
|
||||
# Process raw accelerometer data
|
||||
data = np.loadtxt(logname, comments='#', delimiter=',')
|
||||
helper = shaper_calibrate.ShaperCalibrate(printer=None)
|
||||
calibration_data = helper.process_accelerometer_data(logname, data)
|
||||
calibration_data.normalize_to_frequencies()
|
||||
return calibration_data
|
||||
# Parse power spectral density data
|
||||
data = np.loadtxt(logname, skiprows=1, comments='#', delimiter=',')
|
||||
calibration_data = shaper_calibrate.CalibrationData(
|
||||
freq_bins=data[:,0], psd_sum=data[:,4],
|
||||
psd_x=data[:,1], psd_y=data[:,2], psd_z=data[:,3])
|
||||
calibration_data.set_numpy(np)
|
||||
data = np.genfromtxt(logname, dtype=np.float64, skip_header=1,
|
||||
comments='#', delimiter=',', filling_values=0.)
|
||||
if header.startswith('freq,psd_x,psd_y,psd_z,psd_xyz'):
|
||||
calibration_data = shaper_calibrate.CalibrationData(
|
||||
name=logname, freq_bins=data[:,0], psd_sum=data[:,4],
|
||||
psd_x=data[:,1], psd_y=data[:,2], psd_z=data[:,3])
|
||||
calibration_data.set_numpy(np)
|
||||
else:
|
||||
parsed_header = next(csv.reader([header], delimiter=','))
|
||||
calibration_data = None
|
||||
for i, dataset_name in enumerate(parsed_header[1:]):
|
||||
if dataset_name == 'shapers:':
|
||||
break
|
||||
cdata = shaper_calibrate.CalibrationData(
|
||||
name=dataset_name, freq_bins=data[:,0], psd_sum=data[:,i+1],
|
||||
# Individual per-axis data is not stored
|
||||
psd_x=None, psd_y=None, psd_z=None)
|
||||
cdata.set_numpy(np)
|
||||
if calibration_data is None:
|
||||
calibration_data = cdata
|
||||
else:
|
||||
calibration_data.add_data(cdata)
|
||||
# If input shapers are present in the CSV file, the frequency
|
||||
# response is already normalized to input frequencies
|
||||
if 'mzv' not in header:
|
||||
if ',shapers:' not in header:
|
||||
calibration_data.normalize_to_frequencies()
|
||||
return calibration_data
|
||||
|
||||
@@ -43,19 +64,14 @@ def parse_log(logname):
|
||||
def calibrate_shaper(datas, csv_output, *, shapers, damping_ratio, scv,
|
||||
shaper_freqs, max_smoothing, test_damping_ratios,
|
||||
max_freq):
|
||||
# Combine accelerometer data
|
||||
calibration_data = datas[0]
|
||||
for data in datas[1:]:
|
||||
calibration_data.add_data(data)
|
||||
|
||||
print("Processing resonances from %s"
|
||||
% ",".join(d.name for d in calibration_data.get_datasets()))
|
||||
helper = shaper_calibrate.ShaperCalibrate(printer=None)
|
||||
if isinstance(datas[0], shaper_calibrate.CalibrationData):
|
||||
calibration_data = datas[0]
|
||||
for data in datas[1:]:
|
||||
calibration_data.add_data(data)
|
||||
else:
|
||||
# Process accelerometer data
|
||||
calibration_data = helper.process_accelerometer_data(datas[0])
|
||||
for data in datas[1:]:
|
||||
calibration_data.add_data(helper.process_accelerometer_data(data))
|
||||
calibration_data.normalize_to_frequencies()
|
||||
|
||||
|
||||
shaper, all_shapers = helper.find_best_shaper(
|
||||
calibration_data, shapers=shapers, damping_ratio=damping_ratio,
|
||||
scv=scv, shaper_freqs=shaper_freqs, max_smoothing=max_smoothing,
|
||||
@@ -75,32 +91,48 @@ def calibrate_shaper(datas, csv_output, *, shapers, damping_ratio, scv,
|
||||
# Plot frequency response and suggested input shapers
|
||||
######################################################################
|
||||
|
||||
def plot_freq_response(lognames, calibration_data, shapers,
|
||||
def plot_freq_response(calibration_data, shapers,
|
||||
selected_shaper, max_freq):
|
||||
max_freq_bin = calibration_data.freq_bins.max()
|
||||
selected_shaper_data = [s for s in shapers if s.name == selected_shaper][0]
|
||||
max_freq_bin = selected_shaper_data.freq_bins.max()
|
||||
if max_freq > max_freq_bin:
|
||||
max_freq = max_freq_bin
|
||||
freqs = calibration_data.freq_bins
|
||||
psd = calibration_data.psd_sum[freqs <= max_freq]
|
||||
px = calibration_data.psd_x[freqs <= max_freq]
|
||||
py = calibration_data.psd_y[freqs <= max_freq]
|
||||
pz = calibration_data.psd_z[freqs <= max_freq]
|
||||
freqs = freqs[freqs <= max_freq]
|
||||
|
||||
fontP = matplotlib.font_manager.FontProperties()
|
||||
fontP.set_size('x-small')
|
||||
|
||||
fig, ax = matplotlib.pyplot.subplots()
|
||||
fig, ax = matplotlib.pyplot.subplots(figsize=(8, 5))
|
||||
ax.set_xlabel('Frequency, Hz')
|
||||
ax.set_xlim([0, max_freq])
|
||||
ax.set_ylabel('Power spectral density')
|
||||
|
||||
ax.plot(freqs, psd, label='X+Y+Z', color='purple')
|
||||
ax.plot(freqs, px, label='X', color='red')
|
||||
ax.plot(freqs, py, label='Y', color='green')
|
||||
ax.plot(freqs, pz, label='Z', color='blue')
|
||||
datasets = calibration_data.get_datasets()
|
||||
if len(datasets) == 1:
|
||||
freqs = calibration_data.freq_bins
|
||||
psd = calibration_data.psd_sum[freqs <= max_freq]
|
||||
px = calibration_data.psd_x[freqs <= max_freq]
|
||||
py = calibration_data.psd_y[freqs <= max_freq]
|
||||
pz = calibration_data.psd_z[freqs <= max_freq]
|
||||
freqs = freqs[freqs <= max_freq]
|
||||
after_shaper = np.interp(selected_shaper_data.freq_bins, freqs, psd)
|
||||
ax.plot(freqs, psd, label='X+Y+Z', color='purple')
|
||||
ax.plot(freqs, px, label='X', color='red')
|
||||
ax.plot(freqs, py, label='Y', color='green')
|
||||
ax.plot(freqs, pz, label='Z', color='blue')
|
||||
title = "Frequency response and shapers (%s)" % calibration_data.name
|
||||
else:
|
||||
after_shaper = np.zeros(shape=selected_shaper_data.freq_bins.shape)
|
||||
for data in datasets:
|
||||
freqs = data.freq_bins
|
||||
psd = data.psd_sum[freqs <= max_freq]
|
||||
freqs = freqs[freqs <= max_freq]
|
||||
after_shaper = np.maximum(
|
||||
after_shaper, np.interp(selected_shaper_data.freq_bins,
|
||||
freqs, psd))
|
||||
ax.plot(freqs, psd, label=data.name)
|
||||
title = "Frequency responses and shapers"
|
||||
after_shaper *= selected_shaper_data.vals
|
||||
|
||||
title = "Frequency response and shapers (%s)" % (', '.join(lognames))
|
||||
ax.set_title("\n".join(wrap(title, MAX_TITLE_LENGTH)))
|
||||
ax.xaxis.set_minor_locator(matplotlib.ticker.MultipleLocator(5))
|
||||
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||
@@ -119,10 +151,11 @@ def plot_freq_response(lognames, calibration_data, shapers,
|
||||
linestyle = 'dotted'
|
||||
if shaper.name == selected_shaper:
|
||||
linestyle = 'dashdot'
|
||||
best_shaper_vals = shaper.vals
|
||||
ax2.plot(freqs, shaper.vals, label=label, linestyle=linestyle)
|
||||
ax.plot(freqs, psd * best_shaper_vals,
|
||||
label='After\nshaper', color='cyan')
|
||||
ax2.plot(shaper.freq_bins, shaper.vals,
|
||||
label=label, linestyle=linestyle)
|
||||
ax.plot(selected_shaper_data.freq_bins, after_shaper,
|
||||
label='After%sshaper' % ('\n' if len(datasets) == 1 else ' '),
|
||||
color='cyan')
|
||||
# A hack to add a human-readable shaper recommendation to legend
|
||||
ax2.plot([], [], ' ',
|
||||
label="Recommended shaper: %s" % (selected_shaper.upper()))
|
||||
@@ -153,7 +186,7 @@ def main():
|
||||
default=None, help="filename of output graph")
|
||||
opts.add_option("-c", "--csv", type="string", dest="csv",
|
||||
default=None, help="filename of output csv file")
|
||||
opts.add_option("-f", "--max_freq", type="float", default=200.,
|
||||
opts.add_option("-f", "--max_freq", type="float", default=None,
|
||||
help="maximum frequency to plot")
|
||||
opts.add_option("-s", "--max_smoothing", type="float", dest="max_smoothing",
|
||||
default=None, help="maximum shaper smoothing to allow")
|
||||
@@ -201,13 +234,15 @@ def main():
|
||||
opts.error("--shaper_freq param does not specify correct range " +
|
||||
"in the format [start]:end[:step]")
|
||||
shaper_freqs = (freq_start, freq_end, freq_step)
|
||||
max_freq = max(max_freq, freq_end * 4./3.)
|
||||
if max_freq is not None:
|
||||
max_freq = max(max_freq, freq_end * 4./3.)
|
||||
else:
|
||||
try:
|
||||
shaper_freqs = [float(s) for s in options.shaper_freq.split(',')]
|
||||
except ValueError:
|
||||
opts.error("invalid floating point value in --shaper_freq param")
|
||||
max_freq = max(max_freq, max(shaper_freqs) * 4./3.)
|
||||
if max_freq is not None:
|
||||
max_freq = max(max_freq, max(shaper_freqs) * 4./3.)
|
||||
if options.test_damping_ratios:
|
||||
try:
|
||||
test_damping_ratios = [float(s) for s in
|
||||
@@ -235,12 +270,15 @@ def main():
|
||||
max_freq=max_freq)
|
||||
if selected_shaper is None:
|
||||
return
|
||||
|
||||
if max_freq is None:
|
||||
max_freq = 0.
|
||||
for data in calibration_data.get_datasets():
|
||||
max_freq = max(max_freq, data.freq_bins.max())
|
||||
if not options.csv or options.output:
|
||||
# Draw graph
|
||||
setup_matplotlib(options.output is not None)
|
||||
|
||||
fig = plot_freq_response(args, calibration_data, shapers,
|
||||
fig = plot_freq_response(calibration_data, shapers,
|
||||
selected_shaper, max_freq)
|
||||
|
||||
# Show graph
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
# Generate adxl345 accelerometer graphs
|
||||
#
|
||||
# Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>
|
||||
# Copyright (C) 2020 Dmitry Butyugin <dmbutyugin@google.com>
|
||||
# Copyright (C) 2020-2025 Dmitry Butyugin <dmbutyugin@google.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import importlib, optparse, os, sys
|
||||
import csv, importlib, optparse, os, sys
|
||||
from textwrap import wrap
|
||||
import numpy as np, matplotlib
|
||||
sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)),
|
||||
@@ -19,32 +19,49 @@ def parse_log(logname, opts):
|
||||
for header in f:
|
||||
if header.startswith('#'):
|
||||
continue
|
||||
if header.startswith('freq,psd_x,psd_y,psd_z,psd_xyz'):
|
||||
if header.startswith('freq,'):
|
||||
# Processed power spectral density file
|
||||
break
|
||||
# Raw accelerometer data
|
||||
return np.loadtxt(logname, comments='#', delimiter=',')
|
||||
# Parse power spectral density data
|
||||
data = np.loadtxt(logname, skiprows=1, comments='#', delimiter=',')
|
||||
calibration_data = shaper_calibrate.CalibrationData(
|
||||
freq_bins=data[:,0], psd_sum=data[:,4],
|
||||
psd_x=data[:,1], psd_y=data[:,2], psd_z=data[:,3])
|
||||
calibration_data.set_numpy(np)
|
||||
data = np.genfromtxt(logname, dtype=np.float64, skip_header=1,
|
||||
comments='#', delimiter=',', filling_values=0.)
|
||||
if header.startswith('freq,psd_x,psd_y,psd_z,psd_xyz'):
|
||||
calibration_data = shaper_calibrate.CalibrationData(
|
||||
name=logname, freq_bins=data[:,0], psd_sum=data[:,4],
|
||||
psd_x=data[:,1], psd_y=data[:,2], psd_z=data[:,3])
|
||||
calibration_data.set_numpy(np)
|
||||
else:
|
||||
parsed_header = next(csv.reader([header], delimiter=','))
|
||||
calibration_data = None
|
||||
for i, dataset_name in enumerate(parsed_header[1:]):
|
||||
if dataset_name == 'shapers:':
|
||||
break
|
||||
cdata = shaper_calibrate.CalibrationData(
|
||||
name=dataset_name, freq_bins=data[:,0], psd_sum=data[:,i+1],
|
||||
# Individual axes data is not stored
|
||||
psd_x=None, psd_y=None, psd_z=None)
|
||||
cdata.set_numpy(np)
|
||||
if calibration_data is None:
|
||||
calibration_data = cdata
|
||||
else:
|
||||
calibration_data.add_data(cdata)
|
||||
return calibration_data
|
||||
|
||||
######################################################################
|
||||
# Raw accelerometer graphing
|
||||
######################################################################
|
||||
|
||||
def plot_accel(datas, lognames):
|
||||
def plot_accel(opts, datas, lognames):
|
||||
fig, axes = matplotlib.pyplot.subplots(nrows=3, sharex=True)
|
||||
axes[0].set_title("\n".join(wrap(
|
||||
"Accelerometer data (%s)" % (', '.join(lognames)), MAX_TITLE_LENGTH)))
|
||||
axis_names = ['x', 'y', 'z']
|
||||
for data, logname in zip(datas, lognames):
|
||||
if isinstance(data, shaper_calibrate.CalibrationData):
|
||||
raise error("Cannot plot raw accelerometer data using the processed"
|
||||
" resonances, raw_data input is required")
|
||||
opts.error("Cannot plot raw accelerometer data using the processed"
|
||||
" resonances, raw_data input is required")
|
||||
first_time = data[0, 0]
|
||||
times = data[:,0] - first_time
|
||||
for i in range(len(axis_names)):
|
||||
@@ -70,16 +87,13 @@ def plot_accel(datas, lognames):
|
||||
######################################################################
|
||||
|
||||
# Calculate estimated "power spectral density"
|
||||
def calc_freq_response(data, max_freq):
|
||||
def calc_freq_response(name, data, max_freq):
|
||||
if isinstance(data, shaper_calibrate.CalibrationData):
|
||||
return data
|
||||
helper = shaper_calibrate.ShaperCalibrate(printer=None)
|
||||
return helper.process_accelerometer_data(data)
|
||||
return helper.process_accelerometer_data(name, data)
|
||||
|
||||
def calc_specgram(data, axis):
|
||||
if isinstance(data, shaper_calibrate.CalibrationData):
|
||||
raise error("Cannot calculate the spectrogram using the processed"
|
||||
" resonances, raw_data input is required")
|
||||
N = data.shape[0]
|
||||
Fs = N / (data[-1,0] - data[0,0])
|
||||
# Round up to a power of 2 for faster FFT
|
||||
@@ -99,28 +113,45 @@ def calc_specgram(data, axis):
|
||||
pdata += _specgram(d[ax])[0]
|
||||
return pdata, bins, t
|
||||
|
||||
def plot_frequency(datas, lognames, max_freq):
|
||||
calibration_data = calc_freq_response(datas[0], max_freq)
|
||||
for data in datas[1:]:
|
||||
calibration_data.add_data(calc_freq_response(data, max_freq))
|
||||
freqs = calibration_data.freq_bins
|
||||
psd = calibration_data.psd_sum[freqs <= max_freq]
|
||||
px = calibration_data.psd_x[freqs <= max_freq]
|
||||
py = calibration_data.psd_y[freqs <= max_freq]
|
||||
pz = calibration_data.psd_z[freqs <= max_freq]
|
||||
freqs = freqs[freqs <= max_freq]
|
||||
def plot_frequency(opts, datas, lognames, max_freq, axis):
|
||||
calibration_data = calc_freq_response(lognames[0], datas[0], max_freq)
|
||||
for logname, data in zip(lognames[1:], datas[1:]):
|
||||
calibration_data.add_data(calc_freq_response(logname, data, max_freq))
|
||||
|
||||
fig, ax = matplotlib.pyplot.subplots()
|
||||
ax.set_title("\n".join(wrap(
|
||||
"Frequency response (%s)" % (', '.join(lognames)), MAX_TITLE_LENGTH)))
|
||||
ax.set_xlabel('Frequency (Hz)')
|
||||
ax.set_ylabel('Power spectral density')
|
||||
|
||||
ax.plot(freqs, psd, label='X+Y+Z', alpha=0.6)
|
||||
ax.plot(freqs, px, label='X', alpha=0.6)
|
||||
ax.plot(freqs, py, label='Y', alpha=0.6)
|
||||
ax.plot(freqs, pz, label='Z', alpha=0.6)
|
||||
datasets = calibration_data.get_datasets()
|
||||
if len(datasets) == 1:
|
||||
freqs = calibration_data.freq_bins
|
||||
if axis == 'all':
|
||||
psd = calibration_data.psd_sum[freqs <= max_freq]
|
||||
px = calibration_data.psd_x[freqs <= max_freq]
|
||||
py = calibration_data.psd_y[freqs <= max_freq]
|
||||
pz = calibration_data.psd_z[freqs <= max_freq]
|
||||
freqs = freqs[freqs <= max_freq]
|
||||
ax.plot(freqs, psd, label='X+Y+Z', alpha=0.6)
|
||||
ax.plot(freqs, px, label='X', alpha=0.6)
|
||||
ax.plot(freqs, py, label='Y', alpha=0.6)
|
||||
ax.plot(freqs, pz, label='Z', alpha=0.6)
|
||||
else:
|
||||
psd = calibration_data.get_psd(axis)[freqs <= max_freq]
|
||||
freqs = freqs[freqs <= max_freq]
|
||||
ax.plot(freqs, psd, label=axis.upper(), alpha=0.6)
|
||||
title = "Frequency response (%s)" % calibration_data.name
|
||||
else:
|
||||
for data in datasets:
|
||||
freqs = data.freq_bins
|
||||
psd = data.get_psd(axis)
|
||||
if psd is None:
|
||||
opts.error("Per-axis data is not present in the input file(s)")
|
||||
psd = psd[freqs <= max_freq]
|
||||
freqs = freqs[freqs <= max_freq]
|
||||
ax.plot(freqs, psd, label=data.name)
|
||||
title = "Frequency responses comparison"
|
||||
|
||||
ax.set_title("\n".join(wrap(title, MAX_TITLE_LENGTH)))
|
||||
ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||
ax.grid(which='major', color='grey')
|
||||
@@ -133,31 +164,11 @@ def plot_frequency(datas, lognames, max_freq):
|
||||
fig.tight_layout()
|
||||
return fig
|
||||
|
||||
def plot_compare_frequency(datas, lognames, max_freq, axis):
|
||||
fig, ax = matplotlib.pyplot.subplots()
|
||||
ax.set_title('Frequency responses comparison')
|
||||
ax.set_xlabel('Frequency (Hz)')
|
||||
ax.set_ylabel('Power spectral density')
|
||||
|
||||
for data, logname in zip(datas, lognames):
|
||||
calibration_data = calc_freq_response(data, max_freq)
|
||||
freqs = calibration_data.freq_bins
|
||||
psd = calibration_data.get_psd(axis)[freqs <= max_freq]
|
||||
freqs = freqs[freqs <= max_freq]
|
||||
ax.plot(freqs, psd, label="\n".join(wrap(logname, 60)), alpha=0.6)
|
||||
|
||||
ax.xaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||
ax.yaxis.set_minor_locator(matplotlib.ticker.AutoMinorLocator())
|
||||
ax.grid(which='major', color='grey')
|
||||
ax.grid(which='minor', color='lightgrey')
|
||||
fontP = matplotlib.font_manager.FontProperties()
|
||||
fontP.set_size('x-small')
|
||||
ax.legend(loc='best', prop=fontP)
|
||||
fig.tight_layout()
|
||||
return fig
|
||||
|
||||
# Plot data in a "spectrogram colormap"
|
||||
def plot_specgram(data, logname, max_freq, axis):
|
||||
def plot_specgram(opts, data, logname, max_freq, axis):
|
||||
if isinstance(data, shaper_calibrate.CalibrationData):
|
||||
opts.error("Cannot calculate the spectrogram using the processed"
|
||||
" resonances, raw_data input is required")
|
||||
pdata, bins, t = calc_specgram(data, axis)
|
||||
|
||||
fig, ax = matplotlib.pyplot.subplots()
|
||||
@@ -174,11 +185,12 @@ def plot_specgram(data, logname, max_freq, axis):
|
||||
# CSV output
|
||||
######################################################################
|
||||
|
||||
def write_frequency_response(datas, output):
|
||||
def write_frequency_response(lognames, datas, output):
|
||||
helper = shaper_calibrate.ShaperCalibrate(printer=None)
|
||||
calibration_data = helper.process_accelerometer_data(datas[0])
|
||||
for data in datas[1:]:
|
||||
calibration_data.add_data(helper.process_accelerometer_data(data))
|
||||
calibration_data = helper.process_accelerometer_data(lognames[0], datas[0])
|
||||
for logname, data in zip(lognames[1:], datas[1:]):
|
||||
calibration_data.add_data(
|
||||
helper.process_accelerometer_data(logname, data))
|
||||
helper.save_calibration_data(output, calibration_data)
|
||||
|
||||
def write_specgram(psd, freq_bins, time, output):
|
||||
@@ -223,9 +235,6 @@ def main():
|
||||
help="maximum frequency to graph")
|
||||
opts.add_option("-r", "--raw", action="store_true",
|
||||
help="graph raw accelerometer data")
|
||||
opts.add_option("-c", "--compare", action="store_true",
|
||||
help="graph comparison of power spectral density "
|
||||
"between different accelerometer data files")
|
||||
opts.add_option("-s", "--specgram", action="store_true",
|
||||
help="graph spectrogram of accelerometer data")
|
||||
opts.add_option("-a", type="string", dest="axis", default="all",
|
||||
@@ -242,29 +251,25 @@ def main():
|
||||
if is_csv_output(options.output):
|
||||
if options.raw:
|
||||
opts.error("raw mode is not supported with csv output")
|
||||
if options.compare:
|
||||
opts.error("comparison mode is not supported with csv output")
|
||||
if options.specgram:
|
||||
if len(args) > 1:
|
||||
opts.error("Only 1 input is supported in specgram mode")
|
||||
pdata, bins, t = calc_specgram(datas[0], options.axis)
|
||||
write_specgram(pdata, bins, t, options.output)
|
||||
else:
|
||||
write_frequency_response(datas, options.output)
|
||||
write_frequency_response(args, datas, options.output)
|
||||
return
|
||||
|
||||
# Draw graph
|
||||
if options.raw:
|
||||
fig = plot_accel(datas, args)
|
||||
fig = plot_accel(opts, datas, args)
|
||||
elif options.specgram:
|
||||
if len(args) > 1:
|
||||
opts.error("Only 1 input is supported in specgram mode")
|
||||
fig = plot_specgram(datas[0], args[0], options.max_freq, options.axis)
|
||||
elif options.compare:
|
||||
fig = plot_compare_frequency(datas, args, options.max_freq,
|
||||
options.axis)
|
||||
fig = plot_specgram(opts, datas[0], args[0],
|
||||
options.max_freq, options.axis)
|
||||
else:
|
||||
fig = plot_frequency(datas, args, options.max_freq)
|
||||
fig = plot_frequency(opts, datas, args, options.max_freq, options.axis)
|
||||
|
||||
# Show graph
|
||||
if options.output is None:
|
||||
|
||||
Reference in New Issue
Block a user