Skip to content

Orchestration and Routing ​

This guide explains how the Orchestration module routes user-provided keyword arguments to the correct strategy in a multi-strategy pipeline. It covers the method tuple concept, automatic routing, disambiguation syntax, and the helper functions that power the system.

Prerequisites

Read Implementing a Strategy first. Orchestration builds on top of the strategy metadata system.

We define three fake strategies — a discretizer, a modeler, and a solver — with a shared backend option to demonstrate routing and disambiguation:

julia
# --- Fake discretizer family ---
abstract type AbstractFakeDiscretizer <:
    CTBase.Strategies.AbstractStrategy end

struct FakeCollocation <: AbstractFakeDiscretizer
    options::CTBase.Strategies.StrategyOptions
end

CTBase.Strategies.id(::Type{<:FakeCollocation}) = :collocation
CTBase.Strategies.parameter(::Type{<:FakeCollocation}) = nothing

CTBase.Strategies.metadata(::Type{<:FakeCollocation}) =
    CTBase.Strategies.StrategyMetadata(
        Options.OptionDefinition(
            name = :grid_size, type = Int,
            default = 100, description = "Grid size",
        ),
    )

FakeCollocation(; kwargs...) = FakeCollocation(
    CTBase.Strategies.build_strategy_options(
        FakeCollocation; kwargs...,
    ),
)

# --- Fake modeler family ---
abstract type AbstractFakeModeler <:
    CTBase.Strategies.AbstractStrategy end

struct FakeADNLP <: AbstractFakeModeler
    options::CTBase.Strategies.StrategyOptions
end

CTBase.Strategies.id(::Type{<:FakeADNLP}) = :adnlp
CTBase.Strategies.parameter(::Type{<:FakeADNLP}) = nothing

CTBase.Strategies.metadata(::Type{<:FakeADNLP}) =
    CTBase.Strategies.StrategyMetadata(
        Options.OptionDefinition(
            name = :backend, type = Symbol,
            default = :default, description = "AD backend",
        ),
    )

FakeADNLP(; kwargs...) = FakeADNLP(
    CTBase.Strategies.build_strategy_options(
        FakeADNLP; kwargs...,
    ),
)

# --- Fake solver family ---
abstract type AbstractFakeSolver <:
    CTBase.Strategies.AbstractStrategy end

struct FakeIpopt <: AbstractFakeSolver
    options::CTBase.Strategies.StrategyOptions
end

CTBase.Strategies.id(::Type{<:FakeIpopt}) = :ipopt
CTBase.Strategies.parameter(::Type{<:FakeIpopt}) = nothing

CTBase.Strategies.metadata(::Type{<:FakeIpopt}) =
    CTBase.Strategies.StrategyMetadata(
        Options.OptionDefinition(
            name = :max_iter, type = Integer,
            default = 1000, description = "Max iterations",
        ),
        Options.OptionDefinition(
            name = :backend, type = Symbol,
            default = :cpu, description = "Compute backend",
        ),
    )

FakeIpopt(; kwargs...) = FakeIpopt(
    CTBase.Strategies.build_strategy_options(
        FakeIpopt; kwargs...,
    ),
)

# --- Registry ---
registry = CTBase.Strategies.create_registry(
    AbstractFakeDiscretizer => (FakeCollocation,),
    AbstractFakeModeler     => (FakeADNLP,),
    AbstractFakeSolver      => (FakeIpopt,),
)
StrategyRegistry with 3 families:
├─ AbstractFakeSolver
│  └─ FakeIpopt (id=:ipopt)
├─ AbstractFakeDiscretizer
│  └─ FakeCollocation (id=:collocation)
└─ AbstractFakeModeler
   └─ FakeADNLP (id=:adnlp)

The Method Tuple Concept ​

A method tuple identifies which concrete strategy to use for each role in the pipeline:

julia
method = (:collocation, :adnlp, :ipopt)

Each symbol is a strategy id (returned by Strategies.id(::Type)). The families mapping associates each role with its abstract type:

julia
families = (
    discretizer = AbstractFakeDiscretizer,
    modeler     = AbstractFakeModeler,
    solver      = AbstractFakeSolver,
)

The orchestration system uses the StrategyRegistry to resolve each symbol to its concrete type and access its metadata.

Automatic Routing ​

When a user passes keyword arguments, route_all_options automatically routes each option to the strategy that owns it:

julia
solve(ocp, :collocation, :adnlp, :ipopt;
    grid_size = 100,    # only discretizer defines this → auto-route
    max_iter  = 1000,   # only solver defines this → auto-route
    display   = true,   # action option → extracted separately
)

The routing algorithm:

text
User kwargs
     │
     ▼
Extract action options (display, etc.)
     │
     ▼
Remaining kwargs
     │
     ▼
Build option ownership map
(which family defines each option)
     │
     ├─ 0 owners  →  ERROR: Unknown option
     │
     ├─ 1 owner   →  Auto-route to owner
     │
     └─ 2+ owners →  Disambiguation syntax used?
                          ├─ Yes → Route to specified strategy
                          └─ No  → ERROR: Ambiguous option

How it works internally ​

  • Extract action options — options like display are matched against action_defs and removed from the pool

  • Build strategy-to-family map — maps each strategy ID to its family name (e.g., :ipopt → :solver)

  • Build option ownership map — scans all strategy metadata to determine which family defines each option name

  • Route each remaining option — auto-route if unambiguous, require disambiguation if ambiguous, error if unknown

Disambiguation ​

When an option name appears in multiple strategies (e.g., backend is defined by both the modeler and the solver), the user must disambiguate using route_to:

route_to accepts two equivalent syntaxes — keyword and positional — choose based on preference:

SyntaxExample
Keywordroute_to(adnlp = :sparse)
Positionalroute_to(:adnlp, :sparse)

Single strategy ​

julia
# keyword syntax
solve(ocp, :collocation, :adnlp, :ipopt;
    backend = route_to(adnlp = :sparse),
)

# positional syntax (equivalent)
solve(ocp, :collocation, :adnlp, :ipopt;
    backend = route_to(:adnlp, :sparse),
)

Multiple strategies ​

julia
# keyword syntax
solve(ocp, :collocation, :adnlp, :ipopt;
    backend = route_to(adnlp = :sparse, ipopt = :cpu),
)

# positional syntax (equivalent)
solve(ocp, :collocation, :adnlp, :ipopt;
    backend = route_to(:adnlp, :sparse, :ipopt, :cpu),
)

How route_to works ​

route_to creates a RoutedOption object that carries (strategy_id => value) pairs:

julia
opt = Strategies.route_to(ipopt = 100, adnlp = 50)
CTBase.Strategies.RoutedOption((ipopt = 100, adnlp = 50))

The orchestration layer uses extract_strategy_ids to unpack this object during routing — see Helper Functions below.

Helper Functions ​

The helper functions below are used internally by route_all_options. They operate on a ResolvedMethod — a precomputed view of the method tuple. In normal usage you do not need to call them directly; they are exposed for advanced use cases (custom routing logic, introspection, testing).

To use them, first call resolve_method:

julia
resolved = Orchestration.resolve_method(method, families, registry)

build_strategy_to_family_map ​

Maps each strategy ID in the method to its family name:

julia
Orchestration.build_strategy_to_family_map(resolved, families, registry)
Dict{Symbol, Symbol} with 3 entries:
  :adnlp       => :modeler
  :ipopt       => :solver
  :collocation => :discretizer

build_option_ownership_map ​

Scans all strategy metadata and maps each option name to the set of families that define it:

julia
Orchestration.build_option_ownership_map(resolved, families, registry)
Dict{Symbol, Set{Symbol}} with 3 entries:
  :grid_size => Set([:discretizer])
  :max_iter  => Set([:solver])
  :backend   => Set([:modeler, :solver])

Note that :backend is owned by both :modeler and :solver — it is ambiguous and requires disambiguation.

extract_strategy_ids ​

Unpacks a RoutedOption into a vector of (value, strategy_id) pairs:

julia
Orchestration.extract_strategy_ids(
    Strategies.route_to(ipopt = 100, adnlp = 50), resolved,
)
2-element Vector{Tuple{Any, Symbol}}:
 (100, :ipopt)
 (50, :adnlp)

For plain (non-routed) values, no disambiguation is detected — the function returns nothing:

julia
julia> Orchestration.extract_strategy_ids(:plain_value, resolved)

Passing an unknown strategy ID throws an error:

julia> Orchestration.extract_strategy_ids(
           Strategies.route_to(unknown = 42), resolved,
       )
ERROR: IncorrectArgument
│
│  Strategy ID not found in method tuple
│
│  Got       strategy ID :unknown
│  Expected  one of available strategy IDs: (:collocation, :adnlp, :ipopt)
│
│  Context   extract_strategy_ids - validating RoutedOption strategy ID
│  Hint      Use a valid strategy ID from your method tuple
└─

Complete Example ​

Auto-routing with disambiguation and action option extraction:

julia
action_defs = [
    Options.OptionDefinition(name = :display, type = Bool, default = true,
                     description = "Display solver progress"),
]

kwargs = (
    grid_size = 100,                                     # auto-routed to discretizer
    max_iter  = 500,                                     # auto-routed to solver
    backend   = Strategies.route_to(adnlp = :optimized), # disambiguated to modeler
    display   = false,                                   # action option
)

routed = Orchestration.route_all_options(
    method, families, action_defs,
    kwargs, registry,
)
(action = (display = false (user),), strategies = (modeler = (backend = :optimized,), solver = (max_iter = 500,), discretizer = (grid_size = 100,)))

Action options:

julia
routed.action
(display = false (user),)

Strategy options per family:

julia
routed.strategies
(modeler = (backend = :optimized,), solver = (max_iter = 500,), discretizer = (grid_size = 100,))

Error: unknown option ​

julia> Orchestration.route_all_options(
           method, families, action_defs,
           (foo = 42,), registry,
       )
ERROR: IncorrectArgument
│
│  Unknown option provided
│
│  Got       option :foo in method (:collocation, :adnlp, :ipopt)
│  Expected  valid option name for one of the strategies
│
│  Context   route_options - unknown option validation
│  Hint      Did you mean?
│              - :backend [distance: 7]
│              - :max_iter [distance: 8]
│              - :grid_size [distance: 9]
│            If you're confident this option exists for a specific strategy, use bypass() to skip validation:
│              custom_opt = route_to(<strategy_id>=bypass(<value>))
└─

Error: ambiguous option without disambiguation ​

julia> Orchestration.route_all_options(
           method, families, action_defs,
           (backend = :sparse,), registry,
       )
ERROR: IncorrectArgument
│
│  Ambiguous option requires disambiguation
│
│  Got       option :backend between strategies: adnlp, ipopt
│  Expected  strategy-specific routing using route_to()
│
│  Context   route_options - ambiguous option resolution
│  Hint      Use route_to() like backend = route_to(adnlp=sparse) to specify target strategy
└─