Skip to content

Dev/dto issue 122 fixes - #124

Open
gennadiryan wants to merge 2 commits into
mainfrom
dev/dto-issue-122-fixes
Open

Dev/dto issue 122 fixes#124
gennadiryan wants to merge 2 commits into
mainfrom
dev/dto-issue-122-fixes

Conversation

@gennadiryan

Copy link
Copy Markdown
Member

DirectTrajOpt — follow-up layer on #122

Refs: issue #122 ·
PR #123 (merged
2026-08-02, base 19e414e)

Branch: issue-122 — two commits on top of the merged #123 tree
(1b4acb5 "Spot fixes", e45a0e2 "Updating CHANGELOG").


What this is

#123 satisfied every acceptance criterion in #122: the single-Δt weighting across
value, gradient, full Hessian and Hessian structure, the N-invariance regression,
finite-difference parity at variable timestep, and the migration note. This layer sits
on top of it and covers three things found while reviewing that change — one a direct
extension of #122's own reasoning, two pre-existing and unrelated to the weighting.

Nothing here revisits the weighting decision. R still means what #123 says it means.

1. hessian_structure over-declared the control–control block

R::Vector{Float64} is a vector of per-component weights, so ∂²J/∂v² = Δt · diag(R)
is diagonal — but the full d × d block was declared, reserving d(d-1)/2
structural nonzeros per knot that can never be nonzero.

This is the same defect class as #122's AC4, which called out the Δt–Δt entry as "the
one a mechanical edit of the arithmetic would miss." AC4's rationale — do not declare
a structural nonzero that is always zero
— applies verbatim to the off-diagonals, and
#123 left them declared while removing the Δt–Δt entry. Over-declaration is safe
(the evaluator only ever writes into declared entries, so the risk direction is
under-declaration) but it inflates the assembled sparsity for every consumer.

Measured on a 4-knot, d = 3 fixture: 48 → 24 declared nonzeros.

get_full_hessian now returns dropzeros!(∂²J) so the emitted pattern and the
declared pattern agree exactly.

2. Fixed-timestep trajectories — defensive guards (pre-existing)

gradient!, hessian_structure and get_full_hessian looked up
traj.components[traj.timestep] unconditionally. With a Float64 timestep that
indexes a NamedTuple with a float — a MethodError. LinearRegularizer already
guarded this; QuadraticRegularizer did not, and in gradient! the use was guarded
by traj.timestep isa Symbol while the lookup above it was not.

This is unreachable today. NamedTrajectories types the field timestep::Symbol
(struct_named_trajectory.jl:67), so a fixed timestep is not representable and the
guard constant-folds. It is filed under ### Internal in the changelog rather than
### Fixed for exactly that reason, and the corresponding test was dropped rather than
written against an unconstructible fixture — test_utils.jl's
named_trajectory_type_1(free_time = false) branch throws, and had no callers.

Kept anyway because it costs nothing, restores symmetry with LinearRegularizer, and
matches an assumption Piccolissimo's device backend already documents. Note the guard
must be partial: unlike LinearRegularizer, this objective cannot return early,
because ∂²J/∂v² = Δt·R is nonzero whether or not Δt is a decision variable.

3. Migration note was value-only

The docstring and changelog said the old behaviour is "reproduced exactly by passing
R * Δt". True for the value on a uniform grid, but not for ∂J/∂Δt when the
timestep is free — old Δt ΔvᵀRΔv versus substituted ½Δt ΔvᵀRΔv. Since free-time
problems are the common case downstream, both texts now say so explicitly.

Tests

  • The #123 structure test is extended from one-directional coverage
    (structure ⊇ nonzeros(H)) to exact agreement in both directions, pinning the
    diagonal claim in §1. It uses a deterministic fixture with strictly nonzero R and
    residuals: with a zero weight or a zero residual component the entry is legitimately
    declared but absent from H, and the converse direction would flake against the
    randomly-initialised shared fixture.
  • Renamed to "QuadraticRegularizer Hessian structure declares exactly the nonzeros".

Results: regularizer test items 36/36; evaluator test items 10/10 (the
integration-level check that §1's tightening did not under-declare).

Release

Project.toml is unchanged at 0.9.8 and the changelog entry is still under
[Unreleased], pending the 0.9.9 / 0.10.0 decision. Worth resolving before
release: R silently changes meaning, which argues for the minor bump, though #122
left the staging question ("plain fix with migration note" versus "temporary opt-in")
to the maintainer.

Out of scope

Retuning the R weights baked into downstream templates — explicitly out per #122's
Scope section, and unchanged here. See dto-122-piccolissimo.1.md: the retuning
fallout is now measured, and it reaches Piccolissimo's own suite, not only Piccolo's
templates.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark 'DirectTrajOpt.jl convergence'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.20.

Benchmark suite Current: e45a0e2 Previous: 0cd7923 Ratio
xgate_convergence_ipopt_N51 [infidelity] 7.749312302962608e-11 infidelity 4.429490108037726e-11 infidelity 1.75
xgate_convergence_madnlp_N51 [infidelity] 2.4337198922808057e-12 infidelity 3.086420008457935e-14 infidelity 78.85

This comment was automatically generated by workflow using github-action-benchmark.

@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant