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.metadata(::Type{<:FakeCollocation}) = CTBase.Strategies.StrategyMetadata(
    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.metadata(::Type{<:FakeADNLP}) = CTBase.Strategies.StrategyMetadata(
    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.metadata(::Type{<:FakeIpopt}) = CTBase.Strategies.StrategyMetadata(
    OptionDefinition(name = :max_iter, type = Integer, default = 1000, description = "Max iterations"),
    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)
├─ AbstractFakeModeler
│  └─ FakeADNLP (id=:adnlp)
└─ AbstractFakeDiscretizer
   └─ FakeCollocation (id=:collocation)

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

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

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

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

  4. 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 = 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 = resolve_method(method, families, registry)

build_strategy_to_family_map

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

julia
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
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
extract_strategy_ids(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> extract_strategy_ids(:plain_value, resolved)

Passing an unknown strategy ID throws an error:

julia
julia> extract_strategy_ids(route_to(unknown = 42), resolved)
IncorrectArgument  top-level scope, REPL[1]:2

│  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 = [
    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   = route_to(adnlp = :optimized), # disambiguated to modeler
    display   = false,                        # action option
)

routed = 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
julia> route_all_options(method, families, action_defs, (foo = 42,), registry)
IncorrectArgument  top-level scope, REPL[1]:2

│  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
julia> route_all_options(method, families, action_defs, (backend = :sparse,), registry)
IncorrectArgument  top-level scope, REPL[1]:2

│  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
└─