Skip to content

[Bug] Mixing explicit solve and option routing ignores the routing part #852

Closed

Description

@abavoil

Describe the bug
What is showcased here does not seem to work on v2.0.4.

To Reproduce

using OptimalControl, NLPModelsIpopt

ocp = @def begin
    t ∈ [0, 1], time
    x ∈ R², state
    u ∈ R, control
    x(0) == [-1, 0]
    x(1) == [0, 0]
    ẋ(t) == [x₂(t), u(t)]
    ∫(0.5u(t)^2) → min
end

disc = OptimalControl.Collocation()

## Backend option is ignored
s = solve(ocp; disc, backend=:generic)

## Code that should error
s = solve(ocp; disc, backend=:beep)
s = solve(ocp; disc, beep=:beep)

## Expected errors

s = solve(ocp; backend=:beep)
#=
ERROR: Control Toolbox Error

❌ Error: CTBase.Exceptions.IncorrectArgument, Invalid ADNLPModels backend
=#

s = solve(ocp; beep=:beep)
#=
ERROR: Control Toolbox Error

❌ Error: CTBase.Exceptions.IncorrectArgument, Unknown option provided
=#

Expected behavior
The to-be-routed options should not be discarded, they should be routed if valid, or result in an error otherwise.

Additional context
OptimalControl v2.0.4

Activity

  1. ocots commented on Aug 13, 2026

    @ocots
    Member

    Response to issue #852

    Thanks @abavoil for reporting this and for providing such a clear reproducer.

    There was indeed a bug here, but it was slightly different from backend being ignored intentionally. The actual bug was that solve silently accepted and discarded an option that it could not consume. An exception should have been raised instead.

    There are two different ways to call solve:

    • Descriptive (implicit) mode: the method is selected with symbolic tokens, and strategy options can be passed to solve and routed to the appropriate strategy. For example:

      solve(ocp, :collocation, :adnlp, :ipopt; backend=:generic)
    • Explicit mode: at least one typed strategy instance is passed to solve. In that mode, the strategy is already constructed, so its options must be provided when constructing the strategy. Options are not routed from the solve call.

      disc = OptimalControl.Collocation()
      modeler = OptimalControl.ADNLP(backend=:generic)
      solve(ocp; discretizer=disc, modeler=modeler)

    Therefore, in:

    solve(ocp; disc, backend=:generic)

    disc automatically selects explicit mode. The problem is not that backend is lost; this syntax is not valid for explicit mode because backend is a strategy option and should have been provided when constructing the modeler.

    The behavior has been fixed so that these invalid calls now raise informative IncorrectArgument exceptions instead of being silently ignored. The fix is included in PR #853. It will be included in a future version of OptimalControl.jl; however, we do not plan to publish a release solely for this fix. It will be included in the next regular release.

    The reproducer can therefore be corrected as follows:

    using OptimalControl, NLPModelsIpopt
    
    ocp = @def begin
        t ∈ [0, 1], time
        x ∈ R², state
        u ∈ R, control
        x(0) == [-1, 0]
        x(1) == [0, 0]
        ẋ(t) == [x₂(t), u(t)]
        ∫(0.5u(t)^2) → min
    end
    
    disc = OptimalControl.Collocation()
    modeler = OptimalControl.ADNLP(backend=:generic)
    
    # Correct explicit-mode call:
    s = solve(ocp; discretizer=disc, modeler=modeler)
    
    # Or use descriptive (implicit) mode, where strategy options are routed:
    s = solve(ocp, :collocation, :adnlp, :ipopt; backend=:generic)

    The following are independent invalid examples; each one is expected to raise an exception:

    # Invalid in explicit mode: `backend` must be configured when constructing
    # the modeler. This now raises an informative IncorrectArgument exception.
    s = solve(ocp; disc, backend=:generic)
    
    # Invalid in explicit mode for the same reason.
    s = solve(ocp; disc, backend=:beep)
    
    # Invalid in explicit mode: `beep` is neither a solve action option nor an
    # option declared by any of the completed strategies.
    s = solve(ocp; disc, beep=:beep)

    For comparison, these calls remain descriptive (implicit) mode calls because no typed component is provided:

    # Invalid backend value. The option is routed to ADNLP, which raises an
    # IncorrectArgument for the invalid backend.
    s = solve(ocp; backend=:beep)
    
    # Invalid option. No action or strategy accepts this option, so solve raises
    # an IncorrectArgument for an unknown option.
    s = solve(ocp; beep=:beep)

    The expected distinction is now:

    solve(ocp; disc, backend=:generic)
    → IncorrectArgument: `backend` is a modeler/solver strategy option in
      explicit mode; construct the strategy with `backend=...` and pass it to solve.
    
    solve(ocp; disc, beep=:beep)
    → IncorrectArgument: unknown explicit-mode solve option `beep`; the error lists
      the options accepted directly by the solve action.
    
    solve(ocp; backend=:beep)
    → IncorrectArgument: invalid ADNLP backend.
    
    solve(ocp; beep=:beep)
    → IncorrectArgument: unknown option provided in descriptive mode.
    

    Thanks again for helping us identify this behavior and improve the error handling.

  2. abavoil commented on Aug 14, 2026

    @abavoil
    ContributorAuthor

    Ah I read the doc too quicly, that part describes the behavior, but at least now it clearer that this doesn't work, thank you :)

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions