Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80c8da622d | ||
|
|
b42e377ac6 | ||
|
|
7cfd02a7c6 | ||
|
|
9fa07a12c4 | ||
|
|
1a4fea3c8c | ||
|
|
eab10ce5da | ||
|
|
0696a60b7f | ||
|
|
ac96cb2eb7 |
@@ -52,7 +52,7 @@ gcode:
|
|||||||
ACCELEROMETER_MEASURE CHIP={accel_chip} NAME=axemap
|
ACCELEROMETER_MEASURE CHIP={accel_chip} NAME=axemap
|
||||||
|
|
||||||
RESPOND MSG="Analysis of the movements..."
|
RESPOND MSG="Analysis of the movements..."
|
||||||
RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type axesmap --accel {accel} --chip_name {accel_chip}"
|
RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type axesmap --accel {accel|int} --chip_name {accel_chip}"
|
||||||
|
|
||||||
# Restore the previous acceleration values
|
# Restore the previous acceleration values
|
||||||
SET_VELOCITY_LIMIT ACCEL={old_accel} ACCEL_TO_DECEL={old_accel_to_decel} SQUARE_CORNER_VELOCITY={old_sqv}
|
SET_VELOCITY_LIMIT ACCEL={old_accel} ACCEL_TO_DECEL={old_accel_to_decel} SQUARE_CORNER_VELOCITY={old_sqv}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ gcode:
|
|||||||
{% set max_freq = params.FREQ_END|default(133.3)|float %}
|
{% set max_freq = params.FREQ_END|default(133.3)|float %}
|
||||||
{% set hz_per_sec = params.HZ_PER_SEC|default(1)|float %}
|
{% set hz_per_sec = params.HZ_PER_SEC|default(1)|float %}
|
||||||
{% set axis = params.AXIS|default("all")|string|lower %}
|
{% set axis = params.AXIS|default("all")|string|lower %}
|
||||||
|
{% set scv = params.SCV|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(True) %}
|
||||||
|
|
||||||
@@ -25,13 +27,17 @@ 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 %}
|
||||||
|
{% set scv = printer.toolhead.square_corner_velocity %}
|
||||||
|
{% 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 {% 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 %}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if Y %}
|
{% if Y %}
|
||||||
@@ -40,7 +46,7 @@ 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 {% 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 %}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
M400
|
M400
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ gcode:
|
|||||||
|
|
||||||
RESPOND MSG="Machine and motors vibration graph generation..."
|
RESPOND MSG="Machine and motors vibration graph 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 --axis_name {direction} --accel {accel} --chip_name {accel_chip} {% if keep_csv %}--keep_csv{% endif %}"
|
RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type vibrations --axis_name {direction} --accel {accel|int} --chip_name {accel_chip} {% if keep_csv %}--keep_csv{% endif %}"
|
||||||
M400
|
M400
|
||||||
RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type clean --keep_results {keep_results}"
|
RUN_SHELL_COMMAND CMD=shaketune PARAMS="--type clean --keep_results {keep_results}"
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,10 @@ def compute_mechanical_parameters(psd, freqs):
|
|||||||
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])
|
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
|
||||||
zeta = bandwidth / (2 * fr)
|
bw1 = math.pow(bandwidth/fr,2)
|
||||||
|
bw2 = math.pow(bandwidth/fr,4)
|
||||||
|
|
||||||
|
zeta = math.sqrt(0.5-math.sqrt(1/(4+4*bw1-bw2)))
|
||||||
|
|
||||||
return fr, zeta, max_power_index
|
return fr, zeta, max_power_index
|
||||||
|
|
||||||
|
|||||||
@@ -42,17 +42,22 @@ KLIPPAIN_COLORS = {
|
|||||||
# Computation
|
# Computation
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
# Find the best shaper parameters using Klipper's official algorithm selection
|
# Find the best shaper parameters using Klipper's official algorithm selection with
|
||||||
def calibrate_shaper(datas, max_smoothing):
|
# a proper precomputed damping ratio (zeta) and using the configured printer SQV value
|
||||||
|
def calibrate_shaper(datas, max_smoothing, scv, max_freq):
|
||||||
helper = shaper_calibrate.ShaperCalibrate(printer=None)
|
helper = shaper_calibrate.ShaperCalibrate(printer=None)
|
||||||
calibration_data = helper.process_accelerometer_data(datas)
|
calibration_data = helper.process_accelerometer_data(datas)
|
||||||
calibration_data.normalize_to_frequencies()
|
calibration_data.normalize_to_frequencies()
|
||||||
|
|
||||||
shaper, all_shapers = helper.find_best_shaper(calibration_data, max_smoothing, print_with_c_locale)
|
|
||||||
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)
|
||||||
|
|
||||||
print_with_c_locale("Recommended shaper is %s @ %.1f Hz" % (shaper.name, shaper.freq))
|
shaper, all_shapers = helper.find_best_shaper(
|
||||||
print_with_c_locale("Axis has a main resonant frequency at %.1fHz with an estimated damping ratio of %.3f" % (fr, zeta))
|
calibration_data, shapers=None, damping_ratio=zeta,
|
||||||
|
scv=scv, shaper_freqs=None, max_smoothing=max_smoothing,
|
||||||
|
test_damping_ratios=None, max_freq=max_freq,
|
||||||
|
logger=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 computed damping ratio of %.3f)" % (shaper.name.upper(), shaper.freq, scv, zeta))
|
||||||
|
|
||||||
return shaper.name, all_shapers, calibration_data, fr, zeta
|
return shaper.name, all_shapers, calibration_data, fr, zeta
|
||||||
|
|
||||||
@@ -198,7 +203,7 @@ 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, max_freq=200.):
|
def shaper_calibration(lognames, klipperdir="~/klipper", max_smoothing=None, scv=5. , max_freq=200.):
|
||||||
set_locale()
|
set_locale()
|
||||||
global shaper_calibrate
|
global shaper_calibrate
|
||||||
shaper_calibrate = setup_klipper_import(klipperdir)
|
shaper_calibrate = setup_klipper_import(klipperdir)
|
||||||
@@ -209,7 +214,7 @@ def shaper_calibration(lognames, klipperdir="~/klipper", max_smoothing=None, max
|
|||||||
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 = calibrate_shaper(datas[0], max_smoothing)
|
performance_shaper, shapers, calibration_data, fr, zeta = 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
|
||||||
|
|
||||||
@@ -231,7 +236,7 @@ def shaper_calibration(lognames, klipperdir="~/klipper", max_smoothing=None, max
|
|||||||
# 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("Peaks 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(2, 1, gridspec_kw={
|
||||||
@@ -245,17 +250,23 @@ def shaper_calibration(lognames, klipperdir="~/klipper", max_smoothing=None, max
|
|||||||
})
|
})
|
||||||
fig.set_size_inches(8.3, 11.6)
|
fig.set_size_inches(8.3, 11.6)
|
||||||
|
|
||||||
# Add title
|
# 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'
|
||||||
|
title_line3 = '| Square corner velocity: ' + str(scv) + 'mm/s'
|
||||||
|
title_line4 = '| Max allowed smoothing: ' + str(max_smoothing)
|
||||||
except:
|
except:
|
||||||
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_line4 = ''
|
||||||
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'])
|
||||||
|
fig.text(0.58, 0.960, title_line3, 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)
|
||||||
@@ -284,6 +295,8 @@ def main():
|
|||||||
help="maximum frequency to graph")
|
help="maximum frequency to graph")
|
||||||
opts.add_option("-s", "--max_smoothing", type="float", default=None,
|
opts.add_option("-s", "--max_smoothing", type="float", default=None,
|
||||||
help="maximum shaper smoothing to allow")
|
help="maximum shaper smoothing to allow")
|
||||||
|
opts.add_option("--scv", "--square_corner_velocity", type="float",
|
||||||
|
dest="scv", default=5., help="square corner velocity")
|
||||||
opts.add_option("-k", "--klipper_dir", type="string", dest="klipperdir",
|
opts.add_option("-k", "--klipper_dir", type="string", dest="klipperdir",
|
||||||
default="~/klipper", help="main klipper directory")
|
default="~/klipper", help="main klipper directory")
|
||||||
options, args = opts.parse_args()
|
options, args = opts.parse_args()
|
||||||
@@ -294,7 +307,7 @@ def main():
|
|||||||
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.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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ def create_belts_graph(keep_csv):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
def create_shaper_graph(keep_csv):
|
def create_shaper_graph(keep_csv, max_smoothing, scv):
|
||||||
current_date = datetime.now().strftime('%Y%m%d_%H%M%S')
|
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
|
# Get all the files and sort them based on last modified time to select the most recent one
|
||||||
@@ -120,7 +120,7 @@ def create_shaper_graph(keep_csv):
|
|||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
# Generate the shaper graph and its name
|
# Generate the shaper graph and its name
|
||||||
fig = shaper_calibration([new_file], KLIPPER_FOLDER)
|
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')
|
png_filename = os.path.join(RESULTS_FOLDER, RESULTS_SUBFOLDERS[1], f'resonances_{current_date}_{axis}.png')
|
||||||
fig.savefig(png_filename, dpi=150)
|
fig.savefig(png_filename, dpi=150)
|
||||||
|
|
||||||
@@ -263,6 +263,10 @@ def main():
|
|||||||
help="number of results to keep in the result folder after each run of the script")
|
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",
|
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")
|
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")
|
||||||
options, args = opts.parse_args()
|
options, args = opts.parse_args()
|
||||||
|
|
||||||
if options.type is None:
|
if options.type is None:
|
||||||
@@ -282,7 +286,7 @@ def main():
|
|||||||
create_belts_graph(keep_csv=options.keep_csv)
|
create_belts_graph(keep_csv=options.keep_csv)
|
||||||
print(f"Belt graph created. You will find the results in {RESULTS_FOLDER}/{RESULTS_SUBFOLDERS[0]}")
|
print(f"Belt graph created. You will find the results in {RESULTS_FOLDER}/{RESULTS_SUBFOLDERS[0]}")
|
||||||
elif graph_mode.lower() == 'shaper':
|
elif graph_mode.lower() == 'shaper':
|
||||||
axis = create_shaper_graph(keep_csv=options.keep_csv)
|
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]}")
|
print(f"{axis} input shaper graph created. You will find the results in {RESULTS_FOLDER}/{RESULTS_SUBFOLDERS[1]}")
|
||||||
elif graph_mode.lower() == 'vibrations':
|
elif graph_mode.lower() == 'vibrations':
|
||||||
create_vibrations_graph(axis_name=options.axis_name, accel=options.accel_used, chip_name=options.chip_name, keep_csv=options.keep_csv)
|
create_vibrations_graph(axis_name=options.axis_name, accel=options.accel_used, chip_name=options.chip_name, keep_csv=options.keep_csv)
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -17,6 +17,10 @@ Check out the **[detailed documentation of the Shake&Tune module here](./docs/RE
|
|||||||
|:----------------:|:------------:|:---------------------:|
|
|:----------------:|:------------:|:---------------------:|
|
||||||
| [<img src="./docs/images/belts_example.png">](./docs/macros/belts_tuning.md) | [<img src="./docs/images/axis_example.png">](./docs/macros/axis_tuning.md) | [<img src="./docs/images/vibrations_example.png">](./docs/macros/vibrations_tuning.md) |
|
| [<img src="./docs/images/belts_example.png">](./docs/macros/belts_tuning.md) | [<img src="./docs/images/axis_example.png">](./docs/macros/axis_tuning.md) | [<img src="./docs/images/vibrations_example.png">](./docs/macros/vibrations_tuning.md) |
|
||||||
|
|
||||||
|
> **Note**:
|
||||||
|
>
|
||||||
|
> Be aware that Shake&Tune uses the [Gcode shell command plugin](https://github.com/dw-0/kiauh/blob/master/docs/gcode_shell_command.md) under the hood to call the Python scripts that generate the graphs. While my scripts should be safe, the Gcode shell command plugin also has great potential for abuse if not used carefully for other purposes, since it opens shell access from Klipper.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Follow these steps to install the Shake&Tune module in your printer:
|
Follow these steps to install the Shake&Tune module in your printer:
|
||||||
@@ -29,18 +33,6 @@ Follow these steps to install the Shake&Tune module in your printer:
|
|||||||
```
|
```
|
||||||
[include K-ShakeTune/*.cfg]
|
[include K-ShakeTune/*.cfg]
|
||||||
```
|
```
|
||||||
1. Finally, if you want to get automatic updates, add the following to your `moonraker.cfg` file:
|
|
||||||
```
|
|
||||||
[update_manager Klippain-ShakeTune]
|
|
||||||
type: git_repo
|
|
||||||
path: ~/klippain_shaketune
|
|
||||||
channel: beta
|
|
||||||
origin: https://github.com/Frix-x/klippain-shaketune.git
|
|
||||||
primary_branch: main
|
|
||||||
managed_services: klipper
|
|
||||||
install_script: install.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|||||||
BIN
docs/images/belt_graphs/chipcomp_adxl.png
Normal file
BIN
docs/images/belt_graphs/chipcomp_adxl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 365 KiB |
BIN
docs/images/belt_graphs/chipcomp_s2dw.png
Normal file
BIN
docs/images/belt_graphs/chipcomp_s2dw.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 465 KiB |
BIN
docs/images/shaper_graphs/chipcomp_s2dw_2.png
Normal file
BIN
docs/images/shaper_graphs/chipcomp_s2dw_2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/images/vibrations_graphs/sd2w_spectrogram.png
Normal file
BIN
docs/images/vibrations_graphs/sd2w_spectrogram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 632 KiB |
@@ -22,16 +22,20 @@ When tuning Input Shaper, keep the following in mind:
|
|||||||
1. Finally, remember why you're running these tests: to get clean prints. Don't become too obsessive over perfect graphs, as the last bits of optimization will probably have the least impact on the printed parts in terms of ringing and ghosting.
|
1. Finally, remember why you're running these tests: to get clean prints. Don't become too obsessive over perfect graphs, as the last bits of optimization will probably have the least impact on the printed parts in terms of ringing and ghosting.
|
||||||
|
|
||||||
|
|
||||||
### Special note on accelerometer mounting point
|
### Note on accelerometer mounting point
|
||||||
Input Shaping algorithms work by suppressing a single resonant frequency (or a range around a single resonant frequency). When setting the filter, **the primary goal is to target the resonant frequency of the toolhead and belts system** (see the [theory behind it](#theory-behind-it)), as this has the most significant impact on print quality and is the root cause of ringing.
|
Input Shaping algorithms are designed to mitigate resonances by targeting a specific resonant frequency or a range around it. When setting the filter, **the primary goal is to target the resonant frequency of the toolhead and belts system** (see the [theory behind it](#theory-behind-it)), as this has the most significant impact on print quality and is the root cause of ringing.
|
||||||
|
|
||||||
When setting up Input Shaper, it is important to consider the accelerometer mounting point. There are mainly two possibilities, each with its pros and cons:
|
Choosing the accelerometer's mounting point is important. There are currently three mounting strategies, each offering distinct advantages:
|
||||||
|
|
||||||
| Directly at the nozzle tip | Near the toolhead's center of gravity |
|
| Mounting Point | Advantages | Considerations |
|
||||||
| --- | --- |
|
| --- | --- | --- |
|
||||||
| This method provides a more accurate and comprehensive measurement of everything in your machine. It captures the main resonant frequency along with other vibrations and movements, such as toolhead wobbling and printer frame movements. This approach is excellent for diagnosing your machine's kinematics and troubleshooting problems. However, it also leads to noisier graphs, making it harder for the algorithm to select the correct filter for input shaping. Graphs may appear worse, but this is due to the different "point of view" of the printer's behavior. | I personally recommend mounting the accelerometer in this way, as it provides a clear view of the main resonant frequency you want to target, allowing for accurate input shaper filter settings. This approach results in cleaner graphs with less visible noise from other subsystem vibrations, making interpretation easier for both automatic algorithms and users. However, this method provides less detail in the graphs and may be slightly less effective for troubleshooting printer problems. |
|
| **Directly at the nozzle tip** | Provides a comprehensive view of all machine vibrations, including the main resonance, but also toolhead wobbling and global frame movements. Ideal for diagnosing kinematic issues and troubleshooting. | Results in noisier data, which may complicate the final Input Shaping filter selection on machines that are not perfect and/or not fully rigid. |
|
||||||
|
| **Near the toolhead's center of gravity** | Provides a view of mostly only the primary resonant frequencies of the toolhead and belts, allowing precise filter selection for Input Shaping. The data is often cleaner, with only severe mechanical issues or very problematic toolhead wobble visible on the graphs. | May provide less detail on secondary vibrations (which have a fairly minor effect on ringing) and may be less effective in diagnosing unrelated mechanical problems. |
|
||||||
|
| **Integrated accelerometer on a CANBus Board** | Simple and effective, requires no additional installation and always available. Can help for diagnosing issues like those caused by bowden tubes, umbillical coords and cable chains. If toolhead is very rigid, measurements are close enough to those of the center of gravity. | Not accurate for a detailed analysis or diagnosing mechanical issues due to distance from the nozzle tip and potential noise from attached components. |
|
||||||
|
|
||||||
A suggested workflow is to first use the nozzle mount to diagnose mechanical issues, such as loose screws or a bad X carriage. Once the mechanics are in good condition, switch to a mounting point closer to the toolhead's center of gravity for setting the input shaper filter settings by using cleaner graphs that highlights the most impactful frequency.
|
While you should usually try to focus on the toolhead/belts mechanical subsystem for resonance mitigation (since it has the most impact on ringing and print quality), you don't want to overlook the importance of nozzle tip measurements for other sources of vibration. Indeed, if resonance analysis results vary a lot between mounting points, reinforcing the toolhead's rigidity to minimize wobbling and vibrations is recommended. Here is a strategy that attempts to methodically address mechanical issues and then allow for the day-to-day selection of input shaping filters as needed:
|
||||||
|
1. **Diagnosis phase**: Begin with the nozzle tip mount to identify and troubleshoot mechanical issues to ensure the printer components are healthy and the assembly is well done and optimized.
|
||||||
|
1. **Filter selection phase**: If the graphs are mostly clean, you can transition to a mounting point near the toolhead's center of gravity for cleaner data on the main resonance, facilitating accurate Input Shaping filter settings. You can also consider the CANBus integrated accelerometer for its simplicity, especially if the toolhead is particularly rigid and minimally affected by wobble.
|
||||||
|
|
||||||
|
|
||||||
## Theory behind it
|
## Theory behind it
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ Then, call the `AXES_SHAPER_CALIBRATION` 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|
|
||||||
|AXIS|"all"|Axis you want to test in the list of "all", "X" or "Y"|
|
|AXIS|"all"|Axis you want to test in the list of "all", "X" or "Y"|
|
||||||
|
|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|
|
||||||
|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|True|Weither or not to keep the CSV data file alonside the PNG graphs|
|
||||||
|
|
||||||
@@ -39,13 +41,13 @@ For setting your Input Shaping filters, rely on the auto-computed values display
|
|||||||
* `MZV` is usually the top pick for well-adjusted machines. It's a good compromise for low remaining vibrations while still allowing pretty good acceleration values. Keep in mind, `MZV` is only recommended by Klipper on good graphs.
|
* `MZV` is usually the top pick for well-adjusted machines. It's a good compromise for low remaining vibrations while still allowing pretty good acceleration values. Keep in mind, `MZV` is only recommended by Klipper on good graphs.
|
||||||
* `EI` can be used as a fallback for challenging graphs. But first, try to fix your mechanical issues before using it: almost every printer should be able to run `MZV` instead.
|
* `EI` can be used as a fallback for challenging graphs. But first, try to fix your mechanical issues before using it: almost every printer should be able to run `MZV` instead.
|
||||||
* `2HUMP_EI` and `3HUMP_EI` are last-resort choices. Usually, they lead to a high level of smoothing in order to suppress the ringing while also using relatively low acceleration values. If they pop up as suggestions, it's likely your machine has underlying mechanical issues (that lead to pretty bad or "wide" graphs).
|
* `2HUMP_EI` and `3HUMP_EI` are last-resort choices. Usually, they lead to a high level of smoothing in order to suppress the ringing while also using relatively low acceleration values. If they pop up as suggestions, it's likely your machine has underlying mechanical issues (that lead to pretty bad or "wide" graphs).
|
||||||
- **Recommended Acceleration** (`accel<=...`): This isn't a standalone figure. It's essential to also consider the `vibr` and `sm` values as it's a compromise between the three. They will give you the percentage of remaining vibrations and the smoothing after Input Shaping, when using the recommended acceleration. Nothing will prevent you from using higher acceleration values; they are not a limit. However, when doing so, Input Shaping may not be able to suppress all the ringing on your parts. Finally, keep in mind that high acceleration values are not useful at all if there is still a high level of remaining vibrations: you should address any mechanical issues first.
|
- **Recommended Acceleration** (`accel<=...`): This isn't a standalone figure. It's essential to also consider the `vibr` and `sm` values as it's a compromise between the three. They will give you the percentage of remaining vibrations and the smoothing after Input Shaping, when using the recommended acceleration. Nothing will prevent you from using higher acceleration values; they are not a limit. However, in this case, Input Shaping may not be able to suppress all the ringing on your parts, and more smoothing will occur. Finally, keep in mind that high acceleration values are not useful at all if there is still a high level of remaining vibrations: you should address any mechanical issues first.
|
||||||
- **The remaining vibrations** (`vibr`): This directly correlates with ringing. It correspond to the total value of the blue "after shaper" signal. Ideally, you want a filter with minimal or zero vibrations.
|
- **The remaining vibrations** (`vibr`): This directly correlates with ringing. It correspond to the total value of the "after shaper" signal. Ideally, you want a filter with minimal remaining vibrations.
|
||||||
- **Shaper recommendations**: This script will give you some tailored recommendations based on your graphs. Pick the one that suit your needs:
|
- **Shaper recommendations**: This script will give you some tailored recommendations based on your graphs. Pick the one that suit your needs:
|
||||||
* The "performance" shaper is Klipper's original suggestion that is good for high acceleration while also sometimes allowing a little bit of remaining vibrations. Use it if your goal is speed printing and you don't care much about some remaining ringing.
|
* The "performance" shaper is Klipper's original suggestion, which is good for high acceleration, but sometimes allows a little residual vibration while minimizing smoothing. Use it if your goal is speed printing and you don't care much about some remaining ringing.
|
||||||
* The "low vibration" shaper aims for the lowest level of remaining vibration to ensure the best print quality with minimal ringing. This should be the best bet for most users.
|
* The "low vibration" shaper aims for the lowest level of remaining vibration to ensure the best print quality with minimal ringing. This should be the best bet for most users.
|
||||||
* Sometimes, only a single recommendation called "best" shaper is presented. This means that either no suitable "low vibration" shaper was found (due to a high level of vibration or with too much smoothing) or because the "performance" shaper is also the one with the lowest vibration level.
|
* Sometimes only a single recommendation is given as the "best" shaper. This means that either no suitable "low vibration" shaper was found (due to a high level of residual vibration or too much smoothing), or that the "performance" shaper is also the one with the lowest vibration level.
|
||||||
- **Damping Ratio**: Displayed at the end, this estimatation is only reliable when the graph shows a distinct, standalone and clean peak. On a well tuned machine, setting the damping ratio (instead of Klipper's 0.1 default value) can further reduce the ringing at high accelerations and with higher square corner velocities.
|
- **Damping Ratio**: Displayed at the end, this is an estimate based on your data that is used to improve the shaper recommendations for your machine. Defining it in the `[input_shaper]` section (instead of Klipper's default value of 0.1) can further reduce ringing at high accelerations and higher square corner velocities.
|
||||||
|
|
||||||
Then, add to your configuration:
|
Then, add to your configuration:
|
||||||
```
|
```
|
||||||
@@ -146,17 +148,19 @@ The presence of an unbalanced or poorly running fan can be directly observed in
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|  |  |  |
|
|  |  |  |
|
||||||
|
|
||||||
### Noisy accelerometer
|
### Spectrogram lightshow (LIS2DW)
|
||||||
|
|
||||||
The integration of LIS2DW as a resonance measuring device in Klipper is starting to be more and more common, particularly due to some manufacturers promoting its superiority over the established ADXL345. A critical analysis of their respective datasheets reveals a nuanced reality: the LIS2DW offers a higher sampling rate, but also it tends to exhibit increased noise levels at comparable sensitivity settings compared to the ADXL345. But, given that LIS2DW chips are also 5-10 times cheaper, it definitely makes sense for mass-producing PCBs.
|
The integration of LIS2DW as a resonance measuring device in Klipper is becoming more and more common, especially because some manufacturers are promoting its superiority over the established ADXL345. It's indeed a new generation chip that should be better to measure traditional "accelerations". However, a detailed comparison of their datasheets and practical measurements paints a more complex picture: the LIS2DW boasts greater sensitivity, but it has a lower sampling rate and produce significant aliasing that results in a "lightshow" effect on the spectrogram, characterized by multiple spurious resonance lines parallel to the main resonance, accompanied by intersecting interference lines that distort the harmonic profile.
|
||||||
|
|
||||||
In our use case, this chip manifests aliasing in the spectrogram that can be seen as additional 'ghosting' resonance lines parallel to the main resonance diagonal, with some intersecting interference lines that skew across the harmonics. Fortunately, this apparent lightshow do not distort the overall shape of the top graph and both the resonant frequency and damping ratio remain accurately measured as well as the input shaping filters that are also quite similar. This only makes it more challenging to discern fine details that could be masked, and it doesn't help for diagnosing mechanical issues.
|
While, the top resonance graph's overall shape, including resonant frequency and damping ratio, should be close with pretty similar recommendations for input shaping filters, this aliasing complicates the identification of subtle details and hampers mechanical issue diagnostics. It especially introduces a potential misinterpretation of "[binding](#low-frequency-energy)" due to a global offset of the curve.
|
||||||
|
|
||||||
Finally, please note that LIS2DW are known to add a small offset all over the top graph due to this aliasing. So the curve and peaks might be a bit higher, even at very low frequencies: in this case, this is probably not [#low-frequency-energy] but just some noise and it's not a mechanical problem.
|
> **Note**:
|
||||||
|
>
|
||||||
|
> It seems that some LIS2DW chips are better than others: in some cases aliasing is not a problem, but it can also be very problematic and lead to bad graphs, as seen in the "Extreme Aliasing" example below.
|
||||||
|
|
||||||
| LIS2DW measurement | ADXL345 measurement |
|
| ADXL345 measurement | LIS2DW measurement | LIS2DW extreme aliasing |
|
||||||
| --- | --- |
|
| --- | --- | --- |
|
||||||
|  |  |
|
|  |  |  |
|
||||||
|
|
||||||
### Crazy graphs and miscs
|
### Crazy graphs and miscs
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ The following graphs show the effect of incorrect or uneven belt tension. Rememb
|
|||||||
| The A belt tension is slightly lower than the B belt tension. This can be quickly remedied by tightening the screw only about one-half to one full turn. |  |
|
| The A belt tension is slightly lower than the B belt tension. This can be quickly remedied by tightening the screw only about one-half to one full turn. |  |
|
||||||
| B belt tension is significantly lower than the A belt. If you encounter this graph, I recommend going back to the [Voron belt tensioning documentation](https://docs.vorondesign.com/tuning/secondary_printer_tuning.html#belt-tension) for a more solid base. However, you could slightly increase the B tension and decrease the A tension, but exercise caution to avoid diverging from the recommended 110Hz base. |  |
|
| B belt tension is significantly lower than the A belt. If you encounter this graph, I recommend going back to the [Voron belt tensioning documentation](https://docs.vorondesign.com/tuning/secondary_printer_tuning.html#belt-tension) for a more solid base. However, you could slightly increase the B tension and decrease the A tension, but exercise caution to avoid diverging from the recommended 110Hz base. |  |
|
||||||
|
|
||||||
|
|
||||||
### Belt path problem
|
### Belt path problem
|
||||||
|
|
||||||
If there's an issue within the belt path, aligning and overlaying the curve might be unachievable even with proper belt tension. Begin by verifying that each belt has **the exact same number of teeth**. Then, inspect the belt paths, bearings, any signs of wear (like belt dust), and ensure the belt aligns correctly on all bearing flanges during motion.
|
If there's an issue within the belt path, aligning and overlaying the curve might be unachievable even with proper belt tension. Begin by verifying that each belt has **the exact same number of teeth**. Then, inspect the belt paths, bearings, any signs of wear (like belt dust), and ensure the belt aligns correctly on all bearing flanges during motion.
|
||||||
@@ -70,3 +69,13 @@ If there's an issue within the belt path, aligning and overlaying the curve migh
|
|||||||
| On this chart, there are two peaks. The first pair of peaks seems nearly aligned, but the second peak appears solely on the B belt, significantly deviating from the A belt. This suggests an issue with the belt path, likely with the B belt. |  |
|
| On this chart, there are two peaks. The first pair of peaks seems nearly aligned, but the second peak appears solely on the B belt, significantly deviating from the A belt. This suggests an issue with the belt path, likely with the B belt. |  |
|
||||||
| This chart is quite complex, displaying 3 peaks. While all the pairs seem well-aligned and tension ok, there are more than just two total peaks because `[1]` is split in two smaller peaks. This could be an issue, but it's not certain. It's recommended to generate the [Axis Input Shaper Graphs](./axis_tuning.md) to determine its impact. |  |
|
| This chart is quite complex, displaying 3 peaks. While all the pairs seem well-aligned and tension ok, there are more than just two total peaks because `[1]` is split in two smaller peaks. This could be an issue, but it's not certain. It's recommended to generate the [Axis Input Shaper Graphs](./axis_tuning.md) to determine its impact. |  |
|
||||||
| This graph might indicate too low belt tension, but also potential binding, friction or something impeding the toolhead's smooth movement. Indeed, the signal strength is considerably low (with a peak around 300k, compared to the typical ~1M) and is primarily filled with noise. Start by going back [here](https://docs.vorondesign.com/tuning/secondary_printer_tuning.html#belt-tension) to establish a robust tension foundation. Next, produce the [Axis Input Shaper Graphs](./axis_tuning.md) to identify any binding and address the issue. |  |
|
| This graph might indicate too low belt tension, but also potential binding, friction or something impeding the toolhead's smooth movement. Indeed, the signal strength is considerably low (with a peak around 300k, compared to the typical ~1M) and is primarily filled with noise. Start by going back [here](https://docs.vorondesign.com/tuning/secondary_printer_tuning.html#belt-tension) to establish a robust tension foundation. Next, produce the [Axis Input Shaper Graphs](./axis_tuning.md) to identify any binding and address the issue. |  |
|
||||||
|
|
||||||
|
### Spectrogram lightshow (LIS2DW)
|
||||||
|
|
||||||
|
The integration of LIS2DW as a resonance measuring device in Klipper is becoming more and more common, especially because some manufacturers are promoting its superiority over the established ADXL345. It's indeed a new generation chip that should be better to measure traditional "accelerations". However, a detailed comparison of their datasheets and practical measurements paints a more complex picture: the LIS2DW boasts greater sensitivity, but it has a lower sampling rate and produce significant aliasing that results in a "lightshow" effect on the spectrogram, characterized by multiple spurious resonance lines parallel to the main resonance, accompanied by intersecting interference lines that distort the harmonic profile.
|
||||||
|
|
||||||
|
For the belt graph, this can be problematic because it can introduce a lot of noise into the results and make them difficult to interpret, and it will probably tell you that there is a mechanical problem when there isn't.
|
||||||
|
|
||||||
|
| ADXL345 measurement | LIS2DW measurement |
|
||||||
|
| --- | --- |
|
||||||
|
|  |  |
|
||||||
|
|||||||
@@ -49,3 +49,13 @@ For reference, the default settings used in Klipper are:
|
|||||||
#driver_HEND: 0
|
#driver_HEND: 0
|
||||||
#driver_HSTRT: 5
|
#driver_HSTRT: 5
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Semi-blank spectrogram (LIS2DW)
|
||||||
|
|
||||||
|
The integration of LIS2DW as a resonance measuring device in Klipper is becoming more and more common, especially because some manufacturers are promoting its superiority over the established ADXL345. It's indeed a new generation chip that should be better to measure traditional "accelerations". However, a detailed comparison of their datasheets and practical measurements paints a more complex picture: the LIS2DW boasts greater sensitivity, but it has a lower sampling rate and produce significant aliasing.
|
||||||
|
|
||||||
|
This lower sampling rate is problematic for the vibration graph because it only records data up to 200 Hz, which is too low to produce an accurate graph. This will be seen as a small low frequency band on the spectrogram with a blank area for higher frequencies and incorrect data printed in the speed profile and motor frequency profile.
|
||||||
|
|
||||||
|
| LIS2DW vibration measurement |
|
||||||
|
| --- |
|
||||||
|
|  |
|
||||||
|
|||||||
16
install.sh
16
install.sh
@@ -1,6 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
USER_CONFIG_PATH="${HOME}/printer_data/config"
|
USER_CONFIG_PATH="${HOME}/printer_data/config"
|
||||||
|
MOONRAKER_CONFIG="${HOME}/printer_data/config/moonraker.conf"
|
||||||
KLIPPER_PATH="${HOME}/klipper"
|
KLIPPER_PATH="${HOME}/klipper"
|
||||||
|
|
||||||
K_SHAKETUNE_PATH="${HOME}/klippain_shaketune"
|
K_SHAKETUNE_PATH="${HOME}/klippain_shaketune"
|
||||||
@@ -110,11 +111,24 @@ function link_gcodeshellcommandpy {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function add_updater {
|
||||||
|
update_section=$(grep -c '\[update_manager[a-z ]* Klippain-ShakeTune\]' $MOONRAKER_CONFIG || true)
|
||||||
|
if [ "$update_section" -eq 0 ]; then
|
||||||
|
echo -n "[INSTALL] Adding update manager to moonraker.conf..."
|
||||||
|
cat ${K_SHAKETUNE_PATH}/moonraker.conf >> $MOONRAKER_CONFIG
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
function restart_klipper {
|
function restart_klipper {
|
||||||
echo "[POST-INSTALL] Restarting Klipper..."
|
echo "[POST-INSTALL] Restarting Klipper..."
|
||||||
sudo systemctl restart klipper
|
sudo systemctl restart klipper
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function restart_moonraker {
|
||||||
|
echo "[POST-INSTALL] Restarting Moonraker..."
|
||||||
|
sudo systemctl restart moonraker
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
printf "\n=============================================\n"
|
printf "\n=============================================\n"
|
||||||
echo "- Klippain Shake&Tune module install script -"
|
echo "- Klippain Shake&Tune module install script -"
|
||||||
@@ -126,5 +140,7 @@ preflight_checks
|
|||||||
check_download
|
check_download
|
||||||
setup_venv
|
setup_venv
|
||||||
link_extension
|
link_extension
|
||||||
|
add_updater
|
||||||
link_gcodeshellcommandpy
|
link_gcodeshellcommandpy
|
||||||
restart_klipper
|
restart_klipper
|
||||||
|
restart_moonraker
|
||||||
|
|||||||
11
moonraker.conf
Normal file
11
moonraker.conf
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
## Klippain Shake&Tune automatic update management
|
||||||
|
[update_manager Klippain-ShakeTune]
|
||||||
|
type: git_repo
|
||||||
|
origin: https://github.com/Frix-x/klippain-shaketune.git
|
||||||
|
path: ~/klippain_shaketune
|
||||||
|
virtualenv: ~/klippain_shaketune-env
|
||||||
|
requirements: requirements.txt
|
||||||
|
system_dependencies: system-dependencies.json
|
||||||
|
primary_branch: main
|
||||||
|
managed_services: klipper
|
||||||
9
system-dependencies.json
Normal file
9
system-dependencies.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"debian": [
|
||||||
|
"python3-venv",
|
||||||
|
"python3-numpy",
|
||||||
|
"python3-matplotlib",
|
||||||
|
"libopenblas-dev",
|
||||||
|
"libatlas-base-dev"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user