Rationalize JuMP and comparison test suites: separate generic transcription unit tests from per-problem modeling checks

Offen
#441 2 Kommentare 0 Reaktionen 1 zugewiesene Person Auf GitHub ansehen

Maintainer antworten meist innerhalb von 1 Tag

@ocots arbeitet bereits daran.

Seit 08.11.2026.

Bewertung

Dieses Issue wurde noch nicht bewertet.

Beschreibung

Context and Motivation

Since #368, #369, and #370, JuMP models in ext/JuMPModels/ are no longer transcribed problem by problem:

  • Each problem file (ext/JuMPModels/<problem>.jl) only declares a ProblemData struct (components, bounds, times, dynamics, Lagrange/Mayer costs, path and boundary constraints) and delegates model construction to build.
  • The transcription itself (ext/JuMPModels/schemes/: data.jl, tableaux.jl, build.jl) and the solution accessors / builder (ext/JuMPModels.jl: state, control, costate, variable, time_grid, solution) are completely generic.

In #401, test/suite/comparison/test_equivalence.jl was updated to reflect this separation: it checks :trapeze on every problem (to verify that each problem's modeling matches OptimalControl), and checks all schemes on a small subset of problems selected by their features (every_scheme).

However, the rest of test/suite/JuMP/ and test/suite/comparison/ still loops over the Cartesian product all problems × all schemes (× all orderings), running hundreds of redundant JuMP model builds, CTDirect discretizations, and max_iter=0 Ipopt/MadNLP solves that test the generic transcription code over and over instead of testing problem-specific modeling.

Goals

  1. Unit-test the generic JuMP transcription once (test/suite/JuMP/):
    • Test ButcherTableau traits, scheme equations, convergence orders, variable/constraint orderings (:interleaved, :blocks), the model dictionary contract (:scheme, :ordering, :time_grid, :constraints, container lengths), _start! initial guess loading, JuMP.Model argument forwarding, and error paths (IncorrectArgument, NotImplemented) on small toy ProblemData definitions.
  2. Test each problem's modeling once per problem (test/suite/JuMP/ and test/suite/comparison/):
    • Verify that each problem's ProblemData, metadata parameters, and default initial guess are well-formed and consistent on a single default build (without looping over schemes/orderings or launching a solver just to read the initial time grid).
    • Keep test_smoke.jl (SMOKE_GRID, SMOKE_MAX_ITER) on every problem so every problem is shown to build and run a few solver iterations.
    • Verify NLP equivalence with OptimalControl (CTDirect) on every problem using a single configuration (scheme=:trapeze, ordering=:interleaved), plus component name agreement (state_components, control_components, variable_components).
  3. Test transcription & solution integration with OptimalControl.jl on a representative subset of problems (test/suite/comparison/):
    • Use feature-based selection (covering cost types, fixed/free times, scalar vs. vector state/control/variable per DIM1, box/path/boundary constraints) to test all 4 schemes × 2 orderings against CTDirect and to test OCP.solution(model, pb) and its interpolations (:linear vs. :constant).
  4. Preserve cron/ and robustness/ behavior while sharing common test runners (test/common/):
    • Keep test/cron/ and test/robustness/ solving all problems on their default grids and emitting the exact report tables and .tsv/.toml artifacts expected by tools/cron/report.py.
    • Factor out duplicated test code across OptimalControl and OptimalControlScalarized (test_problem.jl, test_smoke.jl, test_convergence.jl) into shared helpers in test/common/, while preserving tools/ci/scope.py dependency detection (Repository.includers).

Current State and Redundancies (for $P = 22$ problems)

1. test/suite/JuMP/
  • test_contract.jl:
    • Current: Builds $P \times 4 \text{ schemes} \times 2 \text{ orderings} = 176$ JuMP models, runs Ipopt with max_iter=0 whenever tf is free to evaluate model[:time_grid](), and runs $2P = 44$ error checks (scheme=:gauss_legendre_2, ordering=:random).
    • Redundancy: Keys like :scheme, :ordering, :constraints, container sizes ($N+1$, $N$, $M$), and unknown scheme/ordering errors are determined solely by schemes/build.jl, not by <problem>.jl.
  • test_kwargs.jl:
    • Current: Builds $3P = 66$ JuMP models to verify that Ipopt.Optimizer and add_bridges=true are forwarded to JuMP.Model and that passing init = 2 * guess doubles JuMP.start_value.
    • Redundancy: Keyword forwarding and _start! are generic functions in schemes/build.jl. Only checking that the problem's default metadata init populates all start values is problem-specific.
  • test_tableaux.jl, test_build.jl, test_signs.jl, test_draft.jl, test_methods.jl, test_smoke.jl:
    • Already targeted and non-redundant.
2. test/suite/comparison/
  • test_equivalence.jl & test/common/equivalence.jl:
    • Current: Tests :trapeze on every problem and all 4 schemes on selected = every_scheme(both, features). However, Equivalence.test_jump still loops over both ORDERINGS = (:interleaved, :blocks) for every problem, even though :blocks only permutes variable/constraint creation order in schemes/build.jl.
  • test_start.jl:
    • Current: Loops over $P \times 4 \text{ schemes} = 88$ pairs, building the JuMP model and calling OptimalControl.discretize + OptimalControl.nlp_model(..., ADNLP()) to compare JuMP.start_value with nlp.meta.x0.
    • Redundancy: Equivalence.compare and consistent_nlps in test_equivalence.jl already compare meta.x0 (:x0) on every problem for :trapeze and on the representative subset for all 4 schemes.
  • test_init.jl (test/common/comparison.jl):
    • Current: Runs Ipopt/MadNLP with max_iter=0 on SMALL_GRID = 50 for every problem across JuMP, OptimalControl, and OptimalControlScalarized ($3P = 66$ solves) to compare nvar, ncon, component names, time grid, initial trajectories, and initial objective.
    • Redundancy: test_equivalence.jl already checks nvar, ncon, bounds, x0, objective, constraints, gradient, Jacobian, and Hessian at x0 to 1e-10 without invoking any solver. Only the component names (state_components, control_components, variable_components) are specific to test_init.jl, and they can be compared directly on the models without solving.
  • test_solution.jl:
    • Current: Loops over $P \times 4 \text{ schemes} = 88$ JuMP models on SMALL_GRID = 50, running Ipopt with max_iter=0 on each to check that OCP.solution(model, pb) and TestStatus.trajectory match the JuMP accessors, plus a full solve on beam for the 4 schemes.
    • Redundancy: OCP.solution(model, pb) and the JuMP accessors in ext/JuMPModels.jl are completely generic. Testing them on a representative subset of problems (covering scalar vs. vector state/control and $0, 1, >1$ variables) across the 4 schemes is sufficient.
3. Duplicated files across OptimalControl and OptimalControlScalarized
  • test/suite/OptimalControl/test_problem.jl and test/suite/OptimalControlScalarized/test_problem.jl are identical except for the backend constant.
  • test/suite/OptimalControl/test_smoke.jl and test/suite/OptimalControlScalarized/test_smoke.jl are identical except for the backend constant.
  • test/cron/OptimalControl/test_convergence.jl and test/cron/OptimalControlScalarized/test_convergence.jl are identical except for the backend constant.

Proposed Solutions

1. Move generic contract and _start! checks to test/suite/JuMP/test_build.jl
  • In test_build.jl:
    • Check the full model dictionary contract (:scheme, :ordering, :time_grid, :constraints, component keys and container lengths $N+1$, $M$, $N$, $1$) across all 4 schemes × 2 orderings on FAKE, plus the IncorrectArgument throws on unknown scheme and ordering.
    • Unit-test _start! (constant, vector, time-dependent function, custom init override, free final time) and JuMP.Model argument/keyword forwarding (Ipopt.Optimizer, add_bridges).
  • Simplify test_contract.jl (or merge test_kwargs.jl into it):
    • Build one model per problem (grid_size=2, default :trapeze and :interleaved).
    • Check problem-specific modeling invariants: non-empty component names, length(costate_components) == length(state_components), all variables have a finite JuMP.start_value, and the initial time grid evaluated at the start values is sorted with T[1] < T[end] (no Ipopt max_iter=0 call needed).
2. Streamline test/suite/comparison/
  • Shared representative problem selector:
    • Extract the feature-based representative problem selection (every_scheme in test_equivalence.jl, extended if needed to ensure scalar/vector state, control, and variable dimensions 0, 1, >1 are covered) into a shared helper in test/common/.
  • test_equivalence.jl & test/common/equivalence.jl:
    • On every problem: test (scheme=:trapeze, ordering=:interleaved) against CTDirect (ADNLP and Exa), which directly checks problem modeling equivalence (including meta.x0).
    • On the representative subset: test all 4 schemes × 2 orderings.
  • test_features.jl:
    • In addition to comparing features(vector) == features(scalarized), check that component names (state_components, control_components, variable_components) and dimensions agree between JuMP, OptimalControl, and OptimalControlScalarized for every problem (no solver call).
  • test_start.jl and test_init.jl:
    • Remove test_start.jl (since meta.x0 equivalence is already checked by test_equivalence.jl on every problem for :trapeze and on the representative subset for all 4 schemes, and _start! is unit-tested in test_build.jl).
    • Remove test_init.jl (since NLP evaluation at x0 is checked to 1e-10 by test_equivalence.jl and component names are checked by test_features.jl). Keep Comparison.within / Comparison.within_L2 (used by test_solution.jl).
  • test_solution.jl:
    • Run check_solution(problem, scheme) across the 4 schemes on the representative subset of problems (plus beam solved against OptimalControl), instead of all $P \times 4$ pairs.
3. Factorize shared OptimalControl / OptimalControlScalarized runners
  • Extract shared helpers in test/common/ for test_problem, test_smoke, and test_convergence so OptimalControl and OptimalControlScalarized call a single implementation parameterized by the backend, without altering report names, TestStatus.result! payloads, or tools/ci/scope.py scoping rules.
Vorherrschende Sprache
Julia
Sterne
13
Forks
1
Ø Merge
3 Std. 36 Min.
Gemergte PRs (30 T.)
57

Entwicklungsumgebung

Erste Schritte

  1. Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
  3. Forken Sie das Repository und arbeiten Sie in einem Branch.
  4. Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.

Mehr aus control-toolbox/OptimalControlProblems.jl

Alle Issues in control-toolbox/OptimalControlProblems.jl

Ähnliche Issues

Weitere Issues zu Julia

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.