RasmahRasmah

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 LinearAlgebra

Forward 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))))  # ~0
head 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.05
pivot: [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 vertical
6.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 apply
true

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, and hybrid_tool_axis/hybrid_inverse map tool axes to machine angles and back.
  • RTCP: rtcp_toolpath(; configuration=:head/:table/:hybrid) converts tip positions to machine pivot/position coordinates, and gcode_5axis emits them.
  • Dynamics: machine_feed_limits caps the tip feed per axis using the exact Jacobian (ω = |dθ/ds|f, α = |d²θ/ds²|f², j = 3|d²θ/ds²|f·a_tip), and s_curve_lookahead_feeds ramps between segments under the jerk limit.

Next steps