I put up a draft PR here: PR: Enable multiple z_thermal_adjust sections by garethky · Pull Request #6855 · Klipper3d/klipper · GitHub
I’ve been sitting on this code for the better part of a year. This came up when developing load cell based probing but I believe it should also help with any kind of nozzle probe, e.g. Voron Tap.
The Problem
Probing temperatures are limited based a couple of factors: The glass transition temp of the filament and the melting temperature of the bed surface. So probing is carried out at about 140-1670C. Printing temps for PC might be 290C. So there is, potentially a 150C temperature increase between probing and printing.
Investigations on my printer shows that the rate of thermal expansion for the nozzle is 0.000465mm per degree C. Over 150C that is 0.069mm / 69 microns or about 35% of the thickness of a first layer. The general result of this is first layers that are too low.
Proposed Solution
- Extend
z_thermal_adjust
to support compensating for multiple components in the printer. So the Frame and the Nozzle can be compensated and the machine can adapt enclosed open conditions.
Example Config:
[z_thermal_adjust nozzle]
temp_coeff: -0.00046459
sensor_type: temperature_combined
sensor_list: extruder
combination_method: max
min_temp: 0
max_temp: 400
maximum_deviation: 1
max_z_adjustment: 0.5
[z_thermal_adjust frame]
gcode_id: ZFARME
temp_coeff: 0.0088
sensor_type: Generic 3950
sensor_pin: T1
min_temp: 0
max_temp: 120
max_z_adjustment: 1.0
I’ve been testing this in conjunction with load cell probing and the results have been very encouraging. The effect is temperature dependent and easy to measure. A GCode script can measure the nozzle length via probing over a range of temperatures and then produce the thermal coefficient through linear regression. I probed the aluminum bed surface directly with a clean nozzle and no filament loaded. For PCB beds you might have to put a steel surface on top, like the uncoated side of a steel sheet.
The real test is comparing first layers of materials at different temps. I’ve been testing swapping between PLA and PETG. With other variables removed (keeping the door open, using the same print surface) this lets me swap filaments and get very similar results between them.
Config
Typically for a nozzle youre looking at something like this:
[z_thermal_adjust nozzle]
temp_coeff: -0.00046459
sensor: extruder
max_z_adjustment: 0.5
The temp_coeff
is negative. z_thermal_adjust
has positive values indicate that the effective height of the printer is increasing, because it was designed for frame expansion. In this case the height of the printer should be decreasing as temperature increases.
Testing Macro
MEASURE_NOZZLE_Z_THERM_COEFF
This is a macro that can be used to find the thermal coefficient of nozzle expansion. It will sweep over a temperature range and use the PROBE command to measure the Z offset. It climes up to a MAX_TEMP and then back down, so we can see both thermal expansion and contraction. Then it uses linear regression to find the coefficient of expansion.
My suggestions for using this are to probe with no filament and a clean nozzle. Leave the enclosure open so the exterder doesn’t heat up the frame.
If this generally turns out to work well, we can look at turning this into a GCode Command.
# Measure the nozzle length over a temperature range and report the rate of chagne
[gcode_macro MEASURE_NOZZLE_Z_THERM_COEFF]
variable_temperatures: []
variable_z_values: []
gcode:
SET_GCODE_VARIABLE MACRO=MEASURE_NOZZLE_Z_THERM_COEFF VARIABLE=z_values VALUE=[]
{% set temperatures = [] %}
# setup variables
{% set MIN_TEMP = params.MIN_TEMP|default(140)|int %}
{% set MAX_TEMP = params.MAX_TEMP|default(290)|int %}
{% set SOAK_TIME = (params.SOAK_TIME|default(20.0)|float) * 1000.0 %}
{% set INTERVAL = params.INTERVAL|default(10)|int %}
{% set SAMPLES = params.SAMPLES|default(5)|int %}
# measure temp going up ever INTERVAL degrees
{% for EXTRUDER_TEMP in range(MIN_TEMP, MAX_TEMP, INTERVAL) %}
M109 S{EXTRUDER_TEMP}
G4 P{SOAK_TIME}
{% set _ = temperatures.append(EXTRUDER_TEMP) %}
_MEASURE_NOZZLE_Z_THERM_COEFF__MEASURE_Z SAMPLES={SAMPLES}
{% endfor %}
# measure temp going down every INTERVAL degrees
{% for EXTRUDER_TEMP in range(MIN_TEMP, MAX_TEMP, INTERVAL)|reverse %}
M109 S{EXTRUDER_TEMP}
G4 P{SOAK_TIME}
{% set _ = temperatures.append(EXTRUDER_TEMP) %}
_MEASURE_NOZZLE_Z_THERM_COEFF__MEASURE_Z SAMPLES={SAMPLES}
{% endfor %}
# extruder to 0
M104 S0
SET_GCODE_VARIABLE MACRO=MEASURE_NOZZLE_Z_THERM_COEFF VARIABLE=temperatures VALUE="{temperatures | pprint | replace("\n", "")}"
_MEASURE_NOZZLE_Z_THERM_COEFF__SHOW_RESULTS
[gcode_macro _MEASURE_NOZZLE_Z_THERM_COEFF__SHOW_RESULTS]
gcode:
{% set measure = printer['gcode_macro MEASURE_NOZZLE_Z_THERM_COEFF'] %}
{% set temperatures = measure.temperatures %}
{% set z_values = measure.z_values %}
# debug logging
{action_respond_info("Temperatures: %s" % (temperatures | pprint | string |replace("\n", "")))}
{action_respond_info("Z Values: %s" % (z_values | pprint | string | replace("\n", "")))}
# linear regression
{% set avg_temp = (temperatures | sum) / (temperatures | length) %}
{% set avg_z = (z_values | sum) / (z_values | length) %}
{% set a = [] %}
{% set b = [] %}
{% for temp in temperatures %}
{% set _ = a.append(temp * (z_values[loop.index - 1] - avg_z)) %}
{% set _ = b.append(temp * (temp - avg_temp)) %}
{% endfor %}
{% set a_sum = a | sum %}
{% set b_sum = b | sum %}
{% set temp_coeff = ((a_sum / b_sum) * -1.0) | round(8) %}
# tell the user what the setting is:
{action_respond_info("Temperature Coefficient: %s" % (temp_coeff))}
[gcode_macro _MEASURE_NOZZLE_Z_THERM_COEFF__MEASURE_Z]
gcode:
{% set SAMPLES = params.SAMPLES|default(5)|int %}
PROBE SAMPLES={SAMPLES} # take 5 probe samples
# Dragons:
# A move needs to be issued so the kinematics get updated position info!!
# without this move its always off by the length of the pullback move!!
G1 Z5 F{5 * 60} # lift to 5mm
M400
_MEASURE_NOZZLE_Z_THERM_COEFF__SAVE_Z # has to be a sub-macro call to see probing results
# this call save the result of the last probe and the current temperature to two arrays
[gcode_macro _MEASURE_NOZZLE_Z_THERM_COEFF__SAVE_Z]
gcode:
{% set measure = printer['gcode_macro MEASURE_NOZZLE_Z_THERM_COEFF'] %}
{% set z_values = measure.z_values %}
{% set z_probed = printer.probe.last_z_result %} # absolute position of the probe
{% set _ = z_values.append(z_probed) %}
# debug
#{action_respond_info("Z Values: %s" % (z_values | pprint | string | replace("\n", "")))}
SET_GCODE_VARIABLE MACRO=MEASURE_NOZZLE_Z_THERM_COEFF VARIABLE=z_values VALUE="{z_values | pprint | replace("\n", "")}"
This is one of those “I’ve pushed Jinja/klipper too far” type macros. If you see bugs, let me know about it! I’ll update this post with fixes.