Closed-loop MPC for multi-agent tracking sheaf problems#65
Conversation
61d075f to
8990b4d
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #65 +/- ##
==========================================
+ Coverage 73.60% 75.20% +1.59%
==========================================
Files 36 36
Lines 2550 2722 +172
==========================================
+ Hits 1877 2047 +170
- Misses 673 675 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
jpfairbanks
left a comment
There was a problem hiding this comment.
@copilot, there are some mechanical changes that you can help out with. Some of these comments need to be handled by @Optimax14 because they are conceptual clarification questions. If you want to try and answer these questions, I would be interested in your thoughts, even though they are not requests to modify the code directly.
| (1, result1_ol, "Open-loop"), | ||
| (2, result1_mpc, "MPC (W=k)")) | ||
| sp = p[panel] | ||
| plot!(sp; title="$title_str — t = $(round(times[t_idx]; digits=2))", |
There was a problem hiding this comment.
@copilot these titles need to be rounded to exactly 3 digits with @sprintf rather than numeric rounding. This should be done to all plot titles that use string interpolation of a number.
There was a problem hiding this comment.
Fixed in the latest commit. Replaced round(times[t_idx]; digits=2) with @sprintf("%.3f", times[t_idx]) on line 290 and round(sr.residual; sigdigits=3) with @sprintf("%.3e", sr.residual) in the ScenarioResult recipe's plot_title on line 182. Printf was already imported.
| window=k, cost=1.0, y_col=IDX_Y, z_col=IDX_Z) | ||
|
|
||
| show(stdout, MIME("text/plain"), result2_w2); println() | ||
| show(stdout, MIME("text/plain"), result2_wk); println() |
There was a problem hiding this comment.
This shows a higher norm(dz) residual for the W=k case. Is that consistent with the text around this result?
ScenarioResult:
label : MPC W=2
null dim : 2
||dz|| : 2.471e-5
agents : 2
targets : 2
ScenarioResult:
label : MPC W=k
null dim : 8
||dz|| : 0.003643
agents : 2
targets : 2
| horizon(K) | ||
| times(Tall = 0:K) | ||
| track(tr1; agent=a1, target=t1, maps=(R_yz,R_yz), at=Tall) | ||
| track(tr2; agent=a2, target=t2, maps=(R_yz,R_yz), at=Tall) |
There was a problem hiding this comment.
There is no consensus term on this @tracking_problem, so the agents should just track independently their targets. I think that the cleanest comparison to make your point about window size is
consensus(c1; agents=(a1,a2), maps=(R_y,R_y), at=Tall)
track(tr1; agent=a1, target=t1, maps=(R_yz,R_yz), at=Tall)
track(tr2; agent=a2, target=t2, maps=(R_yz,R_yz), at=Tall)But we should do an example like
consensus(c1; agents=(a1,a2), maps=(R_y,R_y), at=t[end])
track(tr1; agent=a1, target=t1, maps=(R_yz,R_yz), at=Tall)
track(tr2; agent=a2, target=t2, maps=(R_yz,R_yz), at=Tall)which will get the agents to try and get to tracking immediately and only arrive at consensus at the end of the horizon. Are also interesting.
| # minimum-energy compromise in which the dynamics edges carry small but nonzero | ||
| # residual. The open-loop extractor reads the sheaf directly while MPC enforces | ||
| # the true dynamics at every step. The trajectories are close but not | ||
| # identical — both are valid answers to slightly different questions. |
There was a problem hiding this comment.
The nonzero residual in this model isn't caused by the infeasibility of the target trajectories. It is caused by the inherent contradiction of trying to track the targets and do consensus at the same time.
consensus(c1; agents=(a1,a2), maps=(R_y,R_y), at=Tall)
track(tr1; agent=a1, target=t1, maps=(R_yz,R_yz), at=Tall)
track(tr2; agent=a2, target=t2, maps=(R_yz,R_yz), at=Tall)This says that you should try and be at consensus in y and tracking in yz every time step. Since the targets aren't at the same y coordinates as each other, this is an infeasible goal.
| annotate!(p_speed[2], i - 0.5, s + maximum(speedups) * 0.05, | ||
| text(@sprintf("%.1f x", s), 9, :center, :steelblue)) | ||
| end | ||
| p_speed |
There was a problem hiding this comment.
This should include a tradoff of control performance. Once we run the simulation of the MPC controller, we have a trajectory of z_t = (x_t,u_t) pairs. We can score that trajectory by computing |dz| for that trajectory. So we can numerically compare the loss due to the changing window size.
The mechanical |
| if size(N, 2) > 0 && !(cost isa Number && iszero(cost)) | ||
| Q_II = Matrix(build_control_cost_matrix(window_prob, stalks, cost)[interior, interior]) | ||
| NtQ_II = N' * Q_II | ||
| Fq = ldlt!(DenseLDLtPivoted(NtQ_II * N), RowMaximum(); check=false) | ||
| coeff = ldlt_pinv_solve(Fq, NtQ_II * H) | ||
| M = H - N * coeff |
| """ | ||
| build_control_cost_matrix(prob::TrackingProblem, λ::Number) -> SparseMatrixCSC{Float64,Int} | ||
| build_control_cost_matrix(prob::TrackingProblem, stalks, λ::Number) -> SparseMatrixCSC{Float64,Int} | ||
|
|
||
| Construct a global quadratic cost matrix `Q` that penalises the control | ||
| components of each agent with a uniform weight `λ`. Internally this builds a | ||
| block‑diagonal matrix where each control block is `λ * I(nu)`. | ||
| """ | ||
| function build_control_cost_matrix(prob::TrackingProblem, λ::Number=1.0) | ||
| # Create a simple cost function that returns λ * I(nu) for each agent/time | ||
| cost_func = (agent_idx::Int, t_step::Int) -> begin | ||
| nu = size(prob.Bd[agent_idx], 2) | ||
| return λ * I(nu) | ||
| end | ||
| return build_control_cost_matrix(prob, cost_func) | ||
| function build_control_cost_matrix(prob::TrackingProblem, stalks::AbstractVector{<:Integer}, λ::Number=1.0) | ||
| return build_control_cost_matrix(prob, stalks, | ||
| (agent_idx::Int, t_step::Int) -> λ * I(size(prob.Bd[agent_idx], 2))) | ||
| end |
| function build_control_cost_matrix(prob::TrackingProblem, stalks::AbstractVector{<:Integer}, cost_func::Function) | ||
| offsets = [0; cumsum(stalks)] | ||
| Q = spzeros(Float64, last(offsets), last(offsets)) | ||
| for i in 1:prob.n_agents | ||
| nx = size(prob.Ad[i], 1); nu = size(prob.Bd[i], 2) | ||
| for t_step in 0:prob.k | ||
| idxs = control_range(i, t_step) | ||
| v = agent_vertex(prob, i, t_step) | ||
| stalks[v] == nx + nu || continue # state-only vertex: no control to penalise | ||
| rng = (offsets[v] + nx + 1):(offsets[v] + nx + nu) | ||
| Q_local = cost_func(i, t_step) | ||
| @assert size(Q_local) == (nu, nu) "cost_func must return a $(nu)×$(nu) matrix for agent $i at time $t_step" | ||
| Q[idxs, idxs] = Q_local | ||
| Q[rng, rng] = Q_local |
| function ldlt_pinv_solve(F, B::AbstractMatrix; tol=nothing) | ||
| nI = size(B, 1) | ||
| X = Matrix{Float64}(undef, nI, size(B, 2)) | ||
| colbuf = Vector{Float64}(undef, nI) | ||
| wbuf = Vector{Float64}(undef, nI) | ||
| outbuf = Vector{Float64}(undef, nI) | ||
| for j in axes(B, 2) | ||
| colbuf .= @view B[:, j] | ||
| ldlt_pinv_solve!(outbuf, F, colbuf, wbuf; tol=tol) | ||
| @inbounds X[:, j] = outbuf | ||
| end | ||
| return X | ||
| end |
1958934 to
d48429f
Compare
This PR implements a receding-horizon MPC algorithm for multi-agent tracking sheaf problems.
Summary
window_problemto take the entire horizon and slice it up into sub problems of sizek.run_mpc_scenariowith:cached(reuses the factorization + orthogonal projection) &:naive(recomputes the open loop problem for each MPC step)ldlt_pinv_solvehelpers toEuclideanSheavesand switchedsolve_quadratic_on_basisto usesvd\Tests
test/network_sheaves/MultiAgentTrackingMPC.jl.Docs
docs/literature/control/mpc_target_tracking.jlshowcasing the results of this PR and the application of the algorithm in different scenarios.Closes #62