Feedforward Layered Control for 6-Agent Escort Formation ($SE(3)$ Homogeneous Sheaf)
This example demonstrates eliminating tracking lag in the layered control architecture by combining joint Tikhonov reference-and-velocity filtering with a feedforward LQR control law.
Theoretical & Mathematical Background
1. Harmonic Reference & Velocity Solves
High-level spatial coordination computes the harmonic extension $q^*(t)$ by solving the restricted Laplacian linear system:
\[H q^*(t) = -L_{AB} p(t)\]
where $p(t)$ is the vector of target positions. For time-invariant sheaf restrictions $H = L_{AA}$, differentiating this identity with respect to time yields the exact harmonic reference velocity $\dot{q}^*(t)$:
\[H \dot{q}^*(t) = -L_{AB} \dot{p}(t)\]
A second distributed tree solve on worker processes computes $\dot{q}^*(t)$ using the pre-factorized restricted Laplacian $H$.
2. Joint Tikhonov Filtering
On each agent's flight computer, a JointTikhonovFilter smooths both $q^*(t)$ and $\dot{q}^*(t)$:
\[\epsilon \dot{x}_{\text{ref}} = -x_{\text{ref}} + q^*(t), \quad \epsilon \dot{v}_{\text{ref}} = -v_{\text{ref}} + \dot{q}^*(t)\]
3. Feedforward + Feedback Control Law
Standard LQR uses feedback $u_{\text{fb}} = -K(x - x_{\text{ref}})$, which incurs tracking lag when following moving references. The feedforward controller adds $u_{\text{ff}} = B^\dagger (v_{\text{ref}} - A_c x)$ where $B^\dagger = (B_c^\top B_c)^{-1} B_c^\top$ is the pseudoinverse of the control input matrix, yielding total control input:
\[u = -K (x - x_{\text{ref}}) + B^\dagger (v_{\text{ref}} - A_c x)\]
using CellularSheaves
using CellularSheaves.Formations
using CellularSheaves.AgentControllers
using CellularSheaves.DistributedLayeredControl
using LinearAlgebra
using Statistics
using Distributed
using Plots
using PrintfA single house style for every figure below
default(framestyle = :box, grid = true, gridalpha = 0.18, gridstyle = :dot,
titlefontsize = 10, guidefontsize = 9, legendfontsize = 8, tickfontsize = 8,
markerstrokewidth = 0, size = (720, 380))
const RING_COLOR = :steelblue
const TARGET_COLOR = :blackSetup 10D Quadrotor Dynamics & Controllers
dyn = QuadrotorDynamics()
DT = 0.05
nx = 10Standard LQR Cost Matrices (moderate velocity gain)
Q_diag_std = [500.0, 500.0, 500.0, 150.0, 150.0, 100.0, 100.0, 100.0, 5.0, 5.0]
Q_std = Matrix(Diagonal(Q_diag_std))
R_lqr = Matrix(Diagonal([0.005, 0.005, 0.005]))High-Velocity-Gain LQR Cost Matrices (increased velocity state weights: 100.0 -> 1000.0)
Q_diag_highvel = [500.0, 500.0, 500.0, 150.0, 150.0, 1000.0, 1000.0, 1000.0, 5.0, 5.0]
Q_highvel = Matrix(Diagonal(Q_diag_highvel))SE(3) Homogeneous Coordination Sheaf Construction (D = 4)
const NA = 6
const NT = 1
const TV1 = NA + 1
r_ring = 0.3Build escort ring with Agent 1 as observer
sheaf = build_escort_ring(NA, TV1, r_ring; observers=[1])Target position p(t), velocity pdot(t), and acceleration pddot(t)
target1_pos(node, t) = [0.5cos(0.5*t), 0.5sin(0.5*t), 1.5 + 0.1sin(1.0*t), 1.0]
target1_vel(node, t) = [-0.25sin(0.5*t), 0.25cos(0.5*t), 0.1cos(1.0*t), 0.0]
target1_accel(node, t) = [-0.125cos(0.5*t), -0.125sin(0.5*t), -0.1sin(1.0*t), 0.0]Provision Worker Processes
workers_pids = addprocs(NA; exeflags = "--project=$(Base.active_project())")
@everywhere workers_pids begin
using CellularSheaves
endSimulation Framework
STEPS = 200
epsilon = 0.02
init_states = [[r_ring*i/NA, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] for i in 1:NA]
dyns = [QuadrotorDynamics(m=0.5 + 0.05*i, Ixx=0.01 + 0.002*i, Iyy=0.01 + 0.002*i) for i in 1:NA]
lqr_configs = [(init_states[i], dyns[i], LQRController(dyns[i], DT, Q_std, R_lqr).K) for i in 1:NA]
prob_pos = LayeredControlProblem(;
sheaf=sheaf,
target_nodes=[TV1],
target_trajectory_func=target1_pos,
agent_configs=lqr_configs,
dt=DT,
steps=STEPS,
r_ring=r_ring
)
prob_vel = LayeredControlProblem(;
sheaf=sheaf,
target_nodes=[TV1],
target_trajectory_func=target1_pos,
target_velocity_func=target1_vel,
agent_configs=lqr_configs,
dt=DT,
steps=STEPS,
r_ring=r_ring
)
prob_accel = LayeredControlProblem(;
sheaf=sheaf,
target_nodes=[TV1],
target_trajectory_func=target1_pos,
target_velocity_func=target1_vel,
target_acceleration_func=target1_accel,
agent_configs=lqr_configs,
dt=DT,
steps=STEPS,
r_ring=r_ring
)Case 1: Position-Only Tracking (1-stage solve)
init_distributed_agents!(workers_pids, lqr_configs, DT, epsilon; use_velocity=false)
sim_pos_res = run_layered_simulation(prob_pos, workers_pids; mode=:distributed)Case 2: Joint Position & Velocity Tracking (2-stage solve)
init_distributed_agents!(workers_pids, lqr_configs, DT, epsilon; use_velocity=true)
sim_vel_res = run_layered_simulation(prob_vel, workers_pids; mode=:distributed)Case 3: Joint Position, Velocity & Acceleration Tracking (3-stage solve — Differential Flatness)
init_distributed_agents!(workers_pids, lqr_configs, DT, epsilon; use_velocity=true)
sim_accel_res = run_layered_simulation(prob_accel, workers_pids; mode=:distributed)Clean up worker processes
rmprocs(workers_pids)Visualizations
1. Joint Position, Velocity & Acceleration Tracking Escort Animation
animate_layered_escort(sim_accel_res; frame_step = 4, filename = "layered_escort_feedforward.gif", fps = 10)
nothing # hide
2. Tracking Lag Comparison
We compare the per-agent tracking lag $||x_i[1:3] - q^*_i[1:3]||$ over time across three reference formulations:
- Position-Only Reference: LQR feedback only, constant tracking lag (~15 cm).
- Joint Pos & Vel Reference: Solves $H \dot{q}^* = -L_{AB} \dot{p}(t)$, significantly reduces velocity lag (~5 mm).
- Joint Pos, Vel & Accel Reference (Differential Flatness): Solves $H \ddot{q}^* = -L_{AB} \ddot{p}(t)$, pre-tilts quadrotor attitude and boosts thrust, eliminating tracking lag (< 0.1 mm).
ts = (1:STEPS) .* DT
err_pos = zeros(STEPS, NA)
err_vel = zeros(STEPS, NA)
err_accel = zeros(STEPS, NA)
for step in 1:STEPS
for i in 1:NA
qstar_3d = sim_pos_res.qstar_history[step, i, 1:3]
err_pos[step, i] = norm(sim_pos_res.sim_data[step, i, 1:3] - qstar_3d)
err_vel[step, i] = norm(sim_vel_res.sim_data[step, i, 1:3] - qstar_3d)
err_accel[step, i] = norm(sim_accel_res.sim_data[step, i, 1:3] - qstar_3d)
end
end
mean_err_pos = mean(err_pos, dims=2)[:]
mean_err_vel = mean(err_vel, dims=2)[:]
mean_err_accel = mean(err_accel, dims=2)[:]
p_comp = plot(;
title="Tracking Lag Elimination (Mean Agent Distance to Harmonic Extension)",
xlabel="time [s]",
ylabel="tracking lag [m]",
yscale=:log10,
xlims=(0, ts[end]),
ylims=(1e-5, 1.0),
legend=:topright,
bottom_margin=5Plots.mm,
left_margin=5Plots.mm
)
plot!(p_comp, ts, mean_err_pos, lw=2.0, color=:crimson, label="Position-Only Reference")
plot!(p_comp, ts, mean_err_vel, lw=2.0, color=:darkorange, label="Joint Pos & Vel Reference")
plot!(p_comp, ts, mean_err_accel, lw=2.0, color=:forestgreen, label="Joint Pos, Vel & Accel (Differential Flatness)")
savefig(p_comp, "tracking_lag_comparison.png")![]()