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

Đang mở
#441 2 bình luận 0 reaction 1 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 1 ngày

@ocots đang làm issue này rồi.

Từ ngày 8/11/2026.

Đánh giá

Issue này chưa được đánh giá.

Mô tả

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.
Ngôn ngữ chính
Julia
Star
13
Fork
1
Merge trung bình
3 giờ 36 phút
Pull request đã merge (30 ngày)
57

Chuẩn bị môi trường

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của control-toolbox/OptimalControlProblems.jl

Tất cả issue của control-toolbox/OptimalControlProblems.jl

Issue tương tự

Thêm issue về Julia

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.