Skip to content

Commit c876f23

Browse files
feat: add fix_global_variable! helper (#78)
Companion to fix_trajectory_variable! for the global-variable case. Pins a global variable via GlobalEqualityConstraint with dedup logic that handles globals (the existing fix_trajectory_variable! filter only dedupes non-global constraints). This enables Intonato's QILC alternating calibration to bake learned parameters back into the control NLP integrator-agnostically — any globals-aware integrator (HermitianExponentialIntegrator, SplineIntegrator, NonHermitianExponentialIntegrator) reads the pinned value through the NLP variable instead of requiring an integrator rebuild on every iteration. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 69f9d64 commit c876f23

1 file changed

Lines changed: 92 additions & 0 deletions

File tree

src/constraints/linear/equality_constraint.jl

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,47 @@ function fix_trajectory_variable!(
143143
return constraints
144144
end
145145

146+
export fix_global_variable!
147+
148+
"""
149+
fix_global_variable!(constraints, name, value)
150+
151+
Pin a global (time-invariant) variable to `value` using a `GlobalEqualityConstraint`.
152+
Removes any existing `BoundsConstraint` or `EqualityConstraint` on the same global
153+
variable to avoid MOI conflicts. Companion to [`fix_trajectory_variable!`](@ref) for
154+
the global-variable case.
155+
156+
This is the integrator-agnostic mechanism for pinning a calibrated parameter
157+
(e.g. learned `θ` in QILC alternating calibration) into the control NLP — any
158+
globals-aware integrator (`HermitianExponentialIntegrator`, `SplineIntegrator`,
159+
`NonHermitianExponentialIntegrator`, ...) will read the pinned value through the
160+
NLP variable rather than requiring an integrator rebuild.
161+
162+
# Arguments
163+
- `constraints::Vector{<:AbstractConstraint}`: mutable constraint list
164+
- `name::Symbol`: global variable name to pin
165+
- `value::AbstractVector{Float64}`: pin value (length must equal the global variable dim)
166+
"""
167+
function fix_global_variable!(
168+
constraints::Vector{<:AbstractConstraint},
169+
name::Symbol,
170+
value::AbstractVector{Float64},
171+
)
172+
# Remove any existing BoundsConstraint and EqualityConstraint on this global variable
173+
# (per-timestep dedup at line ~134 only filters non-global constraints; this is the
174+
# globals counterpart). Pinning supersedes any prior bounds or pin on the same global.
175+
filter!(constraints) do c
176+
if c isa BoundsConstraint && c.var_names == name && c.is_global
177+
return false
178+
elseif c isa EqualityConstraint && c.var_names == name && c.is_global
179+
return false
180+
end
181+
return true
182+
end
183+
push!(constraints, GlobalEqualityConstraint(name, collect(Float64, value)))
184+
return constraints
185+
end
186+
146187
function Base.show(io::IO, c::EqualityConstraint)
147188
print(io, "EqualityConstraint: \"$(c.label)\"")
148189
end
@@ -300,3 +341,54 @@ end
300341
@test prob.trajectory[t][:u] u_ref[:, t] atol=1e-10
301342
end
302343
end
344+
345+
@testitem "fix_global_variable! pins global to supplied value" begin
346+
include("../../../test/test_utils.jl")
347+
348+
G, traj = bilinear_dynamics_and_trajectory(add_global = true)
349+
350+
integrators = [
351+
BilinearIntegrator(G, :x, :u, traj),
352+
DerivativeIntegrator(:u, :du, traj),
353+
DerivativeIntegrator(:du, :ddu, traj),
354+
]
355+
356+
J = TerminalObjective(x -> norm(x - traj.goal.x)^2, :x, traj)
357+
J += QuadraticRegularizer(:u, traj, 1.0)
358+
J += MinimumTimeObjective(traj)
359+
360+
# Build a normal problem first (so bounds/equality on globals may exist)
361+
prob_orig = DirectTrajOptProblem(traj, J, integrators)
362+
363+
# Pin global :g to a specific value via fix_global_variable!
364+
g_dim = length(traj.global_components[:g])
365+
g_value = fill(0.42, g_dim)
366+
constraints = deepcopy(prob_orig.constraints)
367+
fix_global_variable!(constraints, :g, g_value)
368+
369+
# After fixing, no remaining BoundsConstraint or EqualityConstraint on :g
370+
# other than our new GlobalEqualityConstraint
371+
g_eq_cons = filter(
372+
c -> c isa EqualityConstraint && c.var_names == :g && c.is_global,
373+
constraints,
374+
)
375+
@test length(g_eq_cons) == 1
376+
@test !any(c -> c isa BoundsConstraint && c.var_names == :g && c.is_global, constraints)
377+
378+
# Re-applying should still leave exactly one equality constraint (dedup works)
379+
fix_global_variable!(constraints, :g, fill(0.5, g_dim))
380+
g_eq_cons2 = filter(
381+
c -> c isa EqualityConstraint && c.var_names == :g && c.is_global,
382+
constraints,
383+
)
384+
@test length(g_eq_cons2) == 1
385+
@test g_eq_cons2[1].values fill(0.5, g_dim)
386+
387+
# Solve and confirm the global is pinned at the supplied value
388+
fix_global_variable!(constraints, :g, g_value) # pin at 0.42 again
389+
prob = DirectTrajOptProblem(traj, J, integrators, constraints)
390+
solve!(prob; max_iter = 100)
391+
392+
g_components = traj.global_components[:g]
393+
@test prob.trajectory.global_data[g_components] g_value atol=1e-6
394+
end

0 commit comments

Comments
 (0)