-
-
Notifications
You must be signed in to change notification settings - Fork 5.3k
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
toolhead: Fixed junction deviation calculation for straight segments #6747
base: master
Are you sure you want to change the base?
Conversation
FWIW, I'm unsure about the checks failure, but I suspect this is unrelated to my change. |
Hi Dmitry, I also noticed this problem once (with stops), but it was not fundamental from the point of view of normal use, and manifests itself only at very low acceleration. You probably fixed this for your new resonance measurement algorithm, for which I also want to thank you. I didn't really understand what cases Kevin was talking about with "close to straight" moves. I've plotted the old and new code, and I haven't seen any changes. I wanted to open a topic on discourse, but I caught sight of this PR today, and I outlined the part of the material here. For example, this is how Klipper slices the movements generated by gcode from SuperSlicer, at maximum resolution, without using arcs. On the left is the old, on the right is the new one, the toolhead parameters (speed, etc) are shown below the graph - I am also stupefied by the current way of determining the speed on circles. The worse your gcode file is, the larger length of polygons it have, the slower the circles\turns that are so fond of adding to all 3d models now will be printed. But I do not know the right alternatives, unfortunately. I include arcs to simulate more detail of polygons. The same code, but with arcs enabled, the resolution of arcs in the slicer is 0.01, he divided this circle into two arcs. The resolution of arcs for the Klipper ArcParser is also signed at the bottom of the graph. By default, the arcs in the Klipper have a section resolution of 1 mm, which simply does not make sense in conventional printing. Let's start with him - To put it mildly, it does not look like a circle, and the average speed has noticeably decreased, due to the fact that the angle between the sections is large. Next, the arc resolution is 0.1 mm, this is the most popular value among users- This already looks like a circle), please also note that the average printing speed of the circle has risen significantly. But, again, there are no changes regarding the previous code. And you can still see the curved line after the arc, that is, the arc ended in the wrong place for some reason. And also let's see the resolution of 0.01mm, for example, rpi5 can safely withstand such large-scale calculations during printing. I noticed this bug a couple of months ago, the more accurate the resolution (< 0.1mm), the more the speed decreases, although the angle between the sections should be closer to straight. So far, I have not figured out what is the matter, perhaps the ArcParser somehow crookedly convert the arcs.. I will also attach the GCode files that I used - In conclusion, I have not found any practical places where Dmitry's new code would change anything in a negative way. But I think that the klipper should not slow down when printing, for example, a 100mm cylinder, just because it was not given a sufficiently accurate GCode. I will continue to experiment and look for solutions to this problem. And I'll probably duplicate it into a Klipper discourse soon, too. Correct me if I don't understand something. Thanks! -Maxim |
Yes - it's only there to prevent a divide by zero. So, what if we did this instead: --- a/klippy/toolhead.py
+++ b/klippy/toolhead.py
@@ -72,7 +72,7 @@ class Move:
+ axes_r[2] * prev_axes_r[2])
if junction_cos_theta > 0.999999:
return
- junction_cos_theta = max(junction_cos_theta, -0.999999)
+ junction_cos_theta = max(junction_cos_theta, -0.999999999999)
sin_theta_d2 = math.sqrt(0.5*(1.0-junction_cos_theta))
R_jd = sin_theta_d2 / (1. - sin_theta_d2)
# Approximated circle must contact moves no further away than mid-move
Yeah, I'm not sure either. You could try rebasing this to the latest Klipper master so that it picks up commit f2e69a3.
Consider the case where the toolhead is at position 0,0 and the following two moves are made: To address this, the code in commit a4439b9 was added so that the code does not calculate the junction speed solely on angles - it also looks at the move length. This avoids the problem of these infinitesimal moves that some users were running into when printing some models (and/or with some slicer settings). The PR here would bypass the code in a4439b9 based on the angle of moves, but I'm concerned that may reintroduce a corner case because one can introduce a series of infinitesimal moves that all have an arbitrarily small intra-move angle. Cheers, |
OK, thank you for the feedback, Kevin. Let me think about your examples a bit (and your alternative proposal too, which I'd need to give a test). |
@KevinOConnor, if
|
b340245
to
b97d6e2
Compare
FWIW, I think the current code still does it a bit backwards. That is, it has somewhat arbitrary limit for |
Well, that's basically what the code attempts to do today. Using 'Inf' is not desirable because those special floating point values can propagate in subtle ways and lead to hard to debug errors in other parts of the code. So, the goal is to use an arbitrarily large constant instead of 'Inf' . The -0.999999 was intended to bring about that arbitrarily large constant - if the resulting value isn't big enough then we can add more 9s (eg, -0.999999999999).
I don't understand what that additional check does ( At a minimum, the checks on FWIW, if this change is not a prerequisite for #6723 then I think we should merge that first. Cheers, |
b97d6e2
to
2072a74
Compare
Thanks Kevin,
The curvature radius is equal to Edit: and just to expand on this point a bit, if we determine that the radius of curvature is infinite (or very large), we should not add additional limits on junction_v2 that is related to centripetal acceleration calculation, which is the behavior of the new code in this PR. FWIW, the regular junction deviation should also not apply limits in this case, but since that code does not depend on move_d, it is sufficient to use the regular
Yes, you are right, I messed that up previously. I addressed that now, though a bit differently (by taking
No, this change is a prerequisite for that one, so this PR should go in first. |
Oh - okay. I misunderstood that.
Okay, thanks. I think I understand now. However, I'm not sure about this approach for two reasons: it seems to be "rounding up" to higher junction speeds where I think we should prefer "rounding down", and this adds additional complexity to the "fast path". On the first point, it seems this code would basically say "if it seems the junction is mostly straight then assume it is straight and don't limit speed at all". But that's seems backwards to me - if we're unsure about the math we should round down the junction speeds, not round them up. When in doubt, we want to go slower through junctions, not present a possibility that we go too fast. These are all "corner cases" that should not impact real-world prints, so better to "do no harm". We could change the first test to Thoughts? EDIT: Just for perspective, if we limit junction_cos_theta to -0.999999999999 then R_jd would be 3999644429280 (or >=6435656mm/s with square_corner_velocity=5 and accel>=100) and tan_theta_d2 would be 1414229 (or >=8409mm/s with move_d>=1 and accel>=100). |
On further thought, I'm a little confused. What is the new code doing that requires this change? Based on my quick calculations, the current code limits junction_cos_theta to -0.999999, which would result in an R_jd=3999998 (>=6435mm/s with typical values) and a tan_theta_d2=1414 (>=265mm/s with move_d>=1 and accel>=100). I think I may be missing something subtle. What is the test case doing that would be limited by the above? Thanks, |
Sorry, I'm unsure I understand where 'rounding up' and 'rounding down' comes from. FWIW,
is mostly right, however not entirely. The problem at hand is that as the segmentation for a curve becomes finer and finer, the angle between consecutive segments becomes closer and closer to 180 degrees (and cos(theta/2) approaches 0). And so, the current mainline Klipper code has an issue: it has a minimum limit for cos(theta/2), and thus technically produces incorrect results for angles theta very close to 180 degrees, both for consecutive straight segments and for very small segments forming a curve. Adjusting the limit (e.g. setting it to -0.999999999999) just pushes the problematic cases further down the line. My approach is based on the following observation: for smooth curves, as the segmentation becomes finer, both move_d and cos(theta/2) go to 0, but their ratio does not (it does not go to either 0 or infinity), as limit(move_d / cos(theta/2)) = 2 * R. Thus, by checking the ratio move_d / cos(theta/2), we can tell apart the cases "a curve with finite curvature, just very fine"and "(sufficiently) straight segments" regardless of how fine the segmentation is, and without having to play with limits too much, and apply the junction velocity limits from centripetal acceleration only to the first case. You could still say that the code contains a threshold, but the behavior of the code does not depend on its value too much, basically if the threshold is sufficiently large, it won't matter how large exactly it is. This is not the case for junction_cos_theta, as for any threshold you could choose sufficiently small segmentation that would make code produce technically incorrect junction velocity limits.
I tried to explain above why I believe the approach with adjusting the threshold is not optimal: it may still cause issues for sufficiently fine gcode. And I suspect the effect of the changes on the code performance is almost negligible: there are far more complex calculations running per move, e.g. several sqrt operations, then look-ahead processing with moving the moves in and out of the queue, and such. I also did not want to invest into premature optimizations, however some optimizations can be done (e.g. removing
As was reported in the other PR, the problematic code snippet (presented in this comment) is very similar to what the new test executes from time to time, leading to the wrong velocities and timing of moves. |
What I meant is, if there is a move that results in a natural calculation of junction_cos_theta=-0.999999 (eg, So, by "round up" I meant that the new code identifies a set of unusual corner cases and allows them to go faster than they nominally might, where as the current code can make some moves go slower but should not permit moves to go faster than they nominally should ("round down").
I agree that the performance impact is negligible and I'm okay with changing the code if needed. If we do need to change the code, could we directly test for the divide by zero case so we avoid optimizing moves that are not actually straight? (That is,
So the new resonance testing code effectively issues a Thanks again, |
Thinking about this further, I think the 0.999999 constants are arbitrary and kinda confusing. How about we just remove them with something like: --- a/klippy/toolhead.py
+++ b/klippy/toolhead.py
@@ -65,25 +65,30 @@ class Move:
# Allow extruder to calculate its maximum junction
extruder_v2 = self.toolhead.extruder.calc_junction(prev_move, self)
# Find max velocity using "approximated centripetal velocity"
+ move_jd_v2 = prev_move_jd_v2 = extruder_v2
axes_r = self.axes_r
prev_axes_r = prev_move.axes_r
junction_cos_theta = -(axes_r[0] * prev_axes_r[0]
+ axes_r[1] * prev_axes_r[1]
+ axes_r[2] * prev_axes_r[2])
- if junction_cos_theta > 0.999999:
- return
- junction_cos_theta = max(junction_cos_theta, -0.999999)
- sin_theta_d2 = math.sqrt(0.5*(1.0-junction_cos_theta))
- R_jd = sin_theta_d2 / (1. - sin_theta_d2)
+ sin_theta_d2 = math.sqrt(max(0.5*(1.0-junction_cos_theta), 0.))
+ one_minus_sin_theta_d2 = 1. - sin_theta_d2
+ if one_minus_sin_theta_d2 > 0.:
+ R_jd = sin_theta_d2 / one_minus_sin_theta_d2
+ move_jd_v2 = R_jd * self.junction_deviation * self.accel
+ prev_move_jd_v2 = (R_jd * prev_move.junction_deviation
+ * prev_move.accel)
# Approximated circle must contact moves no further away than mid-move
- tan_theta_d2 = sin_theta_d2 / math.sqrt(0.5*(1.0+junction_cos_theta))
- move_centripetal_v2 = .5 * self.move_d * tan_theta_d2 * self.accel
- prev_move_centripetal_v2 = (.5 * prev_move.move_d * tan_theta_d2
- * prev_move.accel)
+ move_centripetal_v2 = prev_move_centripetal_v2 = extruder_v2
+ cos_theta_d2 = math.sqrt(max(0.5*(1.0+junction_cos_theta), 0.))
+ if cos_theta_d2 > 0.:
+ tan_theta_d2 = sin_theta_d2 / cos_theta_d2
+ move_centripetal_v2 = .5 * self.move_d * tan_theta_d2 * self.accel
+ prev_move_centripetal_v2 = (.5 * prev_move.move_d * tan_theta_d2
+ * prev_move.accel)
# Apply limits
self.max_start_v2 = min(
- R_jd * self.junction_deviation * self.accel,
- R_jd * prev_move.junction_deviation * prev_move.accel,
+ move_jd_v2, prev_move_jd_v2,
move_centripetal_v2, prev_move_centripetal_v2,
extruder_v2, self.max_cruise_v2, prev_move.max_cruise_v2,
prev_move.max_start_v2 + prev_move.delta_v2) That should reduce the weird corner cases and is maybe a little easier to read. Thoughts? EDIT: Fixed broken patch. |
Thanks Kevin,
I understand that you meant
Incidentally, even
Admittedly, if junction_cos_theta > 0.999999:
return
- junction_cos_theta = max(junction_cos_theta, -0.999999)
- sin_theta_d2 = math.sqrt(0.5*(1.0-junction_cos_theta))
- R_jd = sin_theta_d2 / (1. - sin_theta_d2)
- # Approximated circle must contact moves no further away than mid-move
- tan_theta_d2 = sin_theta_d2 / math.sqrt(0.5*(1.0+junction_cos_theta))
- move_centripetal_v2 = .5 * self.move_d * tan_theta_d2 * self.accel
- prev_move_centripetal_v2 = (.5 * prev_move.move_d * tan_theta_d2
- * prev_move.accel)
+ sin_theta_d2 = math.sqrt(0.5 * (1.0-junction_cos_theta))
+ cos_theta_d2 = math.sqrt(0.5 * max(1.0+junction_cos_theta, 0.))
# Apply limits
- self.max_start_v2 = min(
- R_jd * self.junction_deviation * self.accel,
- R_jd * prev_move.junction_deviation * prev_move.accel,
- move_centripetal_v2, prev_move_centripetal_v2,
+ max_start_v2 = min(
extruder_v2, self.max_cruise_v2, prev_move.max_cruise_v2,
prev_move.max_start_v2 + prev_move.delta_v2)
+ # Check that the radius of curvature for this junction is finite
+ if min(self.move_d, prev_move.move_d) < 99999999.9 * cos_theta_d2:
+ R_jd = sin_theta_d2 / (1. - sin_theta_d2)
+ # Approximated circle must contact moves no further than mid-move, and
+ # centripetal_v2 = 0.5 * tan_theta_d2 * move_d * accel
+ quarter_tan_theta_d2 = .25 * sin_theta_d2 / cos_theta_d2
+ move_centripetal_v2 = quarter_tan_theta_d2 * self.delta_v2
+ prev_move_centripetal_v2 = quarter_tan_theta_d2 * prev_move.delta_v2
+ max_start_v2 = min(
+ max_start_v2,
+ R_jd * self.junction_deviation * self.accel,
+ R_jd * prev_move.junction_deviation * prev_move.accel,
+ max_start_v2, move_centripetal_v2, prev_move_centripetal_v2)
+ self.max_start_v2 = max_start_v2
self.max_smoothed_v2 = min(
- self.max_start_v2
- , prev_move.max_smoothed_v2 + prev_move.smooth_delta_v2)
+ max_start_v2, prev_move.max_smoothed_v2 + prev_move.smooth_delta_v2) What do you think about it?
My only concern is that this code does not entirely eliminate a possibility to get
Yes, more or less, as evident by this chart (note very small non-zero accelerations occasionally in the beginning, for example): |
Yes, in this case the |
Thanks for the additional information.
Yes, that specific example was for R_jd.
It generally seems fine - merging the two branches and using delta_v2 seems like a good idea. I guess I'm a little uncomfortable by the new 99999999.9 constant though. Basically it does
The
Okay, thanks. I think I understand it better now. Cheers, |
So, your patch but without the constants might look something like: --- a/klippy/toolhead.py
+++ b/klippy/toolhead.py
@@ -64,32 +64,32 @@ class Move:
return
# Allow extruder to calculate its maximum junction
extruder_v2 = self.toolhead.extruder.calc_junction(prev_move, self)
+ max_start_v2 = min(
+ extruder_v2, self.max_cruise_v2, prev_move.max_cruise_v2,
+ prev_move.max_start_v2 + prev_move.delta_v2)
# Find max velocity using "approximated centripetal velocity"
axes_r = self.axes_r
prev_axes_r = prev_move.axes_r
junction_cos_theta = -(axes_r[0] * prev_axes_r[0]
+ axes_r[1] * prev_axes_r[1]
+ axes_r[2] * prev_axes_r[2])
- if junction_cos_theta > 0.999999:
- return
- junction_cos_theta = max(junction_cos_theta, -0.999999)
- sin_theta_d2 = math.sqrt(0.5*(1.0-junction_cos_theta))
- R_jd = sin_theta_d2 / (1. - sin_theta_d2)
- # Approximated circle must contact moves no further away than mid-move
- tan_theta_d2 = sin_theta_d2 / math.sqrt(0.5*(1.0+junction_cos_theta))
- move_centripetal_v2 = .5 * self.move_d * tan_theta_d2 * self.accel
- prev_move_centripetal_v2 = (.5 * prev_move.move_d * tan_theta_d2
- * prev_move.accel)
+ sin_theta_d2 = math.sqrt(max(0.5*(1.0-junction_cos_theta), 0.))
+ cos_theta_d2 = math.sqrt(max(0.5*(1.0+junction_cos_theta), 0.))
+ one_minus_sin_theta_d2 = 1. - sin_theta_d2
+ if cos_theta_d2 > 0. and one_minus_sin_theta_d2 > 0.:
+ R_jd = sin_theta_d2 / one_minus_sin_theta_d2
+ move_jd_v2 = R_jd * self.junction_deviation * self.accel
+ pmove_jd_v2 = R_jd * prev_move.junction_deviation * prev_move.accel
+ # Approximated circle must contact moves no further than mid-move
+ quarter_tan_theta_d2 = .25 * sin_theta_d2 / cos_theta_d2
+ move_centripetal_v2 = quarter_tan_theta_d2 * self.delta_v2
+ pmove_centripetal_v2 = quarter_tan_theta_d2 * prev_move.delta_v2
+ max_start_v2 = min(max_start_v2, move_jd_v2, pmove_jd_v2,
+ move_centripetal_v2, pmove_centripetal_v2)
# Apply limits
- self.max_start_v2 = min(
- R_jd * self.junction_deviation * self.accel,
- R_jd * prev_move.junction_deviation * prev_move.accel,
- move_centripetal_v2, prev_move_centripetal_v2,
- extruder_v2, self.max_cruise_v2, prev_move.max_cruise_v2,
- prev_move.max_start_v2 + prev_move.delta_v2)
+ self.max_start_v2 = max_start_v2
self.max_smoothed_v2 = min(
- self.max_start_v2
- , prev_move.max_smoothed_v2 + prev_move.smooth_delta_v2)
+ max_start_v2, prev_move.max_smoothed_v2 + prev_move.smooth_delta_v2)
def set_junction(self, start_v2, cruise_v2, end_v2):
# Determine accel, cruise, and decel portions of the move distance
half_inv_accel = .5 / self.accel -Kevin |
Signed-off-by: Dmitry Butyugin <[email protected]>
2072a74
to
ae94259
Compare
I updated this PR to match your suggestion, and I also tested it on top of the latest updated resonance testing code, and it seems to be working fine: |
As per suggestion in #6723, opening a separate PR for the toolhead junction deviation fix. As for
I believe right now the constant +/-0.999999 is somewhat arbitrary, and it is easy to add one-two more 9s to it. And for example currently we have
tan_theta_d2 = 1414.21..
forjunction_cos_theta = -0.999999
and.5 * tan_theta_d2 * self.accel = 707106.6...
(with accel = 1000), and for a move of 0.01 mm it would give the cornering velocity of 84 mm/sec (from centripetal acceleration). Adding one more 9 to the constant would give ~150 mm/sec, and one more - 266 mm/sec, if I did not mess up the calculations. So, if we run into issues with slicers, we could adjust the thresholds to +/-0.99999999 and that would likely address them.