5-Axis Machine Kinematics and Feed Dynamics
Overview
A 3-axis machine moves its tool in X, Y, and Z; a 5-axis machine can also tilt the tool or the workpiece, so a part with deep undercuts or tilted walls can be machined in one setup. But those extra axes change the geometry of the problem: the tool tip is no longer where the machine’s linear axes are, and the feed the programmer wants at the tip is not the speed each rotary axis can physically sustain.
This tutorial covers the two halves of that problem. Kinematics maps between tool-tip positions/orientations and machine axis positions — forward and inverse, for the three common 5-axis layouts (a tilting head, a tilting table, and a hybrid head + table). Feed dynamics then caps the programmed feed so that no axis — linear or rotary — exceeds its velocity, acceleration, or jerk limit, using the exact inverse-kinematics Jacobian rather than a rough estimate.
Everything here is closed-form and checkable: a forward/inverse pair must round-trip to machine precision, a tool tip lifted by a tool length must come back down by the same amount, and the feed cap at a rotary segment equals ω_max · seg / Δθ.
using Rasmah
using LinearAlgebraForward and inverse kinematics
Each layout defines a tool axis — the unit direction from tip to spindle — as a function of its rotary angles, and an inverse that recovers the angles from a desired axis. A head tilts about X then Y (A, B), a table tilts about X then Z (A, C), and a hybrid tilts the head about Y and rotates the table about Z (B, C):
println(head_tool_axis(0.3, 0.5)) # sin/cos of (A, B)
println(table_tool_axis(0.4, 0.9)) # sin/cos of (A, C)
println(hybrid_tool_axis(0.4, 0.6)) # sin/cos of (B, C)[0.479425538604203, -0.2593433800522308, 0.8383866435942036]
[0.3050418666328927, -0.24206632340649498, 0.9210609940028851]
[0.3214008270064177, -0.219882135986551, 0.9210609940028851]
Each inverse recovers the angles from the axis, so the round trip is exact:
A, B = head_inverse(head_tool_axis(0.3, 0.5))
println("head round-trip: ", maximum(abs.([A, B] .- [0.3, 0.5]))) # ~0
println("table round-trip: ", maximum(abs.(table_inverse(table_tool_axis(0.4, 0.9)) .- (0.4, 0.9)))) # ~0head round-trip: 0.0
table round-trip: 5.551115123125783e-17
The tool tip vs the machine pivot
On a head machine the linear axes move the pivot (the head’s rotation centre), and the tool tip hangs a tool_length below it along the tool axis. head_tool_tip gives the tip from the pivot, and head_tool_pivot inverts it:
pivot = [0.0, 0.0, 0.1] # 100 mm above the part
tip = head_tool_tip(0.3, 0.5, pivot, 0.05)
println("tip: ", round.(tip; digits=4))
println("pivot: ", round.(head_tool_pivot(0.3, 0.5, tip, 0.05); digits=4)) # back to [0,0,0.1]tip: [-0.024, 0.013, 0.0581]
pivot: [0.0, 0.0, 0.1]
RTCP (rotation tool-centre-point) keeps the tip fixed at the programmed point while the head tilts. rtcp_toolpath transforms a whole toolpath of tip positions into pivot positions — the pivot sits tool_length along the axis above each tip:
tp = ToolPath(EndMill(0.004))
push_rapid!(tp, [0.0, 0.0, 0.01]); push_cut!(tp, [0.005, 0.0, -0.002])
rt = rtcp_toolpath(tp, [[0.0, 0.0, 1.0], [0.0, 0.0, 1.0]], 0.05)
println("pivot: ", rt.motions[1].target) # [0, 0, 0.06] = 0.01 + 0.05pivot: [0.0, 0.0, 0.060000000000000005]
A vertical tool lifts the pivot by exactly the tool length — a closed-form check.
Table and hybrid layouts
A table machine rotates the workpiece, so the machine position is the workpiece point rotated into the machine frame. table_rotation is the workpiece-to-machine rotation, and rtcp_toolpath(; configuration=:table) applies it to every tip:
ax = table_tool_axis(0.5, 0.3)
println(norm(table_rotation(0.5, 0.3) * ax - [0.0, 0.0, 1.0])) # ~0: axis becomes vertical6.206335383118183e-17
A hybrid (head B + table C) combines both: the tip is the rotated workpiece point plus the tool length along the tilted head axis. Each configuration is selected by name:
tp2 = ToolPath(EndMill(0.004)); push_cut!(tp2, [0.01, 0.01, 0.0])
hax = hybrid_tool_axis(0.4, 0.6)
rt_h = rtcp_toolpath(tp2, [hax], 0.05; configuration=:hybrid)
B, C = hybrid_inverse(hax)
R = rotation_z(C)
println(round.(transpose(R) * (rt_h.motions[1].target .- 0.05 .* [sin(B), 0.0, cos(B)]); digits=6))
# [0.01, 0.01, 0.0] — the workpiece tip is recovered exactly[0.01, 0.01, 0.0]
Feed limits from the Jacobian
A machine can only turn its rotary axes so fast. MachineLimits records the velocity/acceleration/jerk envelope of the linear and rotary axes, and machine_feed_limits caps the tip feed so no axis exceeds it:
ml = MachineLimits(; linear_vmax=0.05, linear_amax=1.0, rotary_vmax=0.5, rotary_amax=5.0)
axes = [[0.0, 0.0, 1.0], [0.0, 0.0, 1.0], [0.2, 0.3, sqrt(1 - 0.2^2 - 0.3^2)]]
tp3 = ToolPath(EndMill(0.004))
push_rapid!(tp3, [0.0, 0.0, 0.0]); push_cut!(tp3, [0.01, 0.0, 0.0]); push_cut!(tp3, [0.01, 0.01, 0.0])
lims = machine_feed_limits(tp3, axes, ml; tip_feed=0.03)
println("feeds: ", round.(lims; digits=5))feeds: [0.03, 0.03, 0.01607]
The first two motions are vertical (no rotary travel), so they keep the full tip feed. The third sweeps the tool through Δθ while the tip travels seg, so its feed is capped at ω_max · seg / Δθ:
A3, B3 = head_inverse(axes[3])
seg = norm([0.01, 0.01, 0.0] .- [0.01, 0.0, 0.0])
println(lims[3], " ≈ ", ml.rotary_vmax * seg / max(abs(A3), abs(B3)))0.01606765153425011 ≈ 0.016067651534250108
The acceleration and jerk caps use the exact Jacobian derivatives dθ/ds and d²θ/ds² — a segment whose tool axis slews at a constant rate has zero geometric acceleration, so it is not spuriously feed-capped:
tilt = [0.2, 0.0, sqrt(1 - 0.2^2)]
tp4 = ToolPath(EndMill(0.004)); push_rapid!(tp4, [0.0, 0.0, 0.0])
for k in 1:5; push_cut!(tp4, [0.01 * k, 0.0, 0.0]); end
ax4 = [tilt for _ in 1:6]
lims4 = machine_feed_limits(tp4, ax4, ml; tip_feed=0.03)
println(lims4[3] == 0.03) # uniform slew: only velocity caps applytrue
Jerk-limited lookahead
Finally, the feed must ramp between segments without exceeding the machine’s acceleration and jerk. s_curve_lookahead_feeds runs a forward/backward S-curve (jerk-limited) ramp over the per-motion feed bounds:
bounds = fill(0.05, num_motions(tp3))
v = s_curve_lookahead_feeds(tp3, bounds; vmax=0.05, amax=1.0, jmax=10.0)
println("first feed: ", v[1], ", last feed: ", v[end])
println("path time: ", round(s_curve_path_time(tp3, v; amax=1.0, jmax=10.0); digits=4), " s")first feed: 0.05, last feed: 0.05
path time: 0.4 s
A real part: RTCP finishing of a die cavity
The machinery above comes together on a real job. Take the 40 × 40 × 20 mm die block with its Ø30 mm spherical cavity from the gouge-avoidance tutorial and finish it with a Ø8 mm ball-nose on a tilting-head machine. The gouge-aware planner returns the tip toolpath and one tool axis per motion (the 133 infeasible raster points drop out, leaving 750 motions with orientations):
part = difference(box(0.040, 0.040, 0.020), translate(sphere(0.015), 0.0, 0.0, 0.012))
m = surface_mesh(part; n = 48)
tp, axes = five_axis_finish_gouge_free(m, BallEndMill(0.008); stepover = 0.004, grid = 5)
ax = [a for a in axes if a !== nothing] # one feasible axis per motion
println("motions ", num_motions(tp), ", feasible axes ", length(ax))motions 750, feasible axes 750
RTCP transforms those tip motions into pivot positions — the coordinates the linear axes of a head machine actually visit — hanging the tip a 50 mm gauge length below the pivot along each tool axis:
rt = rtcp_toolpath(tp, ax, 0.050; configuration = :head)
println("pivot moves: ", num_motions(rt), ", first pivot: ",
round.(rt.motions[1].target .* 1000; digits = 1), " mm")pivot moves: 750, first pivot: [-19.0, -70.0, 9.1] mm
The machine’s axes then cap the feed. A Haas-UMC-class envelope — 50 m/min linear, 500°/s rotary — looks generous until the steep cavity wall forces the B axis to slew: the rotary velocity limit caps the tip feed at 0.4 mm/s on the worst segment, and the jerk-limited S-curve turns the whole program into a real cycle time:
ml = MachineLimits(; linear_vmax = 0.83, linear_amax = 5.0, linear_jmax = 50.0,
rotary_vmax = 8.7, rotary_amax = 60.0, rotary_jmax = 600.0)
lims = machine_feed_limits(rt, ax, ml; tip_feed = 0.05)
println("min tip feed = ", round(minimum(lims) * 1000; digits = 1),
" mm/s (programmed ", 0.05 * 1000, " mm/s)")
v = s_curve_lookahead_feeds(rt, lims; vmax = 0.05, amax = 5.0, jmax = 50.0)
println("cavity finish time = ", round(s_curve_path_time(rt, v; amax = 5.0, jmax = 50.0); digits = 1), " s")min tip feed = 0.4 mm/s (programmed 50.0 mm/s)
cavity finish time = 181.6 s
The same check an applications engineer runs before quoting the job: the toolpath is 750 motions, the rotary axes — not the linear ones — set the feed, and the cavity finishes in about three minutes.
Summary
- Kinematics:
head_tool_axis/head_inverse,table_tool_axis/table_inverse, andhybrid_tool_axis/hybrid_inversemap tool axes to machine angles and back. - RTCP:
rtcp_toolpath(; configuration=:head/:table/:hybrid)converts tip positions to machine pivot/position coordinates, andgcode_5axisemits them. - Dynamics:
machine_feed_limitscaps the tip feed per axis using the exact Jacobian (ω = |dθ/ds|f,α = |d²θ/ds²|f²,j = 3|d²θ/ds²|f·a_tip), ands_curve_lookahead_feedsramps between segments under the jerk limit.
Next steps
- 5-Axis Tool Orientation and Gouge Avoidance — choosing the orientation to avoid gouging the surface.
- CNC Toolpaths and 3D-Print Slicing — the strategies that produce the
ToolPaths. - Feature-Based Machining and Tool Selection — recognizing features and picking tools automatically.
