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:
# --- 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:
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:
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:
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:
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 optionHow it works internally
Extract action options — options like
displayare matched againstaction_defsand removed from the poolBuild 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:
| Syntax | Example |
|---|---|
| Keyword | route_to(adnlp = :sparse) |
| Positional | route_to(:adnlp, :sparse) |
Single strategy
# 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
# 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:
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:
resolved = resolve_method(method, families, registry)build_strategy_to_family_map
Maps each strategy ID in the method to its family name:
build_strategy_to_family_map(resolved, families, registry)Dict{Symbol, Symbol} with 3 entries:
:adnlp => :modeler
:ipopt => :solver
:collocation => :discretizerbuild_option_ownership_map
Scans all strategy metadata and maps each option name to the set of families that define it:
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:
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> extract_strategy_ids(:plain_value, resolved)Passing an unknown strategy ID throws an error:
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:
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:
routed.action(display = false (user),)Strategy options per family:
routed.strategies(modeler = (backend = :optimized,), solver = (max_iter = 500,), discretizer = (grid_size = 100,))Error: unknown option
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> 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
└─