diff --git a/src/Flows/Flows.jl b/src/Flows/Flows.jl index e5f63a2..1bd79a4 100644 --- a/src/Flows/Flows.jl +++ b/src/Flows/Flows.jl @@ -35,6 +35,8 @@ import ..Solutions: Solutions include(joinpath(@__DIR__, "abstract_flow.jl")) include(joinpath(@__DIR__, "flow.jl")) +include(joinpath(@__DIR__, "registry.jl")) +include(joinpath(@__DIR__, "flow_routing.jl")) include(joinpath(@__DIR__, "building.jl")) include(joinpath(@__DIR__, "calling.jl")) diff --git a/src/Flows/building.jl b/src/Flows/building.jl index 62fe255..8185e1a 100644 --- a/src/Flows/building.jl +++ b/src/Flows/building.jl @@ -68,3 +68,105 @@ function Flow(data::Data.HamiltonianVectorField; state_dimension::Union{Int, Not return build_flow(system, integrator) end +""" +$(TYPEDSIGNATURES) + +High-level constructor for `HamiltonianFlow` from a scalar Hamiltonian. + +This constructor builds a complete Hamiltonian flow by: +1. Routing keyword options to the appropriate strategy families (backend and integrator) +2. Building a concrete AD backend and integrator from the routed options +3. Building a `HamiltonianSystem` from the Hamiltonian and backend +4. Combining them into a callable `HamiltonianFlow` + +# Arguments +- `h::CTFlows.Data.AbstractHamiltonian`: The scalar Hamiltonian function. +- `kwargs...`: Keyword options passed to the backend and integrator strategies. + Options are automatically routed based on their names: + - Backend options (e.g., `ad_backend`, `prepare_cache`) → `:di` strategy + - Integrator options (e.g., `reltol`, `abstol`, `alg`) → `:sciml` strategy + +# Returns +- `CTFlows.Flows.HamiltonianFlow`: The complete Hamiltonian flow ready for integration. + +# Throws +- [`CTBase.Exceptions.IncorrectArgument`](@extref): If an option is unknown, ambiguous, + or routed to the wrong strategy. +- [`CTBase.Exceptions.ExtensionError`](@extref): If the `CTFlowsSciML` extension is not loaded + (required for `:sciml` strategy metadata). + +# Example +```julia +using CTFlows.Data, CTFlows.Flows + +h = Data.Hamiltonian((t, x, p, v) -> 0.5 * (x[1]^2 + p[1]^2); is_autonomous=true, is_variable=false) +flow = Flows.Flow(h; reltol=1e-8, ad_backend=ADTypes.AutoForwardDiff()) +# flow isa CTFlows.Flows.HamiltonianFlow +``` + +# Notes +- The state dimension is inferred from the Hamiltonian's signature. +- Use the `state_dimension` argument overload if explicit dimension is needed. +- Requires the `CTFlowsSciML` extension to be loaded for integrator options. + +See also: [`CTFlows.Flows.HamiltonianFlow`](@ref), [`CTFlows.Systems.build_system`](@ref), +[`_route_flow_options`](@ref), [`_build_flow_components`](@ref) +""" +function Flow(h::Data.AbstractHamiltonian; kwargs...) + routed = _route_flow_options(kwargs) + components = _build_flow_components(routed) + sys = Systems.build_system(h, components.backend) + return build_flow(sys, components.integrator) +end + +""" +$(TYPEDSIGNATURES) + +High-level constructor for `HamiltonianFlow` from a scalar Hamiltonian with explicit state dimension. + +This constructor builds a complete Hamiltonian flow by: +1. Routing keyword options to the appropriate strategy families (backend and integrator) +2. Building a concrete AD backend and integrator from the routed options +3. Building a `HamiltonianSystem` from the Hamiltonian and backend with explicit state dimension +4. Combining them into a callable `HamiltonianFlow` + +# Arguments +- `h::CTFlows.Data.AbstractHamiltonian`: The scalar Hamiltonian function. +- `state_dimension::Int`: The state dimension (number of state variables, not including costates). +- `kwargs...`: Keyword options passed to the backend and integrator strategies. + Options are automatically routed based on their names: + - Backend options (e.g., `ad_backend`, `prepare_cache`) → `:di` strategy + - Integrator options (e.g., `reltol`, `abstol`, `alg`) → `:sciml` strategy + +# Returns +- `CTFlows.Flows.HamiltonianFlow`: The complete Hamiltonian flow ready for integration. + +# Throws +- [`CTBase.Exceptions.IncorrectArgument`](@extref): If an option is unknown, ambiguous, + or routed to the wrong strategy. +- [`CTBase.Exceptions.ExtensionError`](@extref): If the `CTFlowsSciML` extension is not loaded + (required for `:sciml` strategy metadata). + +# Example +```julia +using CTFlows.Data, CTFlows.Flows + +h = Data.Hamiltonian((t, x, p, v) -> 0.5 * (x[1]^2 + p[1]^2); is_autonomous=true, is_variable=false) +flow = Flows.Flow(h, 1; reltol=1e-8, ad_backend=ADTypes.AutoForwardDiff()) +# flow isa CTFlows.Flows.HamiltonianFlow +``` + +# Notes +- Use this overload when the state dimension cannot be inferred from the Hamiltonian's signature. +- Requires the `CTFlowsSciML` extension to be loaded for integrator options. + +See also: [`CTFlows.Flows.HamiltonianFlow`](@ref), [`CTFlows.Systems.build_system`](@ref), +[`Flow(h::AbstractHamiltonian; kwargs...)`](@ref), [`_route_flow_options`](@ref) +""" +function Flow(h::Data.AbstractHamiltonian, state_dimension::Int; kwargs...) + routed = _route_flow_options(kwargs) + components = _build_flow_components(routed) + sys = Systems.build_system(h, components.backend; state_dimension=state_dimension) + return build_flow(sys, components.integrator) +end + diff --git a/src/Flows/flow_routing.jl b/src/Flows/flow_routing.jl new file mode 100644 index 0000000..1fec782 --- /dev/null +++ b/src/Flows/flow_routing.jl @@ -0,0 +1,119 @@ +""" +$(TYPEDSIGNATURES) + +Return the strategy families used for option routing in flow construction. + +The returned `NamedTuple` maps family names to their abstract types, as expected +by [`CTSolvers.Orchestration.route_all_options`](@extref). + +# Returns +- `NamedTuple`: `(backend, integrator)` mapped to their abstract types + +# Example +```julia +# Get the strategy families for flow construction +fam = Flows._flow_families() +# Returns: (backend = CTFlows.Differentiation.AbstractADBackend, integrator = CTFlows.Integrators.AbstractIntegrator) +``` + +See also: [`_route_flow_options`](@ref), [`flow_registry`](@ref) +""" +function _flow_families() + return ( + backend = Differentiation.AbstractADBackend, + integrator = Integrators.AbstractIntegrator, + ) +end + +const _FLOW_DESCRIPTION = (:di, :sciml) + +""" +$(TYPEDSIGNATURES) + +Route all keyword options to the appropriate strategy families for flow construction. + +This function wraps [`CTSolvers.Orchestration.route_all_options`](@extref) with the +families specific to CTFlows flow construction. Options are routed to either the +backend family (`:di`) or the integrator family (`:sciml`). + +# Arguments +- `kwargs`: All keyword arguments from the user's `Flow` call (strategy options only, + no action-level options). + +# Returns +- `NamedTuple` with fields: + - `action`: action-level options (always empty for flows) + - `strategies`: `NamedTuple` with `backend` and `integrator` sub-tuples + +# Throws +- [`CTBase.Exceptions.IncorrectArgument`](@extref): If an option is unknown, ambiguous, + or routed to the wrong strategy. + +# Example +```julia +# Route options to backend and integrator +routed = Flows._route_flow_options((; reltol=1e-8, ad_backend=ADTypes.AutoForwardDiff())) +# routed.strategies.integrator contains (reltol = 1e-8,) +# routed.strategies.backend contains (ad_backend = AutoForwardDiff(),) +``` + +# Notes +- This function uses `:description` source mode for user-friendly error messages. +- No action-level options are defined for flows (empty `OptionDefinition` array). + +See also: [`_flow_families`](@ref), [`_build_flow_components`](@ref), +[`CTSolvers.Orchestration.route_all_options`](@extref) +""" +function _route_flow_options(kwargs) + return CTSolvers.Orchestration.route_all_options( + _FLOW_DESCRIPTION, + _flow_families(), + CTSolvers.Options.OptionDefinition[], + (; kwargs...), + flow_registry(); + source_mode = :description, + ) +end + +""" +$(TYPEDSIGNATURES) + +Build concrete strategy instances from routed options. + +Each strategy is constructed via +[`CTSolvers.Orchestration.build_strategy_from_resolved`](@extref) using the options +that were routed to its family by [`_route_flow_options`](@ref). + +# Arguments +- `routed`: Result of [`_route_flow_options`](@ref) containing routed option values. + +# Returns +- `NamedTuple{(:backend, :integrator)}`: Concrete strategy instances. + +# Example +```julia +# Build concrete strategies from routed options +routed = Flows._route_flow_options((; reltol=1e-8)) +components = Flows._build_flow_components(routed) +# components.backend isa CTFlows.Differentiation.DifferentiationInterface +# components.integrator isa CTFlows.Integrators.SciML +``` + +See also: [`_route_flow_options`](@ref), [`flow_registry`](@ref), +[`CTSolvers.Orchestration.build_strategy_from_resolved`](@extref) +""" +function _build_flow_components(routed) + families = _flow_families() + resolved = CTSolvers.Orchestration.resolve_method( + _FLOW_DESCRIPTION, families, flow_registry() + ) + backend = CTSolvers.Orchestration.build_strategy_from_resolved( + resolved, :backend, families, flow_registry(); + routed.strategies.backend... + ) + integrator = CTSolvers.Orchestration.build_strategy_from_resolved( + resolved, :integrator, families, flow_registry(); + routed.strategies.integrator... + ) + return (backend = backend, integrator = integrator) +end diff --git a/src/Flows/registry.jl b/src/Flows/registry.jl new file mode 100644 index 0000000..860f11a --- /dev/null +++ b/src/Flows/registry.jl @@ -0,0 +1,28 @@ +const _FLOW_REGISTRY = CTSolvers.Strategies.create_registry( + Differentiation.AbstractADBackend => (Differentiation.DifferentiationInterface,), + Integrators.AbstractIntegrator => (Integrators.SciML,), +) + +""" +$(TYPEDSIGNATURES) + +Return the strategy registry for flow construction. + +The registry maps abstract strategy families to their concrete implementations +for automatic differentiation backends and ODE integrators. + +# Returns +- `CTSolvers.Strategies.StrategyRegistry`: Registry with `:di` (DifferentiationInterface) + and `:sciml` (SciML) strategies registered. + +# Notes +- This registry is used by [`_route_flow_options`](@ref) to resolve and build + concrete strategy instances from keyword arguments. +- The registry is precomputed and cached in `_FLOW_REGISTRY` for performance. + +See also: [`_route_flow_options`](@ref), [`_build_flow_components`](@ref), +[`CTSolvers.Strategies.create_registry`](@extref) +""" +function flow_registry() + return _FLOW_REGISTRY +end diff --git a/test/suite/flows/test_flow_routing.jl b/test/suite/flows/test_flow_routing.jl new file mode 100644 index 0000000..85020e0 --- /dev/null +++ b/test/suite/flows/test_flow_routing.jl @@ -0,0 +1,151 @@ +""" +Unit and integration tests for flow routing via CTSolvers. +""" + +module TestFlowRouting + +import Test +import CTBase.Exceptions +import CTFlows.Flows +import CTFlows.Differentiation +import CTFlows.Integrators +import CTFlows.Data +import CTFlows.Systems +import CTFlows.Traits +import CTFlows.Common +import CTSolvers +import ADTypes +using OrdinaryDiffEqTsit5 + +const VERBOSE = isdefined(Main, :TestOptions) ? Main.TestOptions.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestOptions) ? Main.TestOptions.SHOWTIMING : true + +# ============================================================================== +# Fake Hamiltonian for Testing (at module top-level) +# ============================================================================== + +const _TEST_H = Data.Hamiltonian( + (t, x, p, v) -> 0.5 * (x[1]^2 + p[1]^2); + is_autonomous=true, is_variable=false +) + +function test_flow_routing() + Test.@testset "Flow Routing Tests" verbose=VERBOSE showtiming=SHOWTIMING begin + + # ==================================================================== + # UNIT TESTS - Registry and Families + # ==================================================================== + + Test.@testset "Unit: _flow_families" begin + families = Flows._flow_families() + Test.@test haskey(families, :backend) + Test.@test haskey(families, :integrator) + Test.@test families.backend === Differentiation.AbstractADBackend + Test.@test families.integrator === Integrators.AbstractIntegrator + end + + Test.@testset "Unit: _FLOW_DESCRIPTION" begin + Test.@test Flows._FLOW_DESCRIPTION === (:di, :sciml) + end + + Test.@testset "Unit: flow_registry" begin + registry = Flows.flow_registry() + Test.@test registry isa CTSolvers.Strategies.StrategyRegistry + # Check that strategies are registered + backend_ids = CTSolvers.Strategies.strategy_ids(Differentiation.AbstractADBackend, registry) + Test.@test :di in backend_ids + integrator_ids = CTSolvers.Strategies.strategy_ids(Integrators.AbstractIntegrator, registry) + Test.@test :sciml in integrator_ids + end + + # ==================================================================== + # UNIT TESTS - Option Routing + # ==================================================================== + + Test.@testset "Unit: _route_flow_options — empty kwargs" begin + routed = Flows._route_flow_options(NamedTuple()) + Test.@test haskey(routed, :strategies) + Test.@test haskey(routed.strategies, :backend) + Test.@test haskey(routed.strategies, :integrator) + Test.@test isempty(routed.strategies.backend) + Test.@test isempty(routed.strategies.integrator) + end + + Test.@testset "Unit: _route_flow_options — integrator option" begin + routed = Flows._route_flow_options((; reltol=1e-10)) + Test.@test haskey(routed.strategies, :integrator) + Test.@test haskey(routed.strategies.integrator, :reltol) + Test.@test routed.strategies.integrator.reltol == 1e-10 + end + + Test.@testset "Unit: _route_flow_options — backend alias" begin + routed = Flows._route_flow_options((; ad_backend=ADTypes.AutoForwardDiff())) + Test.@test haskey(routed.strategies, :backend) + Test.@test haskey(routed.strategies.backend, :ad_backend) + Test.@test routed.strategies.backend.ad_backend === ADTypes.AutoForwardDiff() + end + + # ==================================================================== + # ERROR TESTS + # ==================================================================== + + Test.@testset "Error: _route_flow_options — unknown option" begin + Test.@test_throws Exceptions.IncorrectArgument Flows._route_flow_options((; unknown_option=42)) + end + + # ==================================================================== + # UNIT TESTS - Component Building + # ==================================================================== + + Test.@testset "Unit: _build_flow_components — defaults" begin + routed = Flows._route_flow_options(NamedTuple()) + components = Flows._build_flow_components(routed) + Test.@test haskey(components, :backend) + Test.@test haskey(components, :integrator) + Test.@test components.backend isa Differentiation.DifferentiationInterface + Test.@test components.integrator isa Integrators.SciML + end + + # ==================================================================== + # INTEGRATION TESTS + # ==================================================================== + + Test.@testset "Integration: Flow(h) — end-to-end" begin + flow = Flows.Flow(_TEST_H) + Test.@test flow isa Flows.HamiltonianFlow + Test.@test flow isa Flows.AbstractFlow + Test.@test Flows.system(flow) isa Systems.AbstractHamiltonianSystem + Test.@test Flows.integrator(flow) isa Integrators.AbstractIntegrator + end + + Test.@testset "Integration: Flow(h, n) — end-to-end" begin + flow = Flows.Flow(_TEST_H, 1) + Test.@test flow isa Flows.HamiltonianFlow + Test.@test flow isa Flows.AbstractFlow + Test.@test Flows.system(flow) isa Systems.AbstractHamiltonianSystem + Test.@test Flows.integrator(flow) isa Integrators.AbstractIntegrator + end + + Test.@testset "Integration: Flow(h; reltol=1e-9)" begin + flow = Flows.Flow(_TEST_H; reltol=1e-9) + Test.@test flow isa Flows.HamiltonianFlow + Test.@test Flows.integrator(flow) isa Integrators.SciML + end + + # ==================================================================== + # REGRESSION TESTS + # ==================================================================== + + Test.@testset "Regression: Flow(h) sans kwargs" begin + flow = Flows.Flow(_TEST_H) + Test.@test flow isa Flows.HamiltonianFlow + Test.@test Flows.system(flow) isa Systems.AbstractHamiltonianSystem + end + + end +end + +end # module + +# CRITICAL: Redefine in outer scope for TestRunner +test_flow_routing() = TestFlowRouting.test_flow_routing()