Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

z_thermal_adjust: Add Z thermal adjuster module #4157

Merged

Conversation

alchemyEngine
Copy link
Contributor

@alchemyEngine alchemyEngine commented Apr 10, 2021

Context/Rationale

For some enclosed 3D printers, particularly larger-format machines, thermal expansion of the vertical frame members can continue for hours after the build plate and air temperature have stabilized. If the machine has not reached equilibrium when a print starts, the toolhead can slowly drift upwards while the frame continues to expand. This is of particular concern with long first layer times, and can even result in weak adhesion with the second layer.

Using MCU position and a nozzle-triggered endstop on my Voron V2 printer:
image

Enclosed delta and moving gantry printers likely suffer most from this phenomenon.

Approach

Couple a temperature probe to a vertical member of the frame and use the coefficient of linear expansion for the frame material to calculate the expansion relative to last homing measurement, and transform Z accordingly.

I've personally had success with this approach, and several members of the Voron community have tested it on their machines with positive results.

All comments, criticism, and feedback are very welcome! Thank you.

Signed-off by: Robert Pazdzior robertp@norbital.com

Use a frame-coupled temperature probe to compensate for thermal
expansion in real-time.

Signed-off by: Robert Pazdzior <robertp@norbital.com>
@th3fallen
Copy link
Contributor

Can confirm this works perfectly

@KevinOConnor
Copy link
Collaborator

Thanks. In general it looks fine to me. I'd like to give a few more days for others to comment though.

I also saw some minor things:

  1. Please format the changes to Config_Reference.md like the other existing sections (eg, do not add blank lines between fields).
  2. The choice of coeff, frame_z_length, max_comp_z, etc, seem odd to me. I wonder if it be simpler to accept config options like temp1, z_offset_at_temp1, temp2, z_offset_at_temp2 and perform the coefficient calculations internally.
  3. It would be preferable to keep the copyright at the top of new code.py type files the same as all the other files - 1 line summary, 1 blank line, 1 line copyright, 1 blank line, 1 line license statement. Feel free to add any number of additional comment lines after the initial header block.
  4. I'm not sure the value in the QUERY_FRAME_COMP command - wouldn't it be easier to export the info via get_status()?
  5. Why track measured/maximum temperatures?

-Kevin

@alchemyEngine
Copy link
Contributor Author

alchemyEngine commented Apr 16, 2021

Thanks for having a look.

  1. Will do.
  2. The coefficient is generally not determined experimentally by the user. The coefficient can be easily looked up, is a physical constant for a given material, and can be difficult to accurately measure without proper equipment. Measuring the frame length is easy or already known. max_comp_z is a QOL feature to limit correction to a range above the bed in which it is actually consequential, similar to fade on bed meshing.
  3. Will do.
  4. Good point, I should expose that information in get_status() as well. I think it also useful, however, to be able to monitor the current compensation (last homing reference temp, current offset, and state) while printing via terminal.
  5. I believe this is necessary for Moonraker compatibility, but don't have it running personally to test.

-Rob

Add 'current_z_comp', 'frame_ref_temp', and 'state' to get_status.
Match config_reference formatting.

Signed-off by: Robert Pazdzior <robertp@norbital.com>
Signed-off by: Robert Pazdzior <robertp@norbital.com>
@KevinOConnor
Copy link
Collaborator

The coefficient is generally not determined experimentally by the user.

Okay, but that is quite surprising to me. It's surprising that commodity parts would actually follow theoretical expansion numbers so closely. I would have thought a user would want to measure and confirm the actual deviation.

Separately, just as high-level feedback, this support certainly looks interesting, but I don't see a rush to merge it. I'd be interested in hearing the results of many users over the coming months.

Cheers,
-Kevin

@KoiosLabs
Copy link

I've been running this branch for a a couple months now and its fantastic. It seems to greatly improve my first layer's which have been a problem for me since voron 2.1...

@th3fallen
Copy link
Contributor

I've also had zero issues. I'd love to see it merged

@KevinOConnor
Copy link
Collaborator

Thanks. I agree it looks like a useful tool. It would be great to get feedback on the type of printer and the specific configuration for each deployment that is currently using this PR.

-Kevin

@KoiosLabs
Copy link

KoiosLabs commented May 3, 2021

Thanks. I agree it looks like a useful tool. It would be great to get feedback on the type of printer and the specific configuration for each deployment that is currently using this PR.

-Kevin

350^3 voron 2.4, using the default config and frame height 530.0

@th3fallen
Copy link
Contributor

th3fallen commented May 4, 2021

Voron 2.4 300^3 default config

@alchemyEngine
Copy link
Contributor Author

alchemyEngine commented May 5, 2021

Thanks. I agree it looks like a useful tool. It would be great to get feedback on the type of printer and the specific configuration for each deployment that is currently using this PR.

-Kevin

Sounds like a good idea to me, Kevin. Thanks.

Everybody I know currently using it is running a Voron V2. Since I only shared/discussed it on the Voron Discord, that's also probably the only source of testers unless somebody is motivated by this PR.

In theory, any enclosed printer in which the XY gantry is connected to the build surface through an appreciable length of aluminum frame would also be susceptible. The common fixed top gantry and descending lead-screw driven bed layout should manifest the same expansion symptoms.

Though here it gets a bit more complicated:

  • the lead-screw steel expansion coefficient is much smaller than that of aluminum
  • the absolute expansion is Z-position dependant, as the length of screw between the bottom of the frame and bed varies
  • the absolute compensation required would therefore be the difference between the two

Since this is a more common arrangement (see Ender 5, TronXY X5, Hypercubes, Voron V1, etc). Perhaps implementation of configuration options for this two-factor expansion might broaden the applicability of the module?

@glowtape
Copy link

glowtape commented May 6, 2021

That's an interesting idea. Does it run permanently, or just adjust actual print commands? I'm just asking, because I could swear I see some minor frame expansion when doing long bed probing (e.g. 5x5 multisampled).

@robschwieb
Copy link

robschwieb commented May 10, 2021

As a newer Voron V2.4 owner, this feature is invaluable. Not necessary for people only printing small footprint items but once you start printing full plates, absolutely necessary. I was seeing upwards of 0.04mm of drift due to frame expansion that was unable to be solved by heatsoaking alone. Something about printing that first layer heats up my 300mm V2.4 more than just hours of idle heatsoaking is capable of.

This feature is a must have and made me go from hating my expensive and time consuming Voron to loving it again when printing full plates of parts.

Only thing it needs is a smoothing function to cut down on the unnecessary rapid fire z-stepping at times. Should be easy to implement.

@jalfeld
Copy link

jalfeld commented May 11, 2021

Currently running this on a Voron V2.4 and it is working perfectly. This has greatly improved layer consistency as long prints progress and the frame heats and expands. Even after an extended heat soak prior to starting the print there is a noticeable improvement in layer consistency using this code.

Copy link
Collaborator

@KevinOConnor KevinOConnor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks.

I guess my main comment is that the configuration of this module seems more complicated than I think it could be. Is there any reason we can't simplify this to:

[frame_expansion_compensation]
#adjustment:
#   The amount of Z adjustment (in millimeters per degree Celsius) to apply.
#sensor_type:
#sensor_pin:
#min_temp:
#max_temp:
#gcode_id:
#   See the "heater_generic" and "extruder" sections for the definition of these
#   parameters.

If I understand the code correctly, it seems to apply a Z offset based on the temperature difference since the last Z home. If so, I think both the config and code could be simplified.

See some additional comments below.

-Kevin

Comment on lines 69 to 70
raise self.printer.config_error(
"'%s' is not a valid stepper" % self.z_stepper_name)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not valid to raise an error in a "klippy:ready" event handler (see docs/Code_Overview.md). You probably want the "klippy:connect" event handler.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks.

I guess my main comment is that the configuration of this module seems more complicated than I think it could be. Is there any reason we can't simplify this to:

[frame_expansion_compensation]
#adjustment:
#   The amount of Z adjustment (in millimeters per degree Celsius) to apply.
#sensor_type:
#sensor_pin:
#min_temp:
#max_temp:
#gcode_id:
#   See the "heater_generic" and "extruder" sections for the definition of these
#   parameters.

If I understand the code correctly, it seems to apply a Z offset based on the temperature difference since the last Z home. If so, I think both the config and code could be simplified.

See some additional comments below.

-Kevin

Thanks for the review, Kevin.

In principal I agree, and would like to simplify both before finalizing.

I would like to keep the coefficient config parameter name, as this is consistent with the technical term, "coefficient of linear expansion". As well, the length of the frame member is a necessary input as the change is length is proportional to the total length: l_total = l_ref + l_ref * (coeff * (t_current - t_ref)

gantry_factor is a kind of stand-in for broader compatibility I'd like to introduce for top mounted fixed gantry/moving bed machine down the line. Hard-coding the 0.5 factor necessary for Voron V2 would make it specific to that machine, so until I can think of a nice way to take care of as many use-cases as possible I would like to keep this factor accessible in the config.

Is there any reason to omit the optional limiting safety parameters? Perhaps I could hard-code some safety checks in this regard. If the min_temp parameter is defined, will the ADC range error out in time to avoid crashing the head into the bed while trying to e.g. correct for thermal contraction of suddenly dropping to -80C?

Comment on lines 99 to 101
def handle_homing_move_end(self, hmove):
'Triggered when Z axis is homed.'
if self.check_eligible(hmove.get_mcu_endstops()):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should catch the "homing:home_rails_end" event and look for 2 in homing_state.get_axes().

Comment on lines 149 to 151
if temp:
self.measured_min = min(self.measured_min, temp)
self.measured_max = max(self.measured_max, temp)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The if temp check doesn't make sense - it's always valid (or there is a severe error occurring). Unless there is a need for it, I don't think this code should be tracking measure_min/measured_max.

Copy link
Contributor Author

@alchemyEngine alchemyEngine May 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not running Mainsail or other Moonraker-interfaced frontends, so I can't test this at the moment without some effort, but in so far as I understood min/max tracking was necessary for compatibility. It was a kind contribution from @fsironman necessary to get it running on his system, perhaps he might be able to comment?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The frame_expansion_compensation class is more or less a temperature sensor class that can apply a Z-offset.
The feature itself will work without tracking measure_min/measured_max yes,
but wont work with any third party (in this case moonrake/mainsail)
for which it makes sense to display the temperature from frame_expansion_compensation in its UI.

Hence the same functions as the generic temperature sensor are included (https://github.com/KevinOConnor/klipper/blob/master/klippy/extras/temperature_sensor.py)

This will allow 3rd party pluging to display the temperature in its UI as if its a generic temperature sensor:
unknown
(below without it, on top proper display with including min/max change)

Comment on lines 143 to 144
self.last_position[:] = newpos
self.last_position[:] = newpos
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate lines.

@oab1
Copy link

oab1 commented May 12, 2021

I have a question, as I have often wondered if this was some of the reason my second layers were failing on larger prints, also on a 350mm^3 voron v2.

Why do you think the frame expansion effects the Z height of the print head? On the Voron, wouldn't it only increase the tension on the z belt loops? I dont see how that directly translates into the gantry rising.

Your measurements speak for themselves, as do the positive results others are finding. I look forward to trying it myself. Just curious as to the reason why you think this is happening.

@alchemyEngine
Copy link
Contributor Author

I have a question, as I have often wondered if this was some of the reason my second layers were failing on larger prints, also on a 350mm^3 voron v2.

Why do you think the frame expansion effects the Z height of the print head? On the Voron, wouldn't it only increase the tension on the z belt loops? I dont see how that directly translates into the gantry rising.

Your measurements speak for themselves, as do the positive results others are finding. I look forward to trying it myself. Just curious as to the reason why you think this is happening.

Hello,

The reason the tension increases is because the belts are stretching: the Z idlers are mounted to the top of the frame while the drive pulleys are mounted to bottom, therefore the expansion increases the belt length and consequently the tension. Because of the top mounted idler acting as a pulley, the movement gets reduced to 0.5x the increase in frame length (gantry_factor: 0.5 for Voron V2s).

@alchemyEngine
Copy link
Contributor Author

@KevinOConnor I'd like to do a docs write-up on the module's intended use-case, how it works, and how to set it up properly. Would this be welcome on your end?

@KevinOConnor
Copy link
Collaborator

the length of the frame member is a necessary input as the change is length is proportional to the total length: l_total = l_ref + l_ref * (coeff * (t_current - t_ref)

If I understand this code correctly, if I define a printer with coeff = 23.4, frame_z_length = 300, gantry_factor = .5, and if I home at exactly 25 degrees - then the adjustment formula falls out to:
offset = coeff * .000001 * frame_z_length * gantry_factor * (last_temp - last_home_temp)

In this case, I think it would be preferable to define a single adjustment = .003510 in the config and use an internal formula of offset = adjustment * (last_temp - last_home_temp) - last_home_offset. I think both the code and config are easier to understand with that simpler math.

FWIW, with the current code, if I home at 25 degrees, and print a layer at 30 degrees it will apply an offset of .017550mm, and if I print another layer at 35 degrees it will apply an offset of .035100mm. However, if I home at 30 degrees and print a layer at 35 degrees it will apply an offset of 0.0175705335mm. It seems odd to me that going from 30-35 degrees when homing at 25 degrees would have a different offset than going from 30-35 degrees when homing at 30 degrees (.017550mm vs 0.0175705335mm). It looks like a code error to me, but perhaps I'm missing something.

-Kevin

@alchemyEngine
Copy link
Contributor Author

alchemyEngine commented May 15, 2021

the length of the frame member is a necessary input as the change is length is proportional to the total length: l_total = l_ref + l_ref * (coeff * (t_current - t_ref)

If I understand this code correctly, if I define a printer with coeff = 23.4, frame_z_length = 300, gantry_factor = .5, and if I home at exactly 25 degrees - then the adjustment formula falls out to:
offset = coeff * .000001 * frame_z_length * gantry_factor * (last_temp - last_home_temp)

In this case, I think it would be preferable to define a single adjustment = .003510 in the config and use an internal formula of offset = adjustment * (last_temp - last_home_temp) - last_home_offset. I think both the code and config are easier to understand with that simpler math.

FWIW, with the current code, if I home at 25 degrees, and print a layer at 30 degrees it will apply an offset of .017550mm, and if I print another layer at 35 degrees it will apply an offset of .035100mm. However, if I home at 30 degrees and print a layer at 35 degrees it will apply an offset of 0.0175705335mm. It seems odd to me that going from 30-35 degrees when homing at 25 degrees would have a different offset than going from 30-35 degrees when homing at 30 degrees (.017550mm vs 0.0175705335mm). It looks like a code error to me, but perhaps I'm missing something.

-Kevin

Sorry, I'm really not explaining this clearly and forgot to mention that the length of the frame at the time of homing is also calculated assuming the length provided in the config was measured at room temperature (25C). This value is then used as the reference length to calculate the necessary Z offset.

@KevinOConnor
Copy link
Collaborator

the length of the frame at the time of homing is also calculated assuming the length provided in the config was measured at room temperature (25C).

Okay, but that calculation does not look correct to me. In the example above, if I home at 25 degrees, and later put down a layer at 40 degrees followed by a layer at 41 degrees then that layer itself has a Z offset of .003510mm subtracted from it. That makes sense as the Z stepper should move less because of heat expansion. However, if I home at 30 degrees and later put down a layer at 40 degrees followed by a layer at 41 degrees then that layer has .0035141067mm subtracted from it. It does not look correct to me that homing temperature is altering the impact of temperature later in the print.

Cheers,
-Kevin

@alchemyEngine
Copy link
Contributor Author

alchemyEngine commented May 16, 2021

It does not look correct to me that homing temperature is altering the impact of temperature later in the print.

Actually, this is correct, and the reason for sampling the temperature at the time of Z-homing: longer objects expand to a greater (absolute) extent than shorter objects. If you home at 30c then your frame is already longer than it was at 25c, therefore if used as a reference will grow more per degC/K than at 25c - hence the larger offset you mentioned. And since homing to Z-min essentially 'resets' the expansion by driving down past this expansion and re-referencing Z, we don't really care how much a 25c frame would have expanded from 40-41c but rather how much the 30c frame we homed to and are referencing is expanding.

I hope that clarifies my considerations in laying out the calculation.

Edit: that said, I could be using this equation incorrectly. Maybe you're not meant to 'stack' them like I'm doing.

@alchemyEngine
Copy link
Contributor Author

A more accurate relationship between current_temperature and frame length may like this:
length = exp(coeff * 1e-6 * (current_temperature - ref_temperature)) * ref_length
ref_length is length of the frame measured at ref_temperature (for example: ref_length=300mm at ref_temperature=25C)

I'm sorry but I don't see why this would be any more accurate. Could you please explain?

@oab1
Copy link

oab1 commented May 16, 2021

I have a question, as I have often wondered if this was some of the reason my second layers were failing on larger prints, also on a 350mm^3 voron v2.
Why do you think the frame expansion effects the Z height of the print head? On the Voron, wouldn't it only increase the tension on the z belt loops? I dont see how that directly translates into the gantry rising.
Your measurements speak for themselves, as do the positive results others are finding. I look forward to trying it myself. Just curious as to the reason why you think this is happening.

Hello or z

The reason the tension increases is because the belts are stretching: the Z idlers are mounted to the top of the frame while the drive pulleys are mounted to bottom, therefore the expansion increases the belt length and consequently the tension. Because of the top mounted idler acting as a pulley, the movement gets reduced to 0.5x the increase in frame length (gantry_factor: 0.5 for Voron V2s).

That doesn't answer my question. I agree the belt tension increases, and stated as much. The question is how much does the increase in tension raise the gantry? I would propose it would depend on the height of the gantry, ie if you are in the middle of your Z travel, your factor of 0.5x the rate of frame length extension makes good sense. However, if you are at z = 0 or z = max, that surely wouldn't still be the case, would it? I would state the most accurate factor should be a a function of % of z travel currently being used. ( total frame expansion in z * current %z travel).

Which is to say the belt is being stretched linearly, with no displacement at the lower drive pulleys, and full displacement at the location of the upper idlers. Assuming the lower drive pulleys is effectively fixed, and the upper idler pulleys are shifting away as the machine warms up. I don't think it makes sense to assume you can just halve the expansion of the vertical frame members because of the idler pulleys.

@alchemyEngine
Copy link
Contributor Author

alchemyEngine commented May 16, 2021

Why do you think the frame expansion effects the Z height of the print head? I dont see how that directly translates into the gantry rising.

You originally asked why, not how. The belts stretching with the frame and dragging the gantry with it is why. How is, however, a very good question and one that I'm honestly not entirely sure of the answer to.

if you are in the middle of your Z travel, your factor of 0.5x the rate of frame length extension makes good sense. However, if you are at z = 0 or z = max, that surely wouldn't still be the case, would it?

I would expect the belt to stretch more or less uniformly, but I genuinely do not know. I'd like to look into this some more down the road, and if an easy-to-use configuration for such compensation could be implemented (belted: true|false?) that would be great. However, it might be beyond the original intended use-case for this module: first/second layer consistency.

If you're willing to look into the physics and provide some reading, that would be greatly appreciated, however. My main priority right now is cleaning up the code to Kevin's standards and implementing smoothing for the machine-gun microstepping sound.

@lvitol
Copy link

lvitol commented May 17, 2021

I'm sorry but I don't see why this would be any more accurate. Could you please explain?

The difference is very small, ignore my comment.
I was trying to calculate frame length at temperate x, i found this:
frame_length_at_x_a = frame_length_at_ref_temperature * (1 + coeff * 1e-6 * (x - ref_temperature))
in your calculation:
frame_length_at_x_b = (frame_length_at_ref_temperature * (1 + coeff * 1e-6 * (homing_temperature - ref_temperature))) * (1 + coeff * 1e-6 * (x - homing_temperature))

frame_length_at_x_a != frame_length_at_x_b

then i found the difference is so small, we can just ignore.

wlhlm added a commit to wlhlm/klipper that referenced this pull request Aug 4, 2022
@github-actions github-actions bot added the inactive Not currently being worked on label Aug 25, 2022
@github-actions
Copy link

It looks like this GitHub Pull Request has become inactive. If there are any further updates, you can add a comment here or open a new ticket.

Best regards,
~ Your friendly GitIssueBot

PS: I'm just an automated script, not a human being.

@github-actions github-actions bot closed this Aug 25, 2022
Signed-off-by: Robert Pazdzior <robertp@norbital.com>
Signed-off-by: Robert Pazdzior <robertp@norbital.com>
Signed-off-by: Robert Pazdzior <robertp@norbital.com>
Signed-off-by: Robert Pazdzior <robertp@norbital.com>
@alchemyEngine
Copy link
Contributor Author

@KevinOConnor Sorry for the inactivity, got stuck on the code and then summer carried me away. I think I've addressed all of your previous points, and the PR should be ready for another look.

Thanks,
-alch3my

@KevinOConnor KevinOConnor reopened this Aug 25, 2022
@KevinOConnor KevinOConnor removed inactive Not currently being worked on pending feedback Topic is pending feedback from submitter labels Aug 25, 2022
@KevinOConnor
Copy link
Collaborator

Thanks. Looks fine to me. Can you confirm this is still ready to merge? If so, I'll go ahead and merge.

-Kevin

@KevinOConnor KevinOConnor added the pending feedback Topic is pending feedback from submitter label Sep 23, 2022
@alchemyEngine
Copy link
Contributor Author

I'd say it's ready to go out the door, merge away! Thanks for all your help and the discussions along the way, Kevin.

@KevinOConnor KevinOConnor merged commit 34870d3 into Klipper3d:master Sep 25, 2022
@KevinOConnor
Copy link
Collaborator

Thanks.

-Kevin

@danorder
Copy link

Internal error during connect: 'stepper_z' I assume the current implementation doesn't support delta kinematics?

eliasbakken pushed a commit to intelligent-agent/klipper that referenced this pull request Jan 31, 2023
Use a frame-coupled temperature probe to compensate for thermal
expansion in real-time.

Signed-off by: Robert Pazdzior <robertp@norbital.com>
Gi7mo pushed a commit to Gi7mo/klipper that referenced this pull request Feb 15, 2023
Use a frame-coupled temperature probe to compensate for thermal
expansion in real-time.

Signed-off by: Robert Pazdzior <robertp@norbital.com>
tntclaus pushed a commit to tntclaus/klipper that referenced this pull request May 29, 2023
Use a frame-coupled temperature probe to compensate for thermal
expansion in real-time.

Signed-off by: Robert Pazdzior <robertp@norbital.com>
revilo196 pushed a commit to revilo196/klipper that referenced this pull request Jun 24, 2023
Use a frame-coupled temperature probe to compensate for thermal
expansion in real-time.

Signed-off by: Robert Pazdzior <robertp@norbital.com>
@github-actions github-actions bot locked and limited conversation to collaborators Sep 30, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
pending feedback Topic is pending feedback from submitter
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet