Preface: the post got actually too large. So, Iād recommend reading the principles of extruder input shaping and then skip the justification part and take a look at the experiment results because there are some interesting findings there, in case you want to save some time.
From the implementation point of view, the exruder input shaping is done as follows:
- extruder motion is split into 3 āfakeā axis
E_x
, E_y
, E_z
in certain proportion for each move;
- the configured input shapers are applied to
E_x
and E_y
motion;
- the pressure advance calculation (with smoothing) is applied on top of that (in fact, integrals and sums can be swapped in this case, so it is actually implemented in reverse:
IS_x(PA(E_x))
, IS_y(PA(E_y))
, E_z
(no PA or IS for z, retractions/deretractions are also implemented as a motion over this āfakeā z axis);
- the final extruder position E is a sum of per-axis motion, i.e.
E = IS_x(PA(E_x)) + IS_y(PA(E_y)) + E_z
.
If you are curious, hereās some justification on why this might even make sense at all, and how the moves are split into E_x
, E_y
, E_z
.
An infinitesimal extruder motion dE
can be represented as
dE = r_e * sqrt(dx^2 + dy^2 + dz^2) = r_e * dt * sqrt(v_x(t)^2 + v_y(t)^2 + v_z(t)^2)
This is a precise representation, sadly, it would require integration to go directly with it to input shaping (and symbolic integration is not likely to succeed, so a numeric one would be required). Instead, weāll use some tricks. Letās define
v_x(t) = V(t) * r_x(t) + Dv_x(t)
v_y(t) = V(t) * r_y(t) + Dv_y(t)
v_z(t) = V(t) * r_z(t) + Dv_z(t)
r_x(t) = V_x(t) / V(t)
r_y(t) = V_y(t) / V(t)
r_z(t) = V_z(t) / V(t)
Basically, we represent v_x(t)
, v_y(t)
and v_z(t)
as some āaverageā motion \vec V(t)
plus some small (compared to V(t)
) corrections Dv_x(t)
, Dv_y(t)
, Dv_z(t)
. Additionally, r_x(t)^2 + r_y(t)^2 + r_z(t)^2 = 1
. Then,
dE = r_e * dt * sqrt((V(t) * r_x(t) + Dv_x(t))^2 +
(V(t) * r_y(t) + Dv_y(t))^2 +
(V(t) * r_z(t) + Dv_z(t))^2) =
= r_e * V(t) * dt * sqrt(r_x(t)^2 * (1 + Dv_x(t) / (V(t) * r_x(t)))^2 +
r_y(t)^2 * (1 + Dv_y(t) / (V(t) * r_y(t)))^2 +
r_z(t)^2 * (1 + Dv_z(t) / (V(t) * r_z(t)))^2)
Now we can use (1 + x)^a ~= 1 + a * x
when x
is small:
dE ~= r_e * V(t) * dt * sqrt(r_x(t)^2 * (1 + 2 * Dv_x(t) / (V(t) * r_x(t))) +
r_y(t)^2 * (1 + 2 * Dv_y(t) / (V(t) * r_y(t))) +
r_z(t)^2 * (1 + 2 * Dv_z(t) / (V(t) * r_z(t)))) =
= r_e * V(t) * dt * sqrt(r_x(t)^2 + r_y(t)^2 + r_z(t)^2 +
2 * (Dv_x(t) * r_x(t) + Dv_y(t) * r_y(t) +
Dv_z(t) * r_z(t)) / V(t))
We can use the property of sum of squares of r_?
s and then that approximate equality again to get
dE ~= r_e * V(t) * dt * sqrt(1 + 2 * (Dv_x(t) * r_x(t) + Dv_y(t) * r_y(t) +
Dv_z(t) * r_z(t)) / V(t)) ~=
~= r_e * V(t) * dt * (1 + (Dv_x(t) * r_x(t) + Dv_y(t) * r_y(t) +
Dv_z(t) * r_z(t)) / V(t)) =
= r_e * dt * (V(t) + Dv_x(t) * r_x(t) + Dv_y(t) * r_y(t) + Dv_z(t) * r_z(t))
Now we can substitute Dv_x(t) = v_x(t) - V(t) * r_x(t)
, Dv_y(t)=...
and Dv_z(t)=...
from above to get
dE ~= r_e * dt * (V(t) + (v_x(t) - V(t) * r_x(t)) * r_x(t) +
(v_y(t) - V(t) * r_y(t)) * r_y(t) +
(v_z(t) - V(t) * r_z(t)) * r_z(t)) =
= r_e * dt * (V(t) * (1 - r_x(t)^2 - r_y(t)^2 - r_z(t)^2) +
v_x(t) * r_x(t) + v_y(t) * r_y(t) + v_z(t) * r_z(t)) =
= r_e * dt * (v_x(t) * r_x(t) + v_y(t) * r_y(t) + v_z(t) * r_z(t))
So, we eliminated V(t)
, and dE(t)/dt
is (approximately) proportional to the original toolhead motion v_x(t)
, v_y(t)
and v_z(t)
with some some direction coefficients representing some āaveragedā motion that must be r_x(t)^2 + r_y(t)^2 + r_z(t)^2 = 1
. FWIW, depending on the choice of that direction vector, a proper computation of E(t)
may still require integration.
Instead, I went about it this way (though it is a lot more hand-wavy at this point). With input shaping, velocities are computed as follows:
v_x(t) = sum(A_{x,m} * v_m(t - t_{x,m}) * r_{x,m})
v_y(t) = sum(A_{y,m} * v_m(t - t_{y,m}) * r_{y,m})
v_z(t) = v(t) * r_z
with A_{x,m}
, t_{x,m}
, A_{y,m}
and t_{y,m}
being corresponding input shaper constants, summation running over the input shaper pulses potentially spanning multiple moves, and v_m(t)
and r_{x,m}
and r_{y,m}
being velocity and x,y-axis direction of a corresponding move m
. Now, here be dragons:
dE(t) / dt = sum(A_{x,m} * r_{e,m} * v_m(t - t_{x,m}) * r_{x,m}^2) +
sum(A_{y,m} * r_{e,m} * v_m(t - t_{y,m}) * r_{y,m}^2) +
r_e * v(t) * r_z^2
So, it is sorta r_z(t) = r_z
, r_x(t) ~ r_{x, m}
, r_y(t) ~ r{y, m}
, but not really. Still, arguably, this formula may correspond to a certain choice of r_x(t)
and r_y(t)
. And, conveniently, it does not require any complicated integration. Then, for a move m
E_{x,m}(t) = E_{x,m,offst} + r_{e,m} * (v * t + a * t^2 / 2) * r_{x, m}^2
E_{y,m}(t) = E_{y,m,offst} + r_{e,m} * (v * t + a * t^2 / 2) * r_{y, m}^2
E_{z,m}(t) = E_{z,m,offst} + r_{e,m} * (v * t + a * t^2 / 2) * r_{z, m}^2
E_{a,m+1,offst} = E_{a,m}(t_{end,m})
with E_{a,m,offst}
being an offset of a beginning of a move on an axis a
. Afterwards, the procedure to apply input shaping and pressure advance is as described in the beginning.
In principle, there could be other choices of r_x(t)
, r_y(t)
and r_z(t)
than this. Maybe Iāll explore other options as well. But this one is simple enough and, in my opinion, give acceptable results.
FWIW, while I was preparing this explanation, I realized that I made a small mistake in my initial implementation. Namely, I instead used different coefficients
E_{x,m}(t) = E_{x,m,offst} + r_{e,m} * (v * t + a * t^2 / 2) * |r_{x, m}| / R_m
E_{y,m}(t) = E_{y,m,offst} + r_{e,m} * (v * t + a * t^2 / 2) * |r_{y, m}| / R_m
E_{z,m}(t) = E_{z,m,offst} + r_{e,m} * (v * t + a * t^2 / 2) * |r_{z, m}| / R_m
R_m = |r_{x, m}| + |r_{y, m}| + |r_{z, m}|
I couldnāt trace back where exactly I got this in the first place (perhaps I made a mistake or took some different approximation ), so, I pushed a fix and ran some tests to compare the old and the new behaviors.
Left to right are current Klipper PA code (with PA=0.04 and smooth_time=0.02), original extruder input shaping code (with PA=0.04 and smooth_time=0.01) and the fixed code (the same PA=0.04 and smooth_time=0.01). Input shaping in all cases is
3hump_ei
at 70 Hz for X axis and
2hump_ei
at 52.6 Hz on Y axis. The extruder shaping differs from mainline substantially, but the fix itself is pretty cosmetic.
Another example of PA behavior from the same test run (just a different GCode location):
This one is pretty interesting actually. Again, the extruder shaping differs from mainline substantially, but the fix itself is pretty cosmetic, though this time there are a bit more noticeable differences.
Specifically, thereās this jitter with both extruder input shaping versions at the range of supposedly constant extruder velocity. So, I tried to trace it down to see if itās some kind of fluke. In GCode, this corresponds to the part of 3D Benchy perimeter that faces outwards including the Benchy nose (basically that velocity dip in the middle of velocity chart is the Benchyās nose):
So, while cruising velocity stays constant, the motion on X and Y axes isnāt - the toolheat prints a smooth curve and turns all the time. I wanted to check the actual toolhead velocity from the steppers, so I wrote and additional
motan
data analyzer that can be used as
["norm2(derivative(stepq(stepper_x)),derivative(stepq(stepper_y)))"]
to compute
(v_x^2+v_y^2)^(1/2)
. Hereās the result for that part:
So, while the velocity of stepper_x
and stepper_y
is smooth and all, the resulting instantaneous toolhead velocity (after input shaping) is not constant, there are some velocity oscillations. And for better or worse, the extruder input shaping code can now pick up those oscillations and adjust the PA for the extruder accordingly. Also, comparing the two versions, extruder input shaping after the fix seems to better match the amplitude of the velocity oscillations to the PA adjustments: it is stronger where the actual amplitude of velocity oscillations is larger. Prior to fix, the amplitude of PA oscillations was more consistent across the whole curve, even though the actual toolhead velocity oscillations are much smaller on the sides of the Benchy far away from its nose. Whether the whole thing actually makes sense is another question, though in many of my performed prints I didnāt observe any degradation of the surface finish on smooth curves.