@@ -143,6 +143,47 @@ function fix_trajectory_variable!(
143143 return constraints
144144end
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+
146187function Base. show (io:: IO , c:: EqualityConstraint )
147188 print (io, " EqualityConstraint: \" $(c. label) \" " )
148189end
300341 @test prob. trajectory[t][:u ] ≈ u_ref[:, t] atol= 1e-10
301342 end
302343end
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