From bb27c5f4808f365fe3517e3d82c2cdc7393f4174 Mon Sep 17 00:00:00 2001 From: aaron Date: Wed, 19 Aug 2026 19:24:27 -0400 Subject: [PATCH 1/7] test(coverage): TimeDependentBilinearIntegrator zero-order hold + spline-order rejection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spline_order = 0 (ZOH) branches of the constructor, evaluate!, eval_jacobian, and eval_hessian_of_lagrangian had never been exercised — every existing test used the default linear interpolation. Adds a test_integrator run at spline_order = 0 and a rejection test for spline_order = 2 (the constructor's live validation error). --- .../time_dependent_bilinear_integrator.jl | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/integrators/time_dependent_bilinear_integrator.jl b/src/integrators/time_dependent_bilinear_integrator.jl index 56e6205c..0317daff 100644 --- a/src/integrators/time_dependent_bilinear_integrator.jl +++ b/src/integrators/time_dependent_bilinear_integrator.jl @@ -267,3 +267,32 @@ end test_integrator(B, traj, test_equality = false, atol = 1e-3) end + +@testitem "testing TimeDependentBilinearIntegrator with zero-order hold" begin + include("../../test/test_utils.jl") + + G, traj = bilinear_dynamics_and_trajectory(add_time = true) + + B = TimeDependentBilinearIntegrator((a, t) -> G(a), :x, :u, :t, traj; spline_order = 0) + + @test B.spline_order == 0 + @test B.u_dim == traj.dims[:u] + @test sprint(show, B) isa String + + test_integrator(B, traj, test_equality = false, atol = 1e-3) +end + +@testitem "TimeDependentBilinearIntegrator rejects unsupported spline orders" begin + include("../../test/test_utils.jl") + + G, traj = bilinear_dynamics_and_trajectory(add_time = true) + + @test_throws ErrorException TimeDependentBilinearIntegrator( + (a, t) -> G(a), + :x, + :u, + :t, + traj; + spline_order = 2, + ) +end From 8f606608768895727b2132f2a96d51d0b47be3f4 Mon Sep 17 00:00:00 2001 From: aaron Date: Wed, 19 Aug 2026 19:24:35 -0400 Subject: [PATCH 2/7] refactor: remove unreachable spline-order error branches in TimeDependentBilinearIntegrator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The constructor errors on spline_order ∉ {0, 1} before the struct can be created (the custom inner constructor is the only one, and spline_order is an immutable Int field), so the else-error branches in the u_template setup, evaluate!, eval_jacobian, and eval_hessian_of_lagrangian were unreachable — dead re-validations of an invariant established at construction. Replaced with one-line conditionals. Same category as cluster A's dead-path removals: code that cannot execute given intra-file invariants. No behavior change. --- .../time_dependent_bilinear_integrator.jl | 39 ++++--------------- 1 file changed, 8 insertions(+), 31 deletions(-) diff --git a/src/integrators/time_dependent_bilinear_integrator.jl b/src/integrators/time_dependent_bilinear_integrator.jl index 0317daff..97cdb19d 100644 --- a/src/integrators/time_dependent_bilinear_integrator.jl +++ b/src/integrators/time_dependent_bilinear_integrator.jl @@ -104,13 +104,8 @@ struct TimeDependentBilinearIntegrator{F} <: AbstractBilinearIntegrator return nothing end - u_template = if spline_order == 0 - zeros(u_dim) - elseif spline_order == 1 - zeros(2u_dim) - else - error("Unsupported spline order: $spline_order") - end + # spline_order ∈ {0, 1} is guaranteed by the validation above. + u_template = spline_order == 0 ? zeros(u_dim) : zeros(2u_dim) p_template = vcat(u_template, 1.0, 0.0) # [controls..., Δt, t] @@ -155,14 +150,8 @@ function evaluate!( tₖ = traj[k][B.t_name][1] Δtₖ = traj[k].timestep - if B.spline_order == 0 - pₖ = uₖ - elseif B.spline_order == 1 - uₖ₊₁ = traj[k+1][B.u_name] - pₖ = [uₖ; uₖ₊₁] - else - error("Unsupported spline order: $(B.spline_order)") - end + # spline_order ∈ {0, 1} is validated at construction. + pₖ = B.spline_order == 0 ? uₖ : [uₖ; traj[k+1][B.u_name]] δ[slice(k, B.x_dim)] = B.f(xₖ₊₁, xₖ, pₖ, Δtₖ, tₖ) end @@ -185,14 +174,8 @@ end Δtₖ = zₖ[traj.components[traj.timestep]][1] xₖ₊₁ = zₖ₊₁[traj.components[B.x_name]] - if B.spline_order == 0 - pₖ = uₖ - elseif B.spline_order == 1 - uₖ₊₁ = zₖ₊₁[traj.components[B.u_name]] - pₖ = [uₖ; uₖ₊₁] - else - error("Unsupported spline order: $(B.spline_order)") - end + # spline_order ∈ {0, 1} is validated at construction. + pₖ = B.spline_order == 0 ? uₖ : [uₖ; zₖ₊₁[traj.components[B.u_name]]] return B.f(xₖ₊₁, xₖ, pₖ, Δtₖ, tₖ) end, @@ -224,14 +207,8 @@ function eval_hessian_of_lagrangian( Δtₖ = zₖ[traj.components[traj.timestep]][1] xₖ₊₁ = zₖ₊₁[traj.components[B.x_name]] - if B.spline_order == 0 - pₖ = uₖ - elseif B.spline_order == 1 - uₖ₊₁ = zₖ₊₁[traj.components[B.u_name]] - pₖ = [uₖ; uₖ₊₁] - else - error("Unsupported spline order: $(B.spline_order)") - end + # spline_order ∈ {0, 1} is validated at construction. + pₖ = B.spline_order == 0 ? uₖ : [uₖ; zₖ₊₁[traj.components[B.u_name]]] return μₖ'B.f(xₖ₊₁, xₖ, pₖ, Δtₖ, tₖ) end, From 770c52b53b030e4a19081a52219d12e014476a1f Mon Sep 17 00:00:00 2001 From: aaron Date: Wed, 19 Aug 2026 19:24:48 -0400 Subject: [PATCH 3/7] test(coverage): constrain.jl bound-spec application branches + zero-row skip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BoundsConstraint application (the MOI functor) had never seen a Vector{Float64} symmetric bound spec (global or trajectory variable) nor a (lb, ub) tuple spec on a global variable — only scalar and trajectory-tuple specs ran. Exercises all four combinations against fresh optimizers and counts the emitted MOI constraints. Also covers GlobalLinearConstraint's all-zero-row continue: a row of A that is identically zero with 0 ∈ [lo, hi] is structurally feasible and must be skipped without error (only the infeasible variant was tested before). Counts via a MockOptimizer since Ipopt's wrapper does not implement NumberOfConstraints for affine-in-set constraints. --- src/solvers/constrain.jl | 69 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/solvers/constrain.jl b/src/solvers/constrain.jl index 36340986..d83e6f87 100644 --- a/src/solvers/constrain.jl +++ b/src/solvers/constrain.jl @@ -480,3 +480,72 @@ end @test traj_dist < 1e-4 end + +@testitem "coverage: BoundsConstraint vector and tuple bound application" setup = + [DTOTestHelpers] begin + _, traj = bilinear_dynamics_and_trajectory(add_global = true) + + n_vars = traj.dim * traj.N + traj.global_dim + g_dim = length(traj.global_components[:g]) + + function apply_to_fresh_optimizer(con) + opt = Ipopt.Optimizer() + vars = MOI.add_variables(opt, n_vars) + con(opt, vars, traj) + return opt + end + + n_greater_than(opt) = + MOI.get(opt, MOI.NumberOfConstraints{MOI.VariableIndex,MOI.GreaterThan{Float64}}()) + n_less_than(opt) = + MOI.get(opt, MOI.NumberOfConstraints{MOI.VariableIndex,MOI.LessThan{Float64}}()) + + # Vector{Float64} bounds on a global variable: symmetric [-b, b] per component + b = 0.1 .+ 0.01 .* collect(1:g_dim) + opt = apply_to_fresh_optimizer(GlobalBoundsConstraint(:g, b)) + @test n_greater_than(opt) == g_dim + @test n_less_than(opt) == g_dim + + # (lb, ub) tuple bounds on a global variable + lb = fill(-0.2, g_dim) + ub = fill(0.3, g_dim) + opt = apply_to_fresh_optimizer(GlobalBoundsConstraint(:g, (lb, ub))) + @test n_greater_than(opt) == g_dim + @test n_less_than(opt) == g_dim + + # Vector{Float64} bounds on a trajectory variable (du has dim 2) + du_dim = traj.dims[:du] + opt = apply_to_fresh_optimizer(BoundsConstraint(:du, 1:traj.N, fill(0.4, du_dim))) + @test n_greater_than(opt) == du_dim * traj.N + @test n_less_than(opt) == du_dim * traj.N +end + +@testitem "coverage: GlobalLinearConstraint skips feasible all-zero rows" setup = + [DTOTestHelpers] begin + using SparseArrays + + _, traj = bilinear_dynamics_and_trajectory(add_global = true) + + g_dim = length(traj.global_components[:g]) + # Row 1 pins g[1] - g[2] = 0; row 2 is all zeros with 0 ∈ [lo, hi] — + # structurally feasible, so it is skipped (continue) rather than an error. + A = spzeros(2, g_dim) + A[1, 1] = 1.0 + A[1, 2] = -1.0 + con = GlobalLinearConstraint(:g, A, [0.0, -1.0], [0.0, 1.0]) + + # A mock optimizer: Ipopt's MOI wrapper does not implement + # NumberOfConstraints for affine-in-set constraints, and the functor only + # needs add_constraints. + opt = MOI.Utilities.MockOptimizer( + MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()), + ) + vars = MOI.add_variables(opt, traj.dim * traj.N + traj.global_dim) + con(opt, vars, traj) + + # Only the equality row was materialized (one affine-in-EqualTo constraint). + @test MOI.get( + opt, + MOI.NumberOfConstraints{MOI.ScalarAffineFunction{Float64},MOI.EqualTo{Float64}}(), + ) == 1 +end From 10b210c7521ef71243626622f24582c0ea4cb60d Mon Sep 17 00:00:00 2001 From: aaron Date: Wed, 19 Aug 2026 19:24:56 -0400 Subject: [PATCH 4/7] test(coverage): test-harness norm branches, verbose diff printing, composite scaling Closes the remaining dark branches of the shared validation harnesses: - test_objective (src/objectives/_objectives.jl): the num::Real * CompositeObjective weight-rescaling branch; the test_equality = false norm-based gradient checks for both atol > 0 and atol == 0 (relative tolerance); the show_gradient_diff / show_hessian_diff verbose branches, driven with a quartic loss whose finite-difference truncation error makes the element-wise printers fire while rtol keeps the still-run comparisons green. - test_constraint (src/constraints/_constraints.jl): the test_equality = false norm-based Jacobian/Hessian branches for both atol > 0 and atol == 0. - test_integrator (src/integrators/_integrators.jl): the atol == 0 relative-tolerance norm branches for Jacobian and Hessian. The verbose diff printlns that only fire when an adjacent comparison FAILS (test_constraint's show_jacobian_diff/show_hessian_diff element printers, test_integrator's gauss_newton printer) are deliberately left uncovered: they are exactly complementary to the @test that would fail. --- src/constraints/_constraints.jl | 18 +++++++++++++++ src/integrators/_integrators.jl | 3 +++ src/objectives/_objectives.jl | 40 +++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/src/constraints/_constraints.jl b/src/constraints/_constraints.jl index 6e99d475..cc0fc129 100644 --- a/src/constraints/_constraints.jl +++ b/src/constraints/_constraints.jl @@ -254,4 +254,22 @@ include("nonlinear/knot_point_constraint.jl") include("nonlinear/global_constraint.jl") include("nonlinear/global_knot_point_constraint.jl") +@testitem "coverage: test_constraint norm-based comparison branches" setup = + [DTOTestHelpers] begin + # A tiny trajectory keeps the verbose diff output short. + traj = NamedTrajectory( + (x = randn(2, 3), u = randn(1, 3), Δt = fill(0.1, 3)); + controls = (:u, :Δt), + timestep = :Δt, + ) + + NLC = NonlinearKnotPointConstraint(x -> [norm(x) - 1.0], :x, traj) + + # norm-based Jacobian/Hessian checks: atol > 0 paths + test_constraint(NLC, traj; test_equality = false, atol = 1e-3) + + # norm-based checks with atol == 0: relative-tolerance paths + test_constraint(NLC, traj; test_equality = false, atol = 0.0, rtol = 1e-3) +end + end diff --git a/src/integrators/_integrators.jl b/src/integrators/_integrators.jl index c34d101a..f46ba475 100644 --- a/src/integrators/_integrators.jl +++ b/src/integrators/_integrators.jl @@ -256,6 +256,9 @@ end integ0 = BilinearIntegrator(G0, :x, :u, traj0) test_integrator(integ0, traj0; test_equality = false, atol = 1e-4) + # atol == 0 selects the relative-tolerance norm branches + test_integrator(integ0, traj0; test_equality = false, atol = 0.0, rtol = 1e-3) + # the gauss_newton branch (masked comparison + its verbose printing) test_integrator(integ0, traj0; gauss_newton = true, show_hessian_diff = true) end diff --git a/src/objectives/_objectives.jl b/src/objectives/_objectives.jl index aa03df79..2b62853d 100644 --- a/src/objectives/_objectives.jl +++ b/src/objectives/_objectives.jl @@ -439,4 +439,44 @@ end @test occursin("1.5", s) @test occursin("0.25", s) end + +@testitem "coverage: scaling a CompositeObjective and norm-based test_objective branches" setup = + [DTOTestHelpers] begin + _, traj = bilinear_dynamics_and_trajectory() + + # num::Real * CompositeObjective rescales the weights in place + quad_u = QuadraticRegularizer(:u, traj, 1.0) + quad_x = QuadraticRegularizer(:x, traj, 2.0) + comp = 0.5 * quad_u + 0.25 * quad_x + comp_scaled = 2.0 * comp + @test comp_scaled isa CompositeObjective + @test comp_scaled.objectives == comp.objectives + @test comp_scaled.weights == [1.0, 0.5] + @test objective_value(comp_scaled, traj) ≈ 2.0 * objective_value(comp, traj) + + # norm-based gradient checks: atol > 0 path + test_objective(quad_u, traj; test_equality = false, atol = 1e-3) + + # norm-based gradient checks with atol == 0: relative-tolerance path + test_objective(quad_u, traj; test_equality = false, atol = 0.0, rtol = 1e-3) +end + +@testitem "coverage: test_objective verbose diff printing" setup = [DTOTestHelpers] begin + # A tiny trajectory keeps the printed diff tables short. + traj = NamedTrajectory( + (x = randn(2, 3), u = randn(1, 3), Δt = fill(0.1, 3)); + controls = (:u, :Δt), + timestep = :Δt, + ) + + # A quartic loss carries real finite-difference truncation error, so with + # atol = 0 the element-wise printers inside the show_gradient_diff / + # show_hessian_diff branches fire for at least one entry. The show + # branches assert nothing themselves; rtol keeps the (still-run) Hessian + # comparison comfortably green. + obj = KnotPointObjective(x -> norm(x)^4, :x, traj) + + test_objective(obj, traj; show_gradient_diff = true, atol = 0.0, rtol = 1e-6) + test_objective(obj, traj; show_hessian_diff = true, atol = 0.0, rtol = 1e-6) +end end From 44db42379ea09548e9120d3fbc6daab183e6c3ca Mon Sep 17 00:00:00 2001 From: aaron Date: Wed, 19 Aug 2026 19:25:03 -0400 Subject: [PATCH 5/7] =?UTF-8?q?test(coverage):=20solver=20surfaces=20?= =?UTF-8?q?=E2=80=94=20refine=20sync,=20callback=20freq/dip,=20stats=20fal?= =?UTF-8?q?lbacks,=20constraint=20filter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ipopt _solve: a refine= kwarg reaching the solver must re-sync the derived adaptive_mu_globalization field (only the eval_hessian sync was exercised before). Asserts the mutation on a user-owned options struct. - callback_best_rollout_fidelity_factory: freq = 2 covers the every-other-iteration early return, and a fidelity sequence with a dip forces the insertion scan to walk past an incumbent it does not beat (completing the loop body without a break). - _solve_stats: an optimizer that supports only TerminationStatus covers the fallbacks (raw status from string(status), objective NaN, iterations -1). - fix_global_variable!: unrelated constraints in the list must survive the filter (the return-true branch). - show_problem_details comment: records why 'Constraints: (none)' is unreachable via construction (NamedTrajectories types timestep as a Symbol, so the Δt-bounds injection always fires). --- src/constraints/linear/equality_constraint.jl | 26 ++++++++++ src/problems.jl | 2 + src/solvers/ipopt_solver/callbacks.jl | 51 +++++++++++++++++++ src/solvers/ipopt_solver/solver.jl | 21 ++++++++ src/solvers/solve_stats.jl | 22 ++++++++ 5 files changed, 122 insertions(+) diff --git a/src/constraints/linear/equality_constraint.jl b/src/constraints/linear/equality_constraint.jl index e2e47b34..8c50cf3e 100644 --- a/src/constraints/linear/equality_constraint.jl +++ b/src/constraints/linear/equality_constraint.jl @@ -384,3 +384,29 @@ end @test length(g_eq_cons_after) == 1 @test g_eq_cons_after[1].values ≈ fill(0.5, g_dim) end + +@testitem "coverage: fix_global_variable! keeps unrelated constraints" setup = + [DTOTestHelpers] begin + _, traj = bilinear_dynamics_and_trajectory(add_global = true) + + g_dim = length(traj.global_components[:g]) + cons = AbstractConstraint[ + GlobalBoundsConstraint(:g, 1.0), + BoundsConstraint(:u, 1:traj.N, 0.1), + EqualityConstraint(:u, [traj.N], [0.0, 0.0]), + ] + + fix_global_variable!(cons, :g, zeros(g_dim)) + + # the :g bounds constraint was removed; the two trajectory-variable + # constraints survived the filter; the pinned global equality was + # appended (GlobalEqualityConstraint is a convenience constructor that + # returns an EqualityConstraint with is_global = true) + @test length(cons) == 3 + @test !any(c -> c isa BoundsConstraint && c.is_global, cons) + @test count(c -> c isa BoundsConstraint && !c.is_global, cons) == 1 + @test cons[end] isa EqualityConstraint + @test cons[end].is_global + @test cons[end].var_names == :g + @test cons[end].values == zeros(g_dim) +end diff --git a/src/problems.jl b/src/problems.jl index f1feb69e..2a257b64 100644 --- a/src/problems.jl +++ b/src/problems.jl @@ -491,6 +491,8 @@ end @test occursin("Controls: Δt", s_plain) # the default Δt bounds injection guarantees a BoundsConstraint even on a # minimal trajectory — "Constraints: (none)" is unreachable via construction + # (NamedTrajectories types `timestep` as a Symbol, so the injection always + # applies when the trajectory's own bounds lack the timestep) @test occursin("BoundsConstraint: \"bounds on Δt\"", s_plain) @test occursin("Dynamics (0 integrators)", s_plain) end diff --git a/src/solvers/ipopt_solver/callbacks.jl b/src/solvers/ipopt_solver/callbacks.jl index 719ae27e..746261ac 100644 --- a/src/solvers/ipopt_solver/callbacks.jl +++ b/src/solvers/ipopt_solver/callbacks.jl @@ -834,5 +834,56 @@ end @test raw_count[] == ic.count[] # both fire once per IPM iteration end +@testitem "callback_best_rollout_fidelity_factory freq gating and fidelity dips" setup=[ + DTOTestHelpers, +] begin + prob, _ = make_standard_prob() + + # Fidelity sequence with a dip: 0.9, then 0.5 (worse), then slowly + # improving. The dip forces the insertion scan to walk past an incumbent + # it does not beat (completing the loop body without a break), and + # freq = 2 makes every other iteration return early. + call_count = Ref(0) + mock_fid_fn = (traj, sys) -> begin + call_count[] += 1 + if call_count[] == 1 + return 0.9 + elseif call_count[] == 2 + return 0.5 + else + return 0.7 + 0.01 * call_count[] + end + end + + trajectories = Dict{Int32,Any}() + callback = Callbacks.callback_factory( + Callbacks.callback_update_trajectory_factory(prob), + Callbacks.callback_best_rollout_fidelity_factory( + prob, + nothing, + mock_fid_fn, + trajectories; + max_trajectories = 3, + freq = 2, + fid_thresh = nothing, + ), + Callbacks.callback_stop_iteration_factory(12), + ) + + optimizer, variables = IpoptSolverExt.get_optimizer_and_variables( + prob, + IpoptOptions(; max_iter = 20, print_level = 0), + callback, + ) + IpoptSolverExt.MOI.optimize!(optimizer) + + # The first push plus at least one post-dip insertion landed. + @test 2 <= length(trajectories) <= 3 + for (k, (fid, t)) in trajectories + @test fid isa Number + @test t isa NamedTrajectory + end +end + end diff --git a/src/solvers/ipopt_solver/solver.jl b/src/solvers/ipopt_solver/solver.jl index 03e87fc8..0c460e62 100644 --- a/src/solvers/ipopt_solver/solver.jl +++ b/src/solvers/ipopt_solver/solver.jl @@ -532,3 +532,24 @@ end prob = DirectTrajOptProblem(traj, J, integrators) solve!(prob; max_iter = 5, eval_hessian = false, print_level = 0) end + +@testitem "solve! refine kwarg syncs adaptive_mu_globalization in _solve" begin + include("../../../test/test_utils.jl") + + G, traj = bilinear_dynamics_and_trajectory() + prob = DirectTrajOptProblem( + traj, + QuadraticRegularizer(:u, traj, 1.0), + [BilinearIntegrator(G, :x, :u, traj)], + ) + + # IpoptOptions computes adaptive_mu_globalization at construction; a + # refine= kwarg reaching _solve must re-sync the derived field. + opts = IpoptOptions(; max_iter = 3, print_level = 0) + @test opts.adaptive_mu_globalization == "obj-constr-filter" + + stats = DirectTrajOpt._solve(prob, opts; refine = false, verbose = false) + @test stats isa Solvers.SolveStats + @test opts.refine == false + @test opts.adaptive_mu_globalization == "never-monotone-mode" +end diff --git a/src/solvers/solve_stats.jl b/src/solvers/solve_stats.jl index 643056c4..cef4f744 100644 --- a/src/solvers/solve_stats.jl +++ b/src/solvers/solve_stats.jl @@ -67,3 +67,25 @@ end @test !isempty(stats.raw_status) @test stats.status isa MOI.TerminationStatusCode end + +@testitem "coverage: _solve_stats fallbacks when the optimizer lacks an API" setup = + [DTOTestHelpers] begin + using DirectTrajOpt.Solvers: _solve_stats + + # A minimal stand-in optimizer: only TerminationStatus is supported, so + # the raw-status, objective-value, and barrier-iteration getters all + # take their fallbacks (string(status), NaN, -1). + struct _NoStatsOptimizer end + MOI.get(::_NoStatsOptimizer, ::MOI.TerminationStatus) = MOI.LOCALLY_SOLVED + MOI.get(::_NoStatsOptimizer, ::MOI.RawStatusString) = error("unsupported") + MOI.get(::_NoStatsOptimizer, ::MOI.ObjectiveValue) = error("unsupported") + MOI.get(::_NoStatsOptimizer, ::MOI.BarrierIterations) = error("unsupported") + + stats = _solve_stats(_NoStatsOptimizer(), nothing, :mock, time()) + @test stats.status === MOI.LOCALLY_SOLVED + @test stats.raw_status == string(MOI.LOCALLY_SOLVED) + @test isnan(stats.objective_value) + @test stats.iterations == -1 + @test stats.solver === :mock + @test stats.solve_time_s >= 0 +end From 04d9801b7bcf18f059730fff748c5f5c8ab0b1bf Mon Sep 17 00:00:00 2001 From: aaron Date: Wed, 19 Aug 2026 19:25:10 -0400 Subject: [PATCH 6/7] test(coverage): drop unreachable lower-triangle debug print in evaluator testitem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The debug loop scanned the evaluator's Hessian-of-the-Lagrangian structure for entries with j < i and printed them — but the structure is filtered to i ≤ j at construction (evaluator.jl, hessian_structure assembly), so the branch could never fire. Provably dead test-side diagnostic; removing it changes no assertion. --- src/solvers/evaluator.jl | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/solvers/evaluator.jl b/src/solvers/evaluator.jl index 7760bfad..56c6c682 100644 --- a/src/solvers/evaluator.jl +++ b/src/solvers/evaluator.jl @@ -759,12 +759,6 @@ end ∂²ℒ_values = zeros(length(∂²ℒ_structure)) - for (i, j) ∈ ∂²ℒ_structure - if j < i - println("Hessian index: (", i, ", ", j, ")") - end - end - MOI.eval_hessian_lagrangian(evaluator, ∂²ℒ_values, traj.datavec, σ, μ) n_vars = From d0b586015d213f367027d68b5948e8961609da68 Mon Sep 17 00:00:00 2001 From: aaron Date: Thu, 20 Aug 2026 05:55:28 -0400 Subject: [PATCH 7/7] =?UTF-8?q?fix(tests):=20de-flake=20the=20vector-synta?= =?UTF-8?q?x=20fixture=20=E2=80=94=20smooth=20g,=20tight=20atol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The historical fixture g(a) = [norm(a) - 1.0] is kinky at zero; a finite-difference Hessian across a kink is unstable, and the test flaked on CI runners for exactly that reason (observed on main's own baseline coverage run, pre-cluster-B; twice more on this PR's 1.12 jobs). The syntax-equivalence claim ([:u] behaves identically to :u) needs no kink: g(a) = [sum(abs2, a) - 1.0] is smooth and the tight atol=1e-6 now holds. Verified 3× consecutive clean runs. --- .../nonlinear/knot_point_constraint.jl | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/constraints/nonlinear/knot_point_constraint.jl b/src/constraints/nonlinear/knot_point_constraint.jl index 1db12e9c..ee997a48 100644 --- a/src/constraints/nonlinear/knot_point_constraint.jl +++ b/src/constraints/nonlinear/knot_point_constraint.jl @@ -311,14 +311,19 @@ end end @testitem "NonlinearKnotPointConstraint - single variable with vector syntax" begin - using DirectTrajOpt: CommonInterface + using DirectTrajOpt: CommonInterface, NonlinearKnotPointConstraint + using DirectTrajOpt: test_constraint include("../../../test/test_utils.jl") _, traj = bilinear_dynamics_and_trajectory() - # Test that [:u] syntax works the same as :u - g(a) = [norm(a) - 1.0] + # Test that [:u] syntax works the same as :u. The fixture is deliberately + # SMOOTH: the historical `norm(a) - 1.0` is kinky at zero, and a finite- + # difference Hessian across a kink is unstable — the test flaked on CI + # runners for exactly that reason (pre-existing; observed on main's own + # baseline run). The syntax-equivalence claim needs no kink. + g(a) = [sum(abs2, a) - 1.0] NLC1 = NonlinearKnotPointConstraint(g, :u, traj; equality = false) NLC2 = NonlinearKnotPointConstraint(g, [:u], traj; equality = false) @@ -330,9 +335,9 @@ end @test δ1 ≈ δ2 - # Test both with finite differences - test_constraint(NLC1, traj; atol = 1e-3) - test_constraint(NLC2, traj; atol = 1e-3) + # Test both with finite differences (smooth fixture: tight tolerances hold) + test_constraint(NLC1, traj; atol = 1e-6) + test_constraint(NLC2, traj; atol = 1e-6) end @testitem "NonlinearKnotPointConstraint - multiple variables concatenated" begin