From 5f0bdaec871c3b9689903f2f246ff1d07a59ef1f Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Sat, 4 Jul 2026 21:18:47 +0200 Subject: [PATCH 1/6] feat: add ControlLaw, PseudoHamiltonian, feedback trait, and pseudo-Hamiltonian AD contracts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add feedback trait hierarchy (OpenLoopFeedback, ClosedLoopFeedback, DynClosedLoopFeedback) using the type-parameter-only pattern. Implement AbstractControlLaw and ControlLaw with feedback, time-dependence, and variable-dependence traits. Provide OpenLoop, ClosedLoop, DynClosedLoop constructors. Natural and uniform call signatures are feedback-dependent: - OpenLoop: uniform u(t, v) — no state, no costate - ClosedLoop: uniform u(t, x, v) — state but no costate - DynClosedLoop: uniform u(t, x, p, v) — state and costate Implement AbstractPseudoHamiltonian and PseudoHamiltonian with time and variable dependence traits and explicit control argument u. Natural and uniform call signatures (t, x, p, u, v). Implement two AD contracts: - pseudo_hamiltonian_gradient: returns (∂H̃/∂x, ∂H̃/∂p) for the Hamiltonian flow - pseudo_hamiltonian_control_gradient: returns ∂H̃/∂u for stationarity checks Split justified by PMP stationarity condition ∂H̃/∂u = 0 along solutions. Add comprehensive unit tests for traits, construction, call signatures, type stability, subtyping, and AD contracts. --- ext/CTBaseDifferentiationInterface.jl | 82 +++++ src/Data/Data.jl | 11 + src/Data/abstract_control_law.jl | 175 ++++++++++ src/Data/abstract_pseudo_hamiltonian.jl | 95 ++++++ src/Data/control_law.jl | 315 ++++++++++++++++++ src/Data/helpers.jl | 136 ++++++++ src/Data/pseudo_hamiltonian.jl | 163 +++++++++ src/Differentiation/Differentiation.jl | 2 + src/Differentiation/abstract_ad_backend.jl | 83 +++++ src/Traits/Traits.jl | 7 +- src/Traits/feedback.jl | 132 ++++++++ test/suite/data/test_control_law.jl | 313 +++++++++++++++++ test/suite/data/test_data_module.jl | 14 +- test/suite/data/test_pseudo_hamiltonian.jl | 184 ++++++++++ .../test_differentiation_module.jl | 2 + .../test_pseudo_hamiltonian_gradient.jl | 219 ++++++++++++ test/suite/traits/test_feedback.jl | 153 +++++++++ 17 files changed, 2083 insertions(+), 3 deletions(-) create mode 100644 src/Data/abstract_control_law.jl create mode 100644 src/Data/abstract_pseudo_hamiltonian.jl create mode 100644 src/Data/control_law.jl create mode 100644 src/Data/pseudo_hamiltonian.jl create mode 100644 src/Traits/feedback.jl create mode 100644 test/suite/data/test_control_law.jl create mode 100644 test/suite/data/test_pseudo_hamiltonian.jl create mode 100644 test/suite/differentiation/test_pseudo_hamiltonian_gradient.jl create mode 100644 test/suite/traits/test_feedback.jl diff --git a/ext/CTBaseDifferentiationInterface.jl b/ext/CTBaseDifferentiationInterface.jl index a71c5802..0c27e121 100644 --- a/ext/CTBaseDifferentiationInterface.jl +++ b/ext/CTBaseDifferentiationInterface.jl @@ -100,6 +100,88 @@ function Differentiation.variable_gradient( return _derivator(typeof(v))(h_v, di_backend, v) end +""" +$(TYPEDSIGNATURES) + +Compute pseudo-Hamiltonian gradients (∂H̃/∂x, ∂H̃/∂p) via DifferentiationInterface.jl. + +Along a PMP solution, the stationarity condition ∂H̃/∂u = 0 holds, so the +Hamiltonian flow only requires ∂H̃/∂x and ∂H̃/∂p. Use +[`pseudo_hamiltonian_control_gradient`](@ref) for ∂H̃/∂u. + +Anonymous closures are used deliberately so that ForwardDiff `tagcount` values +are assigned at runtime in the correct left-to-right order inside `ForwardDiff.≺`, +avoiding silent zero-gradient bugs in nested-AD contexts (e.g. inside NonlinearSolve). + +# Arguments +- `backend::Differentiation.DifferentiationInterface`: The DI backend strategy. +- `h̃::Data.AbstractPseudoHamiltonian`: The pseudo-Hamiltonian. +- `t`: Time (scalar). +- `x`: State vector. +- `p`: Costate vector. +- `u`: Control (scalar or vector). +- `v`: Variable (scalar or `nothing` for Fixed problems). + +# Returns +- Tuple `(grad_x, grad_p)` where `grad_x` = ∂H̃/∂x, `grad_p` = ∂H̃/∂p. + +See also: [`CTBase.Differentiation.pseudo_hamiltonian_control_gradient`](@ref), +[`CTBase.Differentiation.hamiltonian_gradient`](@ref). +""" +function Differentiation.pseudo_hamiltonian_gradient( + backend::Differentiation.DifferentiationInterface, + h̃::Data.AbstractPseudoHamiltonian, + t, + x, + p, + u, + v, +) + di_backend = Differentiation.ad_backend(backend) + h̃_x(x_) = h̃(t, x_, p, u, v) + h̃_p(p_) = h̃(t, x, p_, u, v) + grad_x = _derivator(typeof(x))(h̃_x, di_backend, x) + grad_p = _derivator(typeof(p))(h̃_p, di_backend, p) + return (grad_x, grad_p) +end + +""" +$(TYPEDSIGNATURES) + +Compute the pseudo-Hamiltonian control gradient ∂H̃/∂u via DifferentiationInterface.jl. + +This is typically used to check the PMP stationarity condition ∂H̃/∂u = 0, +not for the Hamiltonian flow itself. + +# Arguments +- `backend::Differentiation.DifferentiationInterface`: The DI backend strategy. +- `h̃::Data.AbstractPseudoHamiltonian`: The pseudo-Hamiltonian. +- `t`: Time (scalar). +- `x`: State vector. +- `p`: Costate vector. +- `u`: Control (scalar or vector). +- `v`: Variable (scalar or `nothing` for Fixed problems). + +# Returns +- `grad_u`: ∂H̃/∂u. + +See also: [`CTBase.Differentiation.pseudo_hamiltonian_gradient`](@ref). +""" +function Differentiation.pseudo_hamiltonian_control_gradient( + backend::Differentiation.DifferentiationInterface, + h̃::Data.AbstractPseudoHamiltonian, + t, + x, + p, + u, + v, +) + di_backend = Differentiation.ad_backend(backend) + h̃_u(u_) = h̃(t, x, p, u_, v) + grad_u = _derivator(typeof(u))(h̃_u, di_backend, u) + return grad_u +end + # ============================================================================= # Differentiation.gradient — extension contract methods # ============================================================================= diff --git a/src/Data/Data.jl b/src/Data/Data.jl index 20c2ab5a..94531146 100644 --- a/src/Data/Data.jl +++ b/src/Data/Data.jl @@ -23,6 +23,10 @@ include(joinpath(@__DIR__, "abstract_vector_field.jl")) include(joinpath(@__DIR__, "vector_field.jl")) include(joinpath(@__DIR__, "abstract_hamiltonian.jl")) include(joinpath(@__DIR__, "hamiltonian.jl")) +include(joinpath(@__DIR__, "abstract_control_law.jl")) +include(joinpath(@__DIR__, "control_law.jl")) +include(joinpath(@__DIR__, "abstract_pseudo_hamiltonian.jl")) +include(joinpath(@__DIR__, "pseudo_hamiltonian.jl")) include(joinpath(@__DIR__, "abstract_hamiltonian_vector_field.jl")) include(joinpath(@__DIR__, "hamiltonian_vector_field.jl")) @@ -36,5 +40,12 @@ export HamiltonianVectorField export AbstractHamiltonianVectorField export AbstractHamiltonian export Hamiltonian +export AbstractControlLaw +export ControlLaw +export OpenLoop +export ClosedLoop +export DynClosedLoop +export AbstractPseudoHamiltonian +export PseudoHamiltonian end # module Data diff --git a/src/Data/abstract_control_law.jl b/src/Data/abstract_control_law.jl new file mode 100644 index 00000000..884c9b8b --- /dev/null +++ b/src/Data/abstract_control_law.jl @@ -0,0 +1,175 @@ +# ============================================================================= +# AbstractControlLaw — abstract supertype for control laws +# ============================================================================= + +""" +$(TYPEDEF) + +Abstract supertype for control laws together with their feedback, +time-dependence, and variable-dependence traits. + +A control law is a function `u(...)` that provides the control input for an +optimal control problem. The feedback trait ([`AbstractFeedback`](@ref)) +determines which arguments the control law depends on: + +- **Open-loop**: `u(t[, v])` — depends on time (and variable) only. +- **Closed-loop**: `u(t, x[, v])` — depends on time and state. +- **Dynamic closed-loop**: `u(t, x, p[, v])` — depends on time, state, and costate. + +# Type Parameters +- `FB <: AbstractFeedback`: `OpenLoopFeedback`, `ClosedLoopFeedback`, or `DynClosedLoopFeedback`. +- `TD <: TimeDependence`: `Autonomous` or `NonAutonomous`. +- `VD <: VariableDependence`: `Fixed` or `NonFixed`. + +# Notes +- All control law types support both natural and uniform call signatures. +- The uniform signature depends on the feedback trait: + - OpenLoop: `u(t, v)` — no state, no costate. + - ClosedLoop: `u(t, x, v)` — state but no costate. + - DynClosedLoop: `u(t, x, p, v)` — state and costate. + +See also: [`CTBase.Data.ControlLaw`](@ref), [`CTBase.Traits.AbstractFeedback`](@ref), +[`CTBase.Traits.TimeDependence`](@ref), [`CTBase.Traits.VariableDependence`](@ref). +""" +abstract type AbstractControlLaw{ + FB<:Traits.AbstractFeedback, + TD<:Traits.TimeDependence, + VD<:Traits.VariableDependence, +} end + +# ============================================================================= +# Trait accessors for AbstractControlLaw +# ============================================================================= + +""" +$(TYPEDSIGNATURES) + +Return the feedback trait of a control law. + +# Returns +- `FB`: The feedback type (`OpenLoopFeedback`, `ClosedLoopFeedback`, or `DynClosedLoopFeedback`). + +See also: [`CTBase.Traits.feedback`](@ref), [`CTBase.Traits.AbstractFeedback`](@ref). +""" +function Traits.feedback(::AbstractControlLaw{FB,<:Any,<:Any}) where {FB<:Traits.AbstractFeedback} + return FB +end + +""" +$(TYPEDSIGNATURES) + +Indicates that all `AbstractControlLaw` types support time-dependence queries. + +See also: [`CTBase.Traits.time_dependence`](@ref), [`CTBase.Data.AbstractControlLaw`](@ref). +""" +function Traits.has_time_dependence_trait(::AbstractControlLaw) + return true +end + +""" +$(TYPEDSIGNATURES) + +Indicates that all `AbstractControlLaw` types support variable-dependence queries. + +See also: [`CTBase.Traits.variable_dependence`](@ref), [`CTBase.Data.AbstractControlLaw`](@ref). +""" +function Traits.has_variable_dependence_trait(::AbstractControlLaw) + return true +end + +""" +$(TYPEDSIGNATURES) + +Return the time-dependence trait of a control law. + +See also: [`CTBase.Traits.time_dependence`](@ref), [`CTBase.Traits.TimeDependence`](@ref). +""" +function Traits.time_dependence( + ::AbstractControlLaw{<:Any,TD,<:Traits.VariableDependence} +) where {TD<:Traits.TimeDependence} + return TD +end + +""" +$(TYPEDSIGNATURES) + +Return the variable-dependence trait of a control law. + +See also: [`CTBase.Traits.variable_dependence`](@ref), [`CTBase.Traits.VariableDependence`](@ref). +""" +function Traits.variable_dependence( + ::AbstractControlLaw{<:Any,<:Traits.TimeDependence,VD} +) where {VD<:Traits.VariableDependence} + return VD +end + +# ============================================================================= +# Dynamics trait — depends on feedback kind +# ============================================================================= + +""" +$(TYPEDSIGNATURES) + +Return the dynamics trait of an open-loop control law, namely [`StateDynamics`](@ref). + +Open-loop and closed-loop control laws do not involve the costate, so they are +associated with state dynamics. + +See also: [`CTBase.Traits.dynamics_trait`](@ref), [`CTBase.Traits.StateDynamics`](@ref). +""" +Traits.dynamics_trait(::AbstractControlLaw{<:Traits.OpenLoopFeedback}) = Traits.StateDynamics + +""" +$(TYPEDSIGNATURES) + +Return the dynamics trait of a closed-loop control law, namely [`StateDynamics`](@ref). + +See also: [`CTBase.Traits.dynamics_trait`](@ref), [`CTBase.Traits.StateDynamics`](@ref). +""" +Traits.dynamics_trait(::AbstractControlLaw{<:Traits.ClosedLoopFeedback}) = Traits.StateDynamics + +""" +$(TYPEDSIGNATURES) + +Return the dynamics trait of a dynamic closed-loop control law, namely [`HamiltonianDynamics`](@ref). + +Dynamic closed-loop control laws depend on the costate, so they are associated +with Hamiltonian dynamics. + +See also: [`CTBase.Traits.dynamics_trait`](@ref), [`CTBase.Traits.HamiltonianDynamics`](@ref). +""" +Traits.dynamics_trait(::AbstractControlLaw{<:Traits.DynClosedLoopFeedback}) = Traits.HamiltonianDynamics + +# ============================================================================= +# Feedback predicates — dispatch on the FB type parameter +# ============================================================================= + +""" +$(TYPEDSIGNATURES) + +Return `true` if the control law is open-loop, `false` otherwise. + +See also: [`CTBase.Traits.OpenLoopFeedback`](@ref), [`CTBase.Traits.feedback`](@ref). +""" +Traits.is_open_loop(::AbstractControlLaw) = false +Traits.is_open_loop(::AbstractControlLaw{<:Traits.OpenLoopFeedback}) = true + +""" +$(TYPEDSIGNATURES) + +Return `true` if the control law is closed-loop, `false` otherwise. + +See also: [`CTBase.Traits.ClosedLoopFeedback`](@ref), [`CTBase.Traits.feedback`](@ref). +""" +Traits.is_closed_loop(::AbstractControlLaw) = false +Traits.is_closed_loop(::AbstractControlLaw{<:Traits.ClosedLoopFeedback}) = true + +""" +$(TYPEDSIGNATURES) + +Return `true` if the control law is dynamic closed-loop, `false` otherwise. + +See also: [`CTBase.Traits.DynClosedLoopFeedback`](@ref), [`CTBase.Traits.feedback`](@ref). +""" +Traits.is_dyn_closed_loop(::AbstractControlLaw) = false +Traits.is_dyn_closed_loop(::AbstractControlLaw{<:Traits.DynClosedLoopFeedback}) = true diff --git a/src/Data/abstract_pseudo_hamiltonian.jl b/src/Data/abstract_pseudo_hamiltonian.jl new file mode 100644 index 00000000..9a78de6a --- /dev/null +++ b/src/Data/abstract_pseudo_hamiltonian.jl @@ -0,0 +1,95 @@ +# ============================================================================= +# AbstractPseudoHamiltonian — abstract supertype for pseudo-Hamiltonian functions +# ============================================================================= + +""" +$(TYPEDEF) + +Abstract supertype for scalar pseudo-Hamiltonian functions together with their +time-dependence and variable-dependence traits. + +A pseudo-Hamiltonian is a scalar function `H̃(t, x, p, u[, v]) → ℝ` that extends +the standard Hamiltonian with an explicit control argument `u`. Unlike +[`AbstractHamiltonian`](@ref), which encodes the control implicitly, a +pseudo-Hamiltonian takes the control as an additional argument, enabling +dynamic closed-loop flows where the control is computed from the +pseudo-Hamiltonian's maximisation condition. + +# Type Parameters +- `TD <: TimeDependence`: `Autonomous` or `NonAutonomous`. +- `VD <: VariableDependence`: `Fixed` or `NonFixed`. + +# Notes +- All pseudo-Hamiltonian types support both natural and uniform call signatures. +- The uniform signature `(t, x, p, u, v)` is used internally by flow integrators. +- The dynamics trait is always `HamiltonianDynamics`, as pseudo-Hamiltonians + involve both state and costate. + +See also: [`CTBase.Data.PseudoHamiltonian`](@ref), [`CTBase.Data.AbstractHamiltonian`](@ref), +[`CTBase.Traits.TimeDependence`](@ref), [`CTBase.Traits.VariableDependence`](@ref). +""" +abstract type AbstractPseudoHamiltonian{ + TD<:Traits.TimeDependence,VD<:Traits.VariableDependence +} end + +# ============================================================================= +# Trait accessors for AbstractPseudoHamiltonian +# ============================================================================= + +""" +$(TYPEDSIGNATURES) + +Indicates that all `AbstractPseudoHamiltonian` types support time-dependence queries. + +See also: [`CTBase.Traits.time_dependence`](@ref), [`CTBase.Data.AbstractPseudoHamiltonian`](@ref). +""" +function Traits.has_time_dependence_trait(::AbstractPseudoHamiltonian) + return true +end + +""" +$(TYPEDSIGNATURES) + +Indicates that all `AbstractPseudoHamiltonian` types support variable-dependence queries. + +See also: [`CTBase.Traits.variable_dependence`](@ref), [`CTBase.Data.AbstractPseudoHamiltonian`](@ref). +""" +function Traits.has_variable_dependence_trait(::AbstractPseudoHamiltonian) + return true +end + +""" +$(TYPEDSIGNATURES) + +Return the time-dependence trait of a pseudo-Hamiltonian. + +See also: [`CTBase.Traits.time_dependence`](@ref), [`CTBase.Traits.TimeDependence`](@ref). +""" +function Traits.time_dependence( + ::AbstractPseudoHamiltonian{TD,<:Traits.VariableDependence} +) where {TD<:Traits.TimeDependence} + return TD +end + +""" +$(TYPEDSIGNATURES) + +Return the variable-dependence trait of a pseudo-Hamiltonian. + +See also: [`CTBase.Traits.variable_dependence`](@ref), [`CTBase.Traits.VariableDependence`](@ref). +""" +function Traits.variable_dependence( + ::AbstractPseudoHamiltonian{<:Traits.TimeDependence,VD} +) where {VD<:Traits.VariableDependence} + return VD +end + +""" +$(TYPEDSIGNATURES) + +Return the dynamics trait of an `AbstractPseudoHamiltonian`, namely +[`CTBase.Traits.HamiltonianDynamics`](@ref). + +See also: [`CTBase.Traits.dynamics_trait`](@ref), [`CTBase.Data.AbstractPseudoHamiltonian`](@ref). +""" +Traits.dynamics_trait(::AbstractPseudoHamiltonian) = Traits.HamiltonianDynamics diff --git a/src/Data/control_law.jl b/src/Data/control_law.jl new file mode 100644 index 00000000..17c96774 --- /dev/null +++ b/src/Data/control_law.jl @@ -0,0 +1,315 @@ +# ============================================================================= +# Concrete ControlLaw type +# ============================================================================= + +""" +$(TYPEDEF) + +Parametric container for a control law function together with its feedback, +time-dependence, and variable-dependence traits. + +The function provides the control input `u(...)` for an optimal control problem. +The feedback trait determines which arguments the control law depends on (see +[`AbstractControlLaw`](@ref)). + +# Type Parameters +- `F`: concrete type of the wrapped function. +- `FB <: AbstractFeedback`: `OpenLoopFeedback`, `ClosedLoopFeedback`, or `DynClosedLoopFeedback`. +- `TD <: TimeDependence`: `Autonomous` or `NonAutonomous`. +- `VD <: VariableDependence`: `Fixed` or `NonFixed`. + +# Fields +- `f::F`: the control law function. + +# Construction + +Use the user-facing constructors `OpenLoop`, `ClosedLoop`, or `DynClosedLoop`: + +```julia +OpenLoop(u; is_autonomous = true, is_variable = false) # default: u() +ClosedLoop(u; is_autonomous = true, is_variable = false) # default: u(x) +DynClosedLoop(u; is_autonomous = true, is_variable = false) # default: u(x, p) +``` + +# Call Signatures + +Every `ControlLaw` is callable via its **natural** signature (matching the +traits), and via a **uniform** signature that depends on the feedback trait: + +| Feedback | Natural `(Aut, Fixed)` | Uniform | +|---|---|---| +| `OpenLoop` | `u()` | `u(t, v)` | +| `ClosedLoop` | `u(x)` | `u(t, x, v)` | +| `DynClosedLoop` | `u(x, p)` | `u(t, x, p, v)` | + +The uniform signature is used by flow integrators. Unused arguments are ignored. + +See also: [`CTBase.Data.AbstractControlLaw`](@ref), [`CTBase.Data.OpenLoop`](@ref), +[`CTBase.Data.ClosedLoop`](@ref), [`CTBase.Data.DynClosedLoop`](@ref), +[`CTBase.Traits.AbstractFeedback`](@ref). +""" +struct ControlLaw{F<:Function,FB,TD,VD} <: AbstractControlLaw{FB,TD,VD} + f::F +end + +# ============================================================================= +# Internal keyword constructor — requires explicit feedback type +# ============================================================================= + +""" +$(TYPEDSIGNATURES) + +Internal constructor for `ControlLaw` with a specific feedback type and trait flags. + +# Arguments +- `f::Function`: The control law function. +- `::Type{FB}`: The feedback trait type. +- `is_autonomous::Bool`: If true, control law is autonomous (default: `__is_autonomous()`). +- `is_variable::Bool`: If true, control law depends on variable (default: `__is_variable()`). + +# Returns +- `ControlLaw`: A control law with appropriate traits. + +See also: [`CTBase.Data.ControlLaw`](@ref), [`CTBase.Data.OpenLoop`](@ref), +[`CTBase.Data.ClosedLoop`](@ref), [`CTBase.Data.DynClosedLoop`](@ref). +""" +function ControlLaw( + f, + ::Type{FB}; + is_autonomous::Bool=__is_autonomous(), + is_variable::Bool=__is_variable(), +) where {FB<:Traits.AbstractFeedback} + TD = is_autonomous ? Traits.Autonomous : Traits.NonAutonomous + VD = is_variable ? Traits.NonFixed : Traits.Fixed + return ControlLaw{typeof(f),FB,TD,VD}(f) +end + +# ============================================================================= +# Typed constructor — trait types passed positionally +# ============================================================================= + +""" +$(TYPEDSIGNATURES) + +Typed constructor for `ControlLaw` with explicit trait types. + +# Arguments +- `f`: The control law function. +- `::Type{FB}`: The feedback trait type. +- `::Type{TD}`: The time-dependence trait type. +- `::Type{VD}`: The variable-dependence trait type. + +# Returns +- `ControlLaw`: A control law with the specified traits. + +See also: [`CTBase.Data.ControlLaw`](@ref). +""" +function ControlLaw( + f, + ::Type{FB}, + ::Type{TD}, + ::Type{VD}, +) where { + FB<:Traits.AbstractFeedback, + TD<:Traits.TimeDependence, + VD<:Traits.VariableDependence, +} + return ControlLaw{typeof(f),FB,TD,VD}(f) +end + +# ============================================================================= +# User-facing constructors +# ============================================================================= + +""" +$(TYPEDSIGNATURES) + +Construct an open-loop `ControlLaw`. + +An open-loop control law depends only on time (and optionally the variable), +not on the state or costate. + +# Arguments +- `f::Function`: The control law function. +- `is_autonomous::Bool`: If true, control law is autonomous (default: `__is_autonomous()`). +- `is_variable::Bool`: If true, control law depends on variable (default: `__is_variable()`). + +# Example +```julia-repl +julia> using CTBase.Data + +julia> u = OpenLoop(() -> 1.0) +ControlLaw: open-loop, autonomous, fixed (no variable) + natural call: u() + uniform call: u(t, v) + +julia> u = OpenLoop((t, v) -> t * v; is_autonomous=false, is_variable=true) +ControlLaw: open-loop, non-autonomous, variable + natural call: u(t, v) + uniform call: u(t, v) +``` + +See also: [`CTBase.Data.ClosedLoop`](@ref), [`CTBase.Data.DynClosedLoop`](@ref), +[`CTBase.Data.ControlLaw`](@ref). +""" +function OpenLoop(f; is_autonomous::Bool=__is_autonomous(), is_variable::Bool=__is_variable()) + return ControlLaw(f, Traits.OpenLoopFeedback; is_autonomous=is_autonomous, is_variable=is_variable) +end + +""" +$(TYPEDSIGNATURES) + +Construct a closed-loop `ControlLaw`. + +A closed-loop control law depends on the state (and optionally time and +variable), but not on the costate. + +# Arguments +- `f::Function`: The control law function. +- `is_autonomous::Bool`: If true, control law is autonomous (default: `__is_autonomous()`). +- `is_variable::Bool`: If true, control law depends on variable (default: `__is_variable()`). + +# Example +```julia-repl +julia> using CTBase.Data + +julia> u = ClosedLoop(x -> -x) +ControlLaw: closed-loop, autonomous, fixed (no variable) + natural call: u(x) + uniform call: u(t, x, v) +``` + +See also: [`CTBase.Data.OpenLoop`](@ref), [`CTBase.Data.DynClosedLoop`](@ref), +[`CTBase.Data.ControlLaw`](@ref). +""" +function ClosedLoop(f; is_autonomous::Bool=__is_autonomous(), is_variable::Bool=__is_variable()) + return ControlLaw(f, Traits.ClosedLoopFeedback; is_autonomous=is_autonomous, is_variable=is_variable) +end + +""" +$(TYPEDSIGNATURES) + +Construct a dynamic closed-loop `ControlLaw`. + +A dynamic closed-loop control law depends on both the state and the costate +(and optionally time and variable). + +# Arguments +- `f::Function`: The control law function. +- `is_autonomous::Bool`: If true, control law is autonomous (default: `__is_autonomous()`). +- `is_variable::Bool`: If true, control law depends on variable (default: `__is_variable()`). + +# Example +```julia-repl +julia> using CTBase.Data + +julia> u = DynClosedLoop((x, p) -> -x - p) +ControlLaw: dyn-closed-loop, autonomous, fixed (no variable) + natural call: u(x, p) + uniform call: u(t, x, p, v) +``` + +See also: [`CTBase.Data.OpenLoop`](@ref), [`CTBase.Data.ClosedLoop`](@ref), +[`CTBase.Data.ControlLaw`](@ref). +""" +function DynClosedLoop(f; is_autonomous::Bool=__is_autonomous(), is_variable::Bool=__is_variable()) + return ControlLaw(f, Traits.DynClosedLoopFeedback; is_autonomous=is_autonomous, is_variable=is_variable) +end + +# ============================================================================= +# Natural call signatures — OpenLoop +# ============================================================================= + +(cl::ControlLaw{<:Function,Traits.OpenLoopFeedback,Traits.Autonomous,Traits.Fixed})() = cl.f() +(cl::ControlLaw{<:Function,Traits.OpenLoopFeedback,Traits.NonAutonomous,Traits.Fixed})(t) = cl.f(t) +(cl::ControlLaw{<:Function,Traits.OpenLoopFeedback,Traits.Autonomous,Traits.NonFixed})(v) = cl.f(v) +function (cl::ControlLaw{<:Function,Traits.OpenLoopFeedback,Traits.NonAutonomous,Traits.NonFixed})(t, v) + return cl.f(t, v) +end + +# ============================================================================= +# Natural call signatures — ClosedLoop +# ============================================================================= + +(cl::ControlLaw{<:Function,Traits.ClosedLoopFeedback,Traits.Autonomous,Traits.Fixed})(x) = cl.f(x) +(cl::ControlLaw{<:Function,Traits.ClosedLoopFeedback,Traits.NonAutonomous,Traits.Fixed})(t, x) = cl.f(t, x) +(cl::ControlLaw{<:Function,Traits.ClosedLoopFeedback,Traits.Autonomous,Traits.NonFixed})(x, v) = cl.f(x, v) +function (cl::ControlLaw{<:Function,Traits.ClosedLoopFeedback,Traits.NonAutonomous,Traits.NonFixed})(t, x, v) + return cl.f(t, x, v) +end + +# ============================================================================= +# Natural call signatures — DynClosedLoop +# ============================================================================= + +(cl::ControlLaw{<:Function,Traits.DynClosedLoopFeedback,Traits.Autonomous,Traits.Fixed})(x, p) = cl.f(x, p) +(cl::ControlLaw{<:Function,Traits.DynClosedLoopFeedback,Traits.NonAutonomous,Traits.Fixed})(t, x, p) = cl.f(t, x, p) +function (cl::ControlLaw{<:Function,Traits.DynClosedLoopFeedback,Traits.Autonomous,Traits.NonFixed})(x, p, v) + return cl.f(x, p, v) +end +function (cl::ControlLaw{<:Function,Traits.DynClosedLoopFeedback,Traits.NonAutonomous,Traits.NonFixed})(t, x, p, v) + return cl.f(t, x, p, v) +end + +# ============================================================================= +# Uniform call — used by flow integrators +# OpenLoop: u(t, v) — no state, no costate (open-loop by definition) +# ClosedLoop: u(t, x, v) — state but no costate (controlled dynamics flow) +# DynClosedLoop: u(t, x, p, v) — costate needed (Hamiltonian flow) +# (NonAutonomous, NonFixed) is already covered by the natural signature above. +# ============================================================================= + +# OpenLoop — uniform (t, v), no state (open-loop by definition) +(cl::ControlLaw{<:Function,Traits.OpenLoopFeedback,Traits.Autonomous,Traits.Fixed})(t, v) = cl.f() +(cl::ControlLaw{<:Function,Traits.OpenLoopFeedback,Traits.NonAutonomous,Traits.Fixed})(t, v) = cl.f(t) +(cl::ControlLaw{<:Function,Traits.OpenLoopFeedback,Traits.Autonomous,Traits.NonFixed})(t, v) = cl.f(v) +# NonAutonomous, NonFixed — already covered by natural 2-arg + +# ClosedLoop — uniform (t, x, v) +(cl::ControlLaw{<:Function,Traits.ClosedLoopFeedback,Traits.Autonomous,Traits.Fixed})(t, x, v) = cl.f(x) +(cl::ControlLaw{<:Function,Traits.ClosedLoopFeedback,Traits.NonAutonomous,Traits.Fixed})(t, x, v) = cl.f(t, x) +(cl::ControlLaw{<:Function,Traits.ClosedLoopFeedback,Traits.Autonomous,Traits.NonFixed})(t, x, v) = cl.f(x, v) +# NonAutonomous, NonFixed — already covered by natural 3-arg + +# DynClosedLoop — uniform (t, x, p, v) +(cl::ControlLaw{<:Function,Traits.DynClosedLoopFeedback,Traits.Autonomous,Traits.Fixed})(t, x, p, v) = cl.f(x, p) +(cl::ControlLaw{<:Function,Traits.DynClosedLoopFeedback,Traits.NonAutonomous,Traits.Fixed})(t, x, p, v) = cl.f(t, x, p) +(cl::ControlLaw{<:Function,Traits.DynClosedLoopFeedback,Traits.Autonomous,Traits.NonFixed})(t, x, p, v) = cl.f(x, p, v) +# NonAutonomous, NonFixed — already covered by natural 4-arg + +# ============================================================================= +# Base.show +# ============================================================================= + +""" +$(TYPEDSIGNATURES) + +Display a compact representation of a `ControlLaw` showing its traits and call signatures. + +# Output +Displays three lines: +- Header with feedback, time, and variable dependence traits +- Natural call signature +- Uniform call signature + +See also: [`CTBase.Data.ControlLaw`](@ref). +""" +function Base.show(io::IO, ::ControlLaw{F,FB,TD,VD}) where {F,FB,TD,VD} + header = "ControlLaw: $(_fb_label(FB)), $(_td_label(TD)), $(_vd_label(VD))" + natural = _natural_sig_cl(FB, TD, VD) + uniform = _uniform_sig_cl(FB) + println(io, header) + println(io, " natural call: ", natural) + return print(io, " uniform call: ", uniform) +end + +""" +$(TYPEDSIGNATURES) + +Display a `ControlLaw` in the REPL with the same format as the compact `show`. + +See also: [`CTBase.Data.ControlLaw`](@ref). +""" +function Base.show(io::IO, ::MIME"text/plain", cl::ControlLaw{F,FB,TD,VD}) where {F,FB,TD,VD} + return show(io, cl) +end diff --git a/src/Data/helpers.jl b/src/Data/helpers.jl index c1159668..afe33d0c 100644 --- a/src/Data/helpers.jl +++ b/src/Data/helpers.jl @@ -256,3 +256,139 @@ See also: [`CTBase.Data._natural_sig_h`](@ref). function _uniform_sig_h() return "h(t, x, p, v)" end + +# ============================================================================= +# Feedback label helpers +# ============================================================================= + +""" + _fb_label(::Type{Traits.OpenLoopFeedback}) -> String + _fb_label(::Type{Traits.ClosedLoopFeedback}) -> String + _fb_label(::Type{Traits.DynClosedLoopFeedback}) -> String + +Return a user-friendly label for feedback traits. + +# Returns +- `String`: "open-loop", "closed-loop", or "dyn-closed-loop". + +See also: [`CTBase.Data._td_label`](@ref), [`CTBase.Data._vd_label`](@ref). +""" +function _fb_label(::Type{Traits.OpenLoopFeedback}) + return "open-loop" +end +function _fb_label(::Type{Traits.ClosedLoopFeedback}) + return "closed-loop" +end +function _fb_label(::Type{Traits.DynClosedLoopFeedback}) + return "dyn-closed-loop" +end + +# ============================================================================= +# ControlLaw-specific signature helpers +# ============================================================================= + +""" + _natural_sig_cl(::Type{FB}, ::Type{TD}, ::Type{VD}) where {FB, TD, VD} -> String + +Return the natural call signature for a `ControlLaw` based on its traits. + +The arguments depend on the feedback type: +- `OpenLoop`: `u([t][, v])` +- `ClosedLoop`: `u([t, ]x[, v])` +- `DynClosedLoop`: `u([t, ]x, p[, v])` + +See also: [`CTBase.Data._uniform_sig_cl`](@ref). +""" +function _natural_sig_cl( + ::Type{FB}, ::Type{TD}, ::Type{VD} +) where {FB<:Traits.AbstractFeedback,TD<:Traits.TimeDependence,VD<:Traits.VariableDependence} + args = String[] + TD === Traits.NonAutonomous && push!(args, "t") + append!(args, _natural_args_cl(FB)) + VD === Traits.NonFixed && push!(args, "v") + return "u(" * join(args, ", ") * ")" +end + +""" + _natural_args_cl(::Type{FB}) where {FB} -> Vector{String} + +Return the feedback-dependent argument names (excluding `t` and `v`) for the +natural call signature of a `ControlLaw`. + +- `OpenLoopFeedback`: empty (no state or costate). +- `ClosedLoopFeedback`: `["x"]`. +- `DynClosedLoopFeedback`: `["x", "p"]`. + +See also: [`CTBase.Data._natural_sig_cl`](@ref). +""" +function _natural_args_cl(::Type{Traits.OpenLoopFeedback}) + return String[] +end + +function _natural_args_cl(::Type{Traits.ClosedLoopFeedback}) + return ["x"] +end + +function _natural_args_cl(::Type{Traits.DynClosedLoopFeedback}) + return ["x", "p"] +end + +""" + _uniform_sig_cl(::Type{FB}) where {FB} -> String + +Return the uniform call signature for a `ControlLaw` based on its feedback trait. + +- OpenLoop: `u(t, v)` — no state, no costate (open-loop by definition) +- ClosedLoop: `u(t, x, v)` — state but no costate (controlled dynamics flow) +- DynClosedLoop: `u(t, x, p, v)` — costate needed (Hamiltonian flow) + +See also: [`CTBase.Data._natural_sig_cl`](@ref). +""" +function _uniform_sig_cl(::Type{Traits.OpenLoopFeedback}) + return "u(t, v)" +end + +function _uniform_sig_cl(::Type{Traits.ClosedLoopFeedback}) + return "u(t, x, v)" +end + +function _uniform_sig_cl(::Type{Traits.DynClosedLoopFeedback}) + return "u(t, x, p, v)" +end + +# ============================================================================= +# PseudoHamiltonian-specific signature helpers +# ============================================================================= + +""" + _natural_sig_ph(::Type{TD}, ::Type{VD}) where {TD, VD} -> String + +Return the natural call signature for a `PseudoHamiltonian` based on its traits. + +The arguments always include `x, p, u`, with `t` prepended for non-autonomous +and `v` appended for variable. + +See also: [`CTBase.Data._uniform_sig_ph`](@ref). +""" +function _natural_sig_ph(::Type{TD}, ::Type{VD}) where {TD,VD} + args = String[] + TD === Traits.NonAutonomous && push!(args, "t") + push!(args, "x") + push!(args, "p") + push!(args, "u") + VD === Traits.NonFixed && push!(args, "v") + return "h̃(" * join(args, ", ") * ")" +end + +""" + _uniform_sig_ph() -> String + +Return the uniform call signature for a `PseudoHamiltonian`. + +The uniform signature always includes all arguments `(t, x, p, u, v)` regardless of traits. + +See also: [`CTBase.Data._natural_sig_ph`](@ref). +""" +function _uniform_sig_ph() + return "h̃(t, x, p, u, v)" +end diff --git a/src/Data/pseudo_hamiltonian.jl b/src/Data/pseudo_hamiltonian.jl new file mode 100644 index 00000000..4144caba --- /dev/null +++ b/src/Data/pseudo_hamiltonian.jl @@ -0,0 +1,163 @@ +# ============================================================================= +# Concrete PseudoHamiltonian type +# ============================================================================= + +""" +$(TYPEDEF) + +Parametric container for a scalar pseudo-Hamiltonian function together with its +time-dependence and variable-dependence traits. + +The function returns a scalar value `H̃(t, x, p, u[, v]) → ℝ` representing the +pseudo-Hamiltonian of the system, which extends the standard Hamiltonian with +an explicit control argument `u`. + +# Type Parameters +- `F`: concrete type of the wrapped function. +- `TD <: TimeDependence`: `Autonomous` or `NonAutonomous`. +- `VD <: VariableDependence`: `Fixed` or `NonFixed`. + +# Fields +- `f::F`: the pseudo-Hamiltonian function. + +# Construction + +Use the keyword constructor: + +```julia +PseudoHamiltonian(f; is_autonomous = true, is_variable = false) # default: h̃(x, p, u) +PseudoHamiltonian((t, x, p, u) -> ...; is_autonomous = false) # h̃(t, x, p, u) +PseudoHamiltonian((x, p, u, v) -> ...; is_variable = true) # h̃(x, p, u, v) +PseudoHamiltonian((t, x, p, u, v) -> ...; is_autonomous = false, is_variable = true) +``` + +# Call Signatures + +Every `PseudoHamiltonian` is callable via its **natural** signature (matching +the traits), and via a **uniform** signature `(t, x, p, u, v)` that ignores +unused arguments. + +For Autonomous/Fixed: natural `h̃(x, p, u)`, uniform `h̃(t, x, p, u, v)`. +For NonAutonomous/Fixed: natural `h̃(t, x, p, u)`, uniform `h̃(t, x, p, u, v)`. +For Autonomous/NonFixed: natural `h̃(x, p, u, v)`, uniform `h̃(t, x, p, u, v)`. +For NonAutonomous/NonFixed: natural `h̃(t, x, p, u, v)`, uniform `h̃(t, x, p, u, v)`. + +See also: [`CTBase.Data.AbstractPseudoHamiltonian`](@ref), [`CTBase.Data.Hamiltonian`](@ref), +[`CTBase.Traits.TimeDependence`](@ref), [`CTBase.Traits.VariableDependence`](@ref). +""" +struct PseudoHamiltonian{F<:Function,TD,VD} <: AbstractPseudoHamiltonian{TD,VD} + f::F +end + +# ============================================================================= +# Keyword constructor +# ============================================================================= + +""" +$(TYPEDSIGNATURES) + +Construct a `PseudoHamiltonian` with trait flags. + +# Arguments +- `f::Function`: The pseudo-Hamiltonian function returning a scalar value. +- `is_autonomous::Bool`: If true, system is autonomous (default: `__is_autonomous()`). +- `is_variable::Bool`: If true, system depends on variable parameters (default: `__is_variable()`). + +# Example +```julia-repl +julia> using CTBase.Data + +julia> h̃ = PseudoHamiltonian((x, p, u) -> sum(x .* p) + u^2) +PseudoHamiltonian: autonomous, fixed (no variable) + natural call: h̃(x, p, u) + uniform call: h̃(t, x, p, u, v) +``` + +See also: [`CTBase.Data.PseudoHamiltonian`](@ref), [`CTBase.Traits.Autonomous`](@ref), +[`CTBase.Traits.NonAutonomous`](@ref), [`CTBase.Traits.Fixed`](@ref), [`CTBase.Traits.NonFixed`](@ref). +""" +function PseudoHamiltonian( + f; is_autonomous::Bool=__is_autonomous(), is_variable::Bool=__is_variable() +) + TD = is_autonomous ? Traits.Autonomous : Traits.NonAutonomous + VD = is_variable ? Traits.NonFixed : Traits.Fixed + return PseudoHamiltonian{typeof(f),TD,VD}(f) +end + +# ============================================================================= +# Typed constructor +# ============================================================================= + +""" +$(TYPEDSIGNATURES) + +Typed constructor for `PseudoHamiltonian` with explicit trait types. + +See also: [`CTBase.Data.PseudoHamiltonian`](@ref). +""" +function PseudoHamiltonian( + f, ::Type{TD}, ::Type{VD} +) where {TD<:Traits.TimeDependence,VD<:Traits.VariableDependence} + return PseudoHamiltonian{typeof(f),TD,VD}(f) +end + +# ============================================================================= +# Natural call signatures — one per trait combination +# ============================================================================= + +(h̃::PseudoHamiltonian{<:Function,Traits.Autonomous,Traits.Fixed})(x, p, u) = h̃.f(x, p, u) +function (h̃::PseudoHamiltonian{<:Function,Traits.NonAutonomous,Traits.Fixed})(t, x, p, u) + return h̃.f(t, x, p, u) +end +function (h̃::PseudoHamiltonian{<:Function,Traits.Autonomous,Traits.NonFixed})(x, p, u, v) + return h̃.f(x, p, u, v) +end +function (h̃::PseudoHamiltonian{<:Function,Traits.NonAutonomous,Traits.NonFixed})(t, x, p, u, v) + return h̃.f(t, x, p, u, v) +end + +# ============================================================================= +# Uniform (t, x, p, u, v) call — used by flow integrators +# Every combination forwards to its natural call, ignoring unused args. +# (NonAutonomous, NonFixed) is already covered by the natural signature above. +# ============================================================================= + +(h̃::PseudoHamiltonian{<:Function,Traits.Autonomous,Traits.Fixed})(t, x, p, u, v) = h̃.f(x, p, u) +function (h̃::PseudoHamiltonian{<:Function,Traits.NonAutonomous,Traits.Fixed})(t, x, p, u, v) + return h̃.f(t, x, p, u) +end +function (h̃::PseudoHamiltonian{<:Function,Traits.Autonomous,Traits.NonFixed})(t, x, p, u, v) + return h̃.f(x, p, u, v) +end +# NonAutonomous, NonFixed — already covered by natural 5-arg + +# ============================================================================= +# Base.show +# ============================================================================= + +""" +$(TYPEDSIGNATURES) + +Display a compact representation of a `PseudoHamiltonian` showing its traits and call signatures. + +See also: [`CTBase.Data.PseudoHamiltonian`](@ref). +""" +function Base.show(io::IO, ::PseudoHamiltonian{F,TD,VD}) where {F,TD,VD} + header = "PseudoHamiltonian: $(_td_label(TD)), $(_vd_label(VD))" + natural = _natural_sig_ph(TD, VD) + uniform = _uniform_sig_ph() + println(io, header) + println(io, " natural call: ", natural) + return print(io, " uniform call: ", uniform) +end + +""" +$(TYPEDSIGNATURES) + +Display a `PseudoHamiltonian` in the REPL with the same format as the compact `show`. + +See also: [`CTBase.Data.PseudoHamiltonian`](@ref). +""" +function Base.show(io::IO, ::MIME"text/plain", h̃::PseudoHamiltonian{F,TD,VD}) where {F,TD,VD} + return show(io, h̃) +end diff --git a/src/Differentiation/Differentiation.jl b/src/Differentiation/Differentiation.jl index 995926f8..3fa0c5e0 100644 --- a/src/Differentiation/Differentiation.jl +++ b/src/Differentiation/Differentiation.jl @@ -77,6 +77,8 @@ export DifferentiationInterface export build_ad_backend export ad_backend export hamiltonian_gradient +export pseudo_hamiltonian_gradient +export pseudo_hamiltonian_control_gradient export variable_gradient export gradient export derivative diff --git a/src/Differentiation/abstract_ad_backend.jl b/src/Differentiation/abstract_ad_backend.jl index 9ae9c403..6ca4e9f0 100644 --- a/src/Differentiation/abstract_ad_backend.jl +++ b/src/Differentiation/abstract_ad_backend.jl @@ -104,6 +104,89 @@ end """ $(TYPEDSIGNATURES) +Compute the pseudo-Hamiltonian gradient (∂H̃/∂x, ∂H̃/∂p) using the backend. + +Along a PMP solution, the stationarity condition ∂H̃/∂u = 0 holds, so the +Hamiltonian flow only requires ∂H̃/∂x and ∂H̃/∂p. Use +[`pseudo_hamiltonian_control_gradient`](@ref) to compute ∂H̃/∂u separately +(e.g. for checking the stationarity condition). + +# Arguments +- `backend::AbstractADBackend`: The AD backend. +- `h̃`: The pseudo-Hamiltonian function or type. +- `t`: Time (scalar). +- `x`: State vector. +- `p`: Costate vector. +- `u`: Control (scalar or vector). +- `v`: Variable (scalar or `nothing` for Fixed problems). + +# Returns +- `(∂H̃_∂x, ∂H̃_∂p)`: Tuple of partial derivatives, **non-negated**. The RHS + closure is responsible for applying the signs (ṗ = -∂H̃/∂x). + +# Throws +- `CTBase.Exceptions.NotImplemented`: If the concrete backend does not implement this method. + +See also: [`CTBase.Differentiation.pseudo_hamiltonian_control_gradient`](@ref), +[`CTBase.Differentiation.hamiltonian_gradient`](@ref), +[`CTBase.Differentiation.variable_gradient`](@ref). +""" +function pseudo_hamiltonian_gradient( + backend::AbstractADBackend, h̃::Data.AbstractPseudoHamiltonian, t, x, p, u, v +) + return throw( + Exceptions.NotImplemented( + "pseudo_hamiltonian_gradient not implemented for $(typeof(backend))"; + required_method="pseudo_hamiltonian_gradient(backend::$(typeof(backend)), h̃, t, x, p, u, v)", + suggestion="Implement pseudo_hamiltonian_gradient for $(typeof(backend)) or load an extension that provides gradient computation (e.g., CTBaseDifferentiationInterface)", + context="AD backend contract", + ), + ) +end + +""" +$(TYPEDSIGNATURES) + +Compute the pseudo-Hamiltonian control gradient ∂H̃/∂u using the backend. + +This is typically used to check the PMP stationarity condition ∂H̃/∂u = 0, +not for the Hamiltonian flow itself (which only needs ∂H̃/∂x and ∂H̃/∂p; +see [`pseudo_hamiltonian_gradient`](@ref)). + +# Arguments +- `backend::AbstractADBackend`: The AD backend. +- `h̃`: The pseudo-Hamiltonian function or type. +- `t`: Time (scalar). +- `x`: State vector. +- `p`: Costate vector. +- `u`: Control (scalar or vector). +- `v`: Variable (scalar or `nothing` for Fixed problems). + +# Returns +- `∂H̃_∂u`: The partial derivative with respect to the control. + +# Throws +- `CTBase.Exceptions.NotImplemented`: If the concrete backend does not implement this method. + +See also: [`CTBase.Differentiation.pseudo_hamiltonian_gradient`](@ref), +[`CTBase.Differentiation.variable_gradient`](@ref). +""" +function pseudo_hamiltonian_control_gradient( + backend::AbstractADBackend, h̃::Data.AbstractPseudoHamiltonian, t, x, p, u, v +) + return throw( + Exceptions.NotImplemented( + "pseudo_hamiltonian_control_gradient not implemented for $(typeof(backend))"; + required_method="pseudo_hamiltonian_control_gradient(backend::$(typeof(backend)), h̃, t, x, p, u, v)", + suggestion="Implement pseudo_hamiltonian_control_gradient for $(typeof(backend)) or load an extension that provides gradient computation (e.g., CTBaseDifferentiationInterface)", + context="AD backend contract", + ), + ) +end + +""" +$(TYPEDSIGNATURES) + Extract the AD backend from a backend strategy. # Arguments diff --git a/src/Traits/Traits.jl b/src/Traits/Traits.jl index 92fc4537..db457229 100644 --- a/src/Traits/Traits.jl +++ b/src/Traits/Traits.jl @@ -13,6 +13,7 @@ no runtime cost. - **time_dependence.jl**: Time-dependence traits and the opt-in contract - **variable_dependence.jl**: Variable-dependence traits and the opt-in contract - **control_dependence.jl**: Control-dependence traits and the opt-in contract +- **feedback.jl**: Feedback traits (`OpenLoopFeedback`, `ClosedLoopFeedback`, `DynClosedLoopFeedback`) - **mutability.jl**: Mutability traits and the opt-in contract - **mode.jl**: Integration-mode traits (`EndPointMode`, `TrajectoryMode`) - **dynamics.jl**: Dynamics-type traits (`StateDynamics`, `HamiltonianDynamics`, `AugmentedHamiltonianDynamics`) @@ -27,6 +28,7 @@ no runtime cost. - **Time dependence**: `TimeDependence`, `Autonomous`, `NonAutonomous` - **Variable dependence**: `VariableDependence`, `Fixed`, `NonFixed` - **Control dependence**: `ControlDependence`, `ControlFree`, `WithControl` +- **Feedback**: `AbstractFeedback`, `OpenLoopFeedback`, `ClosedLoopFeedback`, `DynClosedLoopFeedback` - **Mutability**: `InPlace`, `OutOfPlace` - **Integration mode**: `EndPointMode`, `TrajectoryMode` - **Dynamics**: `StateDynamics`, `HamiltonianDynamics`, `AugmentedHamiltonianDynamics` @@ -83,6 +85,7 @@ include(joinpath(@__DIR__, "mutability.jl")) include(joinpath(@__DIR__, "time_dependence.jl")) include(joinpath(@__DIR__, "variable_dependence.jl")) include(joinpath(@__DIR__, "control_dependence.jl")) +include(joinpath(@__DIR__, "feedback.jl")) # ============================================================================== # Module exports @@ -101,7 +104,9 @@ export WithAD, WithoutAD export SupportsVariableCostate, NoVariableCostate export VariableDependence, Fixed, NonFixed export ControlDependence, ControlFree, WithControl -export ad_trait, variable_costate_trait, dynamics_trait +export AbstractFeedback, OpenLoopFeedback, ClosedLoopFeedback, DynClosedLoopFeedback +export ad_trait, variable_costate_trait, dynamics_trait, feedback +export is_open_loop, is_closed_loop, is_dyn_closed_loop export is_inplace, is_outofplace export is_autonomous, is_nonautonomous, is_variable, is_nonvariable, has_variable export is_control_free, has_control diff --git a/src/Traits/feedback.jl b/src/Traits/feedback.jl new file mode 100644 index 00000000..252cde4f --- /dev/null +++ b/src/Traits/feedback.jl @@ -0,0 +1,132 @@ +""" +$(TYPEDEF) + +Abstract base type for feedback traits. + +Feedback traits encode, at the type level, the kind of control law used to close +the loop in an optimal control flow. They distinguish between open-loop control +(time-only dependence), closed-loop control (state dependence), and dynamic +closed-loop control (state and costate dependence). + +# Trait Pattern + +This trait follows the **type-parameter-only** contract (like +[`AbstractDynamicsTrait`](@ref)): the trait value is read from a type parameter +of the concrete data type (e.g. `ControlLaw{F,FB,TD,VD}`) by the `feedback` +accessor. No `has_feedback_trait` guard is provided; calling `feedback` on a +type that does not implement it yields a standard `MethodError`. + +# See also + +- [`CTBase.Traits.OpenLoopFeedback`](@ref) +- [`CTBase.Traits.ClosedLoopFeedback`](@ref) +- [`CTBase.Traits.DynClosedLoopFeedback`](@ref) +- [`CTBase.Traits.feedback`](@ref) +""" +abstract type AbstractFeedback <: AbstractTrait end + +""" +$(TYPEDEF) + +Trait indicating open-loop feedback: the control depends only on time (and +optionally the variable), not on the state or costate. + +An open-loop control law has the form `u(t)` (or `u(t, v)` for variable +problems). The trajectory is determined entirely by the pre-specified control +function, without feedback from the current state. + +# See also + +- [`CTBase.Traits.ClosedLoopFeedback`](@ref) +- [`CTBase.Traits.DynClosedLoopFeedback`](@ref) +- [`CTBase.Traits.AbstractFeedback`](@ref) +""" +struct OpenLoopFeedback <: AbstractFeedback end + +""" +$(TYPEDEF) + +Trait indicating closed-loop feedback: the control depends on the state (and +optionally time and variable), but not on the costate. + +A closed-loop control law has the form `u(t, x)` (or `u(t, x, v)` for variable +problems). The control is a function of the current state, providing static +state feedback without costate information. + +# See also + +- [`CTBase.Traits.OpenLoopFeedback`](@ref) +- [`CTBase.Traits.DynClosedLoopFeedback`](@ref) +- [`CTBase.Traits.AbstractFeedback`](@ref) +""" +struct ClosedLoopFeedback <: AbstractFeedback end + +""" +$(TYPEDEF) + +Trait indicating dynamic closed-loop feedback: the control depends on both the +state and the costate (and optionally time and variable). + +A dynamic closed-loop control law has the form `u(t, x, p)` (or `u(t, x, p, v)` +for variable problems). The control is a function of the full Hamiltonian state, +providing dynamic feedback that uses costate information — typically derived +from the pseudo-Hamiltonian maximisation condition. + +# See also + +- [`CTBase.Traits.OpenLoopFeedback`](@ref) +- [`CTBase.Traits.ClosedLoopFeedback`](@ref) +- [`CTBase.Traits.AbstractFeedback`](@ref) +""" +struct DynClosedLoopFeedback <: AbstractFeedback end + +""" +$(TYPEDSIGNATURES) + +Return the feedback trait of `x`. + +Methods are defined on concrete types in `Data` (e.g. `AbstractControlLaw`). +The trait value is one of [`OpenLoopFeedback`](@ref), [`ClosedLoopFeedback`](@ref), +or [`DynClosedLoopFeedback`](@ref). + +# See also + +- [`CTBase.Traits.AbstractFeedback`](@ref) +- [`CTBase.Traits.OpenLoopFeedback`](@ref) +- [`CTBase.Traits.ClosedLoopFeedback`](@ref) +- [`CTBase.Traits.DynClosedLoopFeedback`](@ref) +""" +function feedback end + +""" +$(TYPEDSIGNATURES) + +Return `true` if `x` has open-loop feedback. + +Methods are defined on concrete types in `Data` (e.g. `AbstractControlLaw`). + +See also: [`CTBase.Traits.OpenLoopFeedback`](@ref), [`CTBase.Traits.feedback`](@ref). +""" +function is_open_loop end + +""" +$(TYPEDSIGNATURES) + +Return `true` if `x` has closed-loop feedback. + +Methods are defined on concrete types in `Data` (e.g. `AbstractControlLaw`). + +See also: [`CTBase.Traits.ClosedLoopFeedback`](@ref), [`CTBase.Traits.feedback`](@ref). +""" +function is_closed_loop end + +""" +$(TYPEDSIGNATURES) + +Return `true` if `x` has dynamic closed-loop feedback. + +Methods are defined on concrete types in `Data` (e.g. `AbstractControlLaw`). + +See also: [`CTBase.Traits.DynClosedLoopFeedback`](@ref), [`CTBase.Traits.feedback`](@ref). +""" +function is_dyn_closed_loop end diff --git a/test/suite/data/test_control_law.jl b/test/suite/data/test_control_law.jl new file mode 100644 index 00000000..23a54726 --- /dev/null +++ b/test/suite/data/test_control_law.jl @@ -0,0 +1,313 @@ +module TestControlLaw + +using Test: Test +import CTBase.Data: Data, OpenLoop, ClosedLoop, DynClosedLoop +import CTBase.Traits + +const VERBOSE = isdefined(Main, :TestData) ? Main.TestData.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestData) ? Main.TestData.SHOWTIMING : true + +function test_control_law() + Test.@testset "ControlLaw Tests" verbose=VERBOSE showtiming=SHOWTIMING begin + + # ==================================================================== + # UNIT TESTS - Construction with all trait combinations + # ==================================================================== + + Test.@testset "Unit: Construction with all trait combinations" begin + # OpenLoop, Autonomous, Fixed + cl1 = OpenLoop(() -> 1.0; is_autonomous=true, is_variable=false) + Test.@test cl1 isa Data.ControlLaw + Test.@test Traits.feedback(cl1) == Traits.OpenLoopFeedback + Test.@test Traits.time_dependence(cl1) == Traits.Autonomous + Test.@test Traits.variable_dependence(cl1) == Traits.Fixed + + # OpenLoop, NonAutonomous, Fixed + cl2 = OpenLoop((t) -> t; is_autonomous=false, is_variable=false) + Test.@test cl2 isa Data.ControlLaw + Test.@test Traits.feedback(cl2) == Traits.OpenLoopFeedback + Test.@test Traits.time_dependence(cl2) == Traits.NonAutonomous + Test.@test Traits.variable_dependence(cl2) == Traits.Fixed + + # OpenLoop, Autonomous, NonFixed + cl3 = OpenLoop((v) -> v; is_autonomous=true, is_variable=true) + Test.@test cl3 isa Data.ControlLaw + Test.@test Traits.feedback(cl3) == Traits.OpenLoopFeedback + Test.@test Traits.time_dependence(cl3) == Traits.Autonomous + Test.@test Traits.variable_dependence(cl3) == Traits.NonFixed + + # OpenLoop, NonAutonomous, NonFixed + cl4 = OpenLoop((t, v) -> t + v; is_autonomous=false, is_variable=true) + Test.@test cl4 isa Data.ControlLaw + Test.@test Traits.feedback(cl4) == Traits.OpenLoopFeedback + Test.@test Traits.time_dependence(cl4) == Traits.NonAutonomous + Test.@test Traits.variable_dependence(cl4) == Traits.NonFixed + + # ClosedLoop, Autonomous, Fixed + cl5 = ClosedLoop((x) -> x; is_autonomous=true, is_variable=false) + Test.@test cl5 isa Data.ControlLaw + Test.@test Traits.feedback(cl5) == Traits.ClosedLoopFeedback + Test.@test Traits.time_dependence(cl5) == Traits.Autonomous + Test.@test Traits.variable_dependence(cl5) == Traits.Fixed + + # ClosedLoop, NonAutonomous, Fixed + cl6 = ClosedLoop((t, x) -> t + x; is_autonomous=false, is_variable=false) + Test.@test cl6 isa Data.ControlLaw + Test.@test Traits.feedback(cl6) == Traits.ClosedLoopFeedback + Test.@test Traits.time_dependence(cl6) == Traits.NonAutonomous + Test.@test Traits.variable_dependence(cl6) == Traits.Fixed + + # ClosedLoop, Autonomous, NonFixed + cl7 = ClosedLoop((x, v) -> x + v; is_autonomous=true, is_variable=true) + Test.@test cl7 isa Data.ControlLaw + Test.@test Traits.feedback(cl7) == Traits.ClosedLoopFeedback + Test.@test Traits.time_dependence(cl7) == Traits.Autonomous + Test.@test Traits.variable_dependence(cl7) == Traits.NonFixed + + # ClosedLoop, NonAutonomous, NonFixed + cl8 = ClosedLoop((t, x, v) -> t + x + v; is_autonomous=false, is_variable=true) + Test.@test cl8 isa Data.ControlLaw + Test.@test Traits.feedback(cl8) == Traits.ClosedLoopFeedback + Test.@test Traits.time_dependence(cl8) == Traits.NonAutonomous + Test.@test Traits.variable_dependence(cl8) == Traits.NonFixed + + # DynClosedLoop, Autonomous, Fixed + cl9 = DynClosedLoop((x, p) -> x + p; is_autonomous=true, is_variable=false) + Test.@test cl9 isa Data.ControlLaw + Test.@test Traits.feedback(cl9) == Traits.DynClosedLoopFeedback + Test.@test Traits.time_dependence(cl9) == Traits.Autonomous + Test.@test Traits.variable_dependence(cl9) == Traits.Fixed + + # DynClosedLoop, NonAutonomous, Fixed + cl10 = DynClosedLoop((t, x, p) -> t + x + p; is_autonomous=false, is_variable=false) + Test.@test cl10 isa Data.ControlLaw + Test.@test Traits.feedback(cl10) == Traits.DynClosedLoopFeedback + Test.@test Traits.time_dependence(cl10) == Traits.NonAutonomous + Test.@test Traits.variable_dependence(cl10) == Traits.Fixed + + # DynClosedLoop, Autonomous, NonFixed + cl11 = DynClosedLoop((x, p, v) -> x + p + v; is_autonomous=true, is_variable=true) + Test.@test cl11 isa Data.ControlLaw + Test.@test Traits.feedback(cl11) == Traits.DynClosedLoopFeedback + Test.@test Traits.time_dependence(cl11) == Traits.Autonomous + Test.@test Traits.variable_dependence(cl11) == Traits.NonFixed + + # DynClosedLoop, NonAutonomous, NonFixed + cl12 = DynClosedLoop((t, x, p, v) -> t + x + p + v; is_autonomous=false, is_variable=true) + Test.@test cl12 isa Data.ControlLaw + Test.@test Traits.feedback(cl12) == Traits.DynClosedLoopFeedback + Test.@test Traits.time_dependence(cl12) == Traits.NonAutonomous + Test.@test Traits.variable_dependence(cl12) == Traits.NonFixed + end + + # ==================================================================== + # UNIT TESTS - Natural call signatures + # ==================================================================== + + Test.@testset "Unit: Natural call signatures" begin + # OpenLoop + cl1 = OpenLoop(() -> 1.0) + Test.@test cl1() == 1.0 + + cl2 = OpenLoop((t) -> 2t; is_autonomous=false) + Test.@test cl2(3.0) == 6.0 + + cl3 = OpenLoop((v) -> 3v; is_variable=true) + Test.@test cl3(2.0) == 6.0 + + cl4 = OpenLoop((t, v) -> t + v; is_autonomous=false, is_variable=true) + Test.@test cl4(1.0, 2.0) == 3.0 + + # ClosedLoop + cl5 = ClosedLoop((x) -> 2 .* x) + Test.@test cl5([1.0, 2.0]) == [2.0, 4.0] + + cl6 = ClosedLoop((t, x) -> t .+ x; is_autonomous=false) + Test.@test cl6(1.0, [2.0, 3.0]) == [3.0, 4.0] + + cl7 = ClosedLoop((x, v) -> x .+ v; is_variable=true) + Test.@test cl7([1.0, 2.0], 3.0) == [4.0, 5.0] + + cl8 = ClosedLoop((t, x, v) -> t .+ x .+ v; is_autonomous=false, is_variable=true) + Test.@test cl8(1.0, [2.0, 3.0], 4.0) == [7.0, 8.0] + + # DynClosedLoop + cl9 = DynClosedLoop((x, p) -> x .+ p) + Test.@test cl9([1.0, 2.0], [3.0, 4.0]) == [4.0, 6.0] + + cl10 = DynClosedLoop((t, x, p) -> t .+ x .+ p; is_autonomous=false) + Test.@test cl10(1.0, [2.0, 3.0], [4.0, 5.0]) == [7.0, 9.0] + + cl11 = DynClosedLoop((x, p, v) -> x .+ p .+ v; is_variable=true) + Test.@test cl11([1.0, 2.0], [3.0, 4.0], 5.0) == [9.0, 11.0] + + cl12 = DynClosedLoop((t, x, p, v) -> t .+ x .+ p .+ v; is_autonomous=false, is_variable=true) + Test.@test cl12(1.0, [2.0, 3.0], [4.0, 5.0], 6.0) == [13.0, 15.0] + end + + # ==================================================================== + # UNIT TESTS - Uniform call signature + # ==================================================================== + + Test.@testset "Unit: Uniform call signature" begin + # OpenLoop — uniform (t, v), no state + cl1 = OpenLoop(() -> 1.0) + Test.@test cl1(0.0, 3.0) == 1.0 + + cl2 = OpenLoop((t) -> 2t; is_autonomous=false) + Test.@test cl2(3.0, 4.0) == 6.0 + + cl3 = OpenLoop((v) -> 3v; is_variable=true) + Test.@test cl3(0.0, 2.0) == 6.0 + + cl4 = OpenLoop((t, v) -> t + v; is_autonomous=false, is_variable=true) + Test.@test cl4(1.0, 4.0) == 5.0 + + # ClosedLoop — uniform (t, x, v) + cl5 = ClosedLoop((x) -> 2 .* x) + Test.@test cl5(0.0, [1.0, 2.0], 4.0) == [2.0, 4.0] + + cl6 = ClosedLoop((t, x) -> t .+ x; is_autonomous=false) + Test.@test cl6(1.0, [2.0, 3.0], 5.0) == [3.0, 4.0] + + cl7 = ClosedLoop((x, v) -> x .+ v; is_variable=true) + Test.@test cl7(0.0, [1.0, 2.0], 4.0) == [5.0, 6.0] + + cl8 = ClosedLoop((t, x, v) -> t .+ x .+ v; is_autonomous=false, is_variable=true) + Test.@test cl8(1.0, [2.0, 3.0], 5.0) == [8.0, 9.0] + + # DynClosedLoop — uniform (t, x, p, v) + cl9 = DynClosedLoop((x, p) -> x .+ p) + Test.@test cl9(0.0, [1.0, 2.0], [3.0, 4.0], 5.0) == [4.0, 6.0] + + cl10 = DynClosedLoop((t, x, p) -> t .+ x .+ p; is_autonomous=false) + Test.@test cl10(1.0, [2.0, 3.0], [4.0, 5.0], 6.0) == [7.0, 9.0] + + cl11 = DynClosedLoop((x, p, v) -> x .+ p .+ v; is_variable=true) + Test.@test cl11(0.0, [1.0, 2.0], [3.0, 4.0], 5.0) == [9.0, 11.0] + + cl12 = DynClosedLoop((t, x, p, v) -> t .+ x .+ p .+ v; is_autonomous=false, is_variable=true) + Test.@test cl12(1.0, [2.0, 3.0], [4.0, 5.0], 6.0) == [13.0, 15.0] + end + + # ==================================================================== + # UNIT TESTS - Typed constructor + # ==================================================================== + + Test.@testset "Unit: Typed constructor" begin + cl = Data.ControlLaw( + (x, p) -> x .+ p, + Traits.DynClosedLoopFeedback, + Traits.Autonomous, + Traits.Fixed, + ) + # also test via the typed constructor with OpenLoopFeedback + Test.@test cl isa Data.ControlLaw + Test.@test Traits.feedback(cl) === Traits.DynClosedLoopFeedback + Test.@test Traits.time_dependence(cl) === Traits.Autonomous + Test.@test Traits.variable_dependence(cl) === Traits.Fixed + Test.@test cl([1.0, 2.0], [3.0, 4.0]) == [4.0, 6.0] + end + + # ==================================================================== + # UNIT TESTS - Trait accessors + # ==================================================================== + + Test.@testset "Unit: Trait accessors" begin + # dynamics_trait + ol = OpenLoop(() -> 1.0) + Test.@test Traits.dynamics_trait(ol) == Traits.StateDynamics + + cl_ = ClosedLoop((x) -> x) + Test.@test Traits.dynamics_trait(cl_) == Traits.StateDynamics + + dcl = DynClosedLoop((x, p) -> x .+ p) + Test.@test Traits.dynamics_trait(dcl) == Traits.HamiltonianDynamics + + # Predicates + Test.@test Traits.is_open_loop(ol) + Test.@test !Traits.is_closed_loop(ol) + Test.@test !Traits.is_dyn_closed_loop(ol) + + Test.@test !Traits.is_open_loop(cl_) + Test.@test Traits.is_closed_loop(cl_) + Test.@test !Traits.is_dyn_closed_loop(cl_) + + Test.@test !Traits.is_open_loop(dcl) + Test.@test !Traits.is_closed_loop(dcl) + Test.@test Traits.is_dyn_closed_loop(dcl) + end + + # ==================================================================== + # UNIT TESTS - Show Methods + # ==================================================================== + + Test.@testset "Show Methods" begin + cl = OpenLoop(() -> 1.0) + + Test.@testset "Base.show (compact)" begin + io = IOBuffer() + show(io, cl) + str = String(take!(io)) + Test.@test occursin("ControlLaw", str) + Test.@test occursin("open-loop", str) + Test.@test occursin("autonomous", str) + Test.@test occursin("fixed", str) + Test.@test occursin("natural call", str) + Test.@test occursin("uniform call", str) + end + + Test.@testset "Base.show (text/plain)" begin + io = IOBuffer() + show(io, MIME("text/plain"), cl) + str = String(take!(io)) + Test.@test occursin("ControlLaw", str) + Test.@test occursin("open-loop", str) + end + + Test.@testset "Show: DynClosedLoop" begin + dcl = DynClosedLoop((x, p) -> x .+ p) + io = IOBuffer() + show(io, dcl) + str = String(take!(io)) + Test.@test occursin("dyn-closed-loop", str) + Test.@test occursin("u(x, p)", str) + end + end + + # ==================================================================== + # UNIT TESTS - Subtyping + # ==================================================================== + + Test.@testset "Subtyping" begin + Test.@testset "ControlLaw is an AbstractControlLaw" begin + cl = OpenLoop(() -> 1.0) + Test.@test cl isa Data.AbstractControlLaw + end + + Test.@testset "ClosedLoop is an AbstractControlLaw" begin + cl = ClosedLoop((x) -> x) + Test.@test cl isa Data.AbstractControlLaw + end + + Test.@testset "DynClosedLoop is an AbstractControlLaw" begin + cl = DynClosedLoop((x, p) -> x .+ p) + Test.@test cl isa Data.AbstractControlLaw + end + end + + # ==================================================================== + # EXPORTS TESTS + # ==================================================================== + + Test.@testset "Exports" begin + for sym in (:AbstractControlLaw, :ControlLaw, :OpenLoop, :ClosedLoop, :DynClosedLoop) + Test.@test isdefined(Data, sym) + end + end + end +end + +end # module TestControlLaw + +test_control_law() = TestControlLaw.test_control_law() diff --git a/test/suite/data/test_data_module.jl b/test/suite/data/test_data_module.jl index 71ec17c8..a0d43572 100644 --- a/test/suite/data/test_data_module.jl +++ b/test/suite/data/test_data_module.jl @@ -34,10 +34,15 @@ const CurrentModule = TestDataModule # These lists define the expected public API of the Data module. const EXPORTED_ABSTRACT_TYPES = ( - :AbstractVectorField, :AbstractHamiltonianVectorField, :AbstractHamiltonian + :AbstractVectorField, :AbstractHamiltonianVectorField, :AbstractHamiltonian, + :AbstractControlLaw, :AbstractPseudoHamiltonian, ) -const EXPORTED_CONCRETE_TYPES = (:VectorField, :HamiltonianVectorField, :Hamiltonian) +const EXPORTED_CONCRETE_TYPES = ( + :VectorField, :HamiltonianVectorField, :Hamiltonian, + :ControlLaw, :OpenLoop, :ClosedLoop, :DynClosedLoop, + :PseudoHamiltonian, +) const PRIVATE_SYMBOLS = ( :__is_autonomous, @@ -56,6 +61,11 @@ const PRIVATE_SYMBOLS = ( :_uniform_sig_hvf, :_uniform_sig_vf, :_vd_label, + :_fb_label, + :_natural_sig_cl, + :_uniform_sig_cl, + :_natural_sig_ph, + :_uniform_sig_ph, ) # ============================================================================ diff --git a/test/suite/data/test_pseudo_hamiltonian.jl b/test/suite/data/test_pseudo_hamiltonian.jl new file mode 100644 index 00000000..470cbf3b --- /dev/null +++ b/test/suite/data/test_pseudo_hamiltonian.jl @@ -0,0 +1,184 @@ +module TestPseudoHamiltonian + +using Test: Test +import CTBase.Data: Data, PseudoHamiltonian +import CTBase.Traits + +const VERBOSE = isdefined(Main, :TestData) ? Main.TestData.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestData) ? Main.TestData.SHOWTIMING : true + +function test_pseudo_hamiltonian() + Test.@testset "PseudoHamiltonian Tests" verbose=VERBOSE showtiming=SHOWTIMING begin + + # ==================================================================== + # UNIT TESTS - Construction with all trait combinations + # ==================================================================== + + Test.@testset "Unit: Construction with all trait combinations" begin + # Autonomous, Fixed + ph1 = PseudoHamiltonian((x, p, u) -> sum(x .* p) + u^2) + Test.@test ph1 isa Data.PseudoHamiltonian + Test.@test Traits.time_dependence(ph1) == Traits.Autonomous + Test.@test Traits.variable_dependence(ph1) == Traits.Fixed + + # NonAutonomous, Fixed + ph2 = PseudoHamiltonian((t, x, p, u) -> t * sum(x .* p) + u^2; is_autonomous=false) + Test.@test ph2 isa Data.PseudoHamiltonian + Test.@test Traits.time_dependence(ph2) == Traits.NonAutonomous + Test.@test Traits.variable_dependence(ph2) == Traits.Fixed + + # Autonomous, NonFixed + ph3 = PseudoHamiltonian((x, p, u, v) -> sum(x .* p) + u^2 + v; is_variable=true) + Test.@test ph3 isa Data.PseudoHamiltonian + Test.@test Traits.time_dependence(ph3) == Traits.Autonomous + Test.@test Traits.variable_dependence(ph3) == Traits.NonFixed + + # NonAutonomous, NonFixed + ph4 = PseudoHamiltonian( + (t, x, p, u, v) -> t * sum(x .* p) + u^2 + v; + is_autonomous=false, is_variable=true, + ) + Test.@test ph4 isa Data.PseudoHamiltonian + Test.@test Traits.time_dependence(ph4) == Traits.NonAutonomous + Test.@test Traits.variable_dependence(ph4) == Traits.NonFixed + end + + # ==================================================================== + # UNIT TESTS - Natural call signatures + # ==================================================================== + + Test.@testset "Unit: Natural call signatures" begin + # Autonomous, Fixed: h̃(x, p, u) + ph1 = PseudoHamiltonian((x, p, u) -> sum(x .* p) + u^2) + Test.@test ph1([1.0, 2.0], [3.0, 4.0], 5.0) == 3.0 + 8.0 + 25.0 + + # NonAutonomous, Fixed: h̃(t, x, p, u) + ph2 = PseudoHamiltonian((t, x, p, u) -> t + sum(x .* p) + u^2; is_autonomous=false) + Test.@test ph2(1.0, [2.0, 3.0], [4.0, 5.0], 6.0) == 1.0 + 8.0 + 15.0 + 36.0 + + # Autonomous, NonFixed: h̃(x, p, u, v) + ph3 = PseudoHamiltonian((x, p, u, v) -> sum(x .* p) + u^2 + v; is_variable=true) + Test.@test ph3([1.0, 2.0], [3.0, 4.0], 5.0, 6.0) == 3.0 + 8.0 + 25.0 + 6.0 + + # NonAutonomous, NonFixed: h̃(t, x, p, u, v) + ph4 = PseudoHamiltonian( + (t, x, p, u, v) -> t + sum(x .* p) + u^2 + v; + is_autonomous=false, is_variable=true, + ) + Test.@test ph4(1.0, [2.0, 3.0], [4.0, 5.0], 6.0, 7.0) == 1.0 + 8.0 + 15.0 + 36.0 + 7.0 + end + + # ==================================================================== + # UNIT TESTS - Uniform call signature + # ==================================================================== + + Test.@testset "Unit: Uniform call signature (t, x, p, u, v)" begin + # Autonomous, Fixed — ignores t, v + ph1 = PseudoHamiltonian((x, p, u) -> sum(x .* p) + u^2) + Test.@test ph1(0.0, [1.0, 2.0], [3.0, 4.0], 5.0, 6.0) == 3.0 + 8.0 + 25.0 + + # NonAutonomous, Fixed — ignores v + ph2 = PseudoHamiltonian((t, x, p, u) -> t + sum(x .* p) + u^2; is_autonomous=false) + Test.@test ph2(1.0, [2.0, 3.0], [4.0, 5.0], 6.0, 7.0) == 1.0 + 8.0 + 15.0 + 36.0 + + # Autonomous, NonFixed — ignores t + ph3 = PseudoHamiltonian((x, p, u, v) -> sum(x .* p) + u^2 + v; is_variable=true) + Test.@test ph3(0.0, [1.0, 2.0], [3.0, 4.0], 5.0, 6.0) == 3.0 + 8.0 + 25.0 + 6.0 + + # NonAutonomous, NonFixed — uses all + ph4 = PseudoHamiltonian( + (t, x, p, u, v) -> t + sum(x .* p) + u^2 + v; + is_autonomous=false, is_variable=true, + ) + Test.@test ph4(1.0, [2.0, 3.0], [4.0, 5.0], 6.0, 7.0) == 1.0 + 8.0 + 15.0 + 36.0 + 7.0 + end + + # ==================================================================== + # UNIT TESTS - Typed constructor + # ==================================================================== + + Test.@testset "Unit: Typed constructor" begin + ph = PseudoHamiltonian( + (x, p, u) -> sum(x .* p) + u^2, + Traits.Autonomous, + Traits.Fixed, + ) + Test.@test ph isa Data.PseudoHamiltonian + Test.@test Traits.time_dependence(ph) === Traits.Autonomous + Test.@test Traits.variable_dependence(ph) === Traits.Fixed + Test.@test ph([1.0, 2.0], [3.0, 4.0], 5.0) == 3.0 + 8.0 + 25.0 + end + + # ==================================================================== + # UNIT TESTS - Trait accessors + # ==================================================================== + + Test.@testset "Unit: Trait accessors" begin + ph = PseudoHamiltonian((x, p, u) -> sum(x .* p) + u^2) + Test.@test Traits.dynamics_trait(ph) == Traits.HamiltonianDynamics + Test.@test Traits.has_time_dependence_trait(ph) + Test.@test Traits.has_variable_dependence_trait(ph) + end + + # ==================================================================== + # UNIT TESTS - Show Methods + # ==================================================================== + + Test.@testset "Show Methods" begin + ph = PseudoHamiltonian((x, p, u) -> sum(x .* p) + u^2) + + Test.@testset "Base.show (compact)" begin + io = IOBuffer() + show(io, ph) + str = String(take!(io)) + Test.@test occursin("PseudoHamiltonian", str) + Test.@test occursin("autonomous", str) + Test.@test occursin("fixed", str) + Test.@test occursin("natural call", str) + Test.@test occursin("uniform call", str) + end + + Test.@testset "Base.show (text/plain)" begin + io = IOBuffer() + show(io, MIME("text/plain"), ph) + str = String(take!(io)) + Test.@test occursin("PseudoHamiltonian", str) + end + + Test.@testset "Show: NonAutonomous, NonFixed" begin + ph2 = PseudoHamiltonian( + (t, x, p, u, v) -> t + sum(x .* p) + u^2 + v; + is_autonomous=false, is_variable=true, + ) + io = IOBuffer() + show(io, ph2) + str = String(take!(io)) + Test.@test occursin("non-autonomous", str) + Test.@test occursin("variable", str) + end + end + + # ==================================================================== + # UNIT TESTS - Subtyping + # ==================================================================== + + Test.@testset "Subtyping" begin + ph = PseudoHamiltonian((x, p, u) -> sum(x .* p) + u^2) + Test.@test ph isa Data.AbstractPseudoHamiltonian + end + + # ==================================================================== + # EXPORTS TESTS + # ==================================================================== + + Test.@testset "Exports" begin + for sym in (:AbstractPseudoHamiltonian, :PseudoHamiltonian) + Test.@test isdefined(Data, sym) + end + end + end +end + +end # module TestPseudoHamiltonian + +test_pseudo_hamiltonian() = TestPseudoHamiltonian.test_pseudo_hamiltonian() diff --git a/test/suite/differentiation/test_differentiation_module.jl b/test/suite/differentiation/test_differentiation_module.jl index 008840a6..72827d2f 100644 --- a/test/suite/differentiation/test_differentiation_module.jl +++ b/test/suite/differentiation/test_differentiation_module.jl @@ -36,6 +36,8 @@ const EXPORTED_FUNCTIONS = ( :build_ad_backend, :ad_backend, :hamiltonian_gradient, + :pseudo_hamiltonian_gradient, + :pseudo_hamiltonian_control_gradient, :variable_gradient, :gradient, :derivative, diff --git a/test/suite/differentiation/test_pseudo_hamiltonian_gradient.jl b/test/suite/differentiation/test_pseudo_hamiltonian_gradient.jl new file mode 100644 index 00000000..a0a5cefa --- /dev/null +++ b/test/suite/differentiation/test_pseudo_hamiltonian_gradient.jl @@ -0,0 +1,219 @@ +""" +Unit and error tests for pseudo_hamiltonian_gradient and pseudo_hamiltonian_control_gradient AD contracts and extensions. +""" + +module TestPseudoHamiltonianGradient + +using Test: Test +import CTBase.Differentiation +import CTBase.Data +import CTBase.Traits +import CTBase.Exceptions +import CTBase.Strategies +using ADTypes: ADTypes +using ForwardDiff: ForwardDiff +using DifferentiationInterface: DifferentiationInterface + +const VERBOSE = isdefined(Main, :TestData) ? Main.TestData.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestData) ? Main.TestData.SHOWTIMING : true + +# ============================================================================== +# Fake AD Backend for Testing (at module top-level) +# ============================================================================== + +struct FakeADBackend <: Differentiation.AbstractADBackend + options::Strategies.StrategyOptions +end + +FakeADBackend() = FakeADBackend(Strategies.StrategyOptions()) + +# ============================================================================== +# Fake PseudoHamiltonian for Testing (at module top-level) +# ============================================================================== + +struct FakePseudoHamiltonian <: + Data.AbstractPseudoHamiltonian{Traits.Autonomous,Traits.Fixed} + f::Function +end + +FakePseudoHamiltonian() = FakePseudoHamiltonian((x, p, u) -> 0.0) + +function test_pseudo_hamiltonian_gradient() + Test.@testset "PseudoHamiltonian Gradient Tests" verbose=VERBOSE showtiming=SHOWTIMING begin + + # ============================================================================== + # Error Tests — NotImplemented stubs + # ============================================================================== + + Test.@testset "Error: pseudo_hamiltonian_gradient stub throws NotImplemented" begin + backend = FakeADBackend() + h̃ = FakePseudoHamiltonian() + t = 0.0 + x = [1.0, 2.0] + p = [3.0, 4.0] + u = [5.0, 6.0] + v = 7.0 + + try + Differentiation.pseudo_hamiltonian_gradient(backend, h̃, t, x, p, u, v) + Test.@test false + catch err + Test.@test err isa Exceptions.NotImplemented + Test.@test occursin("pseudo_hamiltonian_gradient", err.required_method) + end + end + + Test.@testset "Error: pseudo_hamiltonian_control_gradient stub throws NotImplemented" begin + backend = FakeADBackend() + h̃ = FakePseudoHamiltonian() + t = 0.0 + x = [1.0, 2.0] + p = [3.0, 4.0] + u = [5.0, 6.0] + v = 7.0 + + try + Differentiation.pseudo_hamiltonian_control_gradient(backend, h̃, t, x, p, u, v) + Test.@test false + catch err + Test.@test err isa Exceptions.NotImplemented + Test.@test occursin("pseudo_hamiltonian_control_gradient", err.required_method) + end + end + + # ============================================================================== + # Unit Tests — pseudo_hamiltonian_gradient via DI + # ============================================================================== + + Test.@testset "Unit: pseudo_hamiltonian_gradient via DI (Autonomous, Fixed)" begin + backend = Differentiation.DifferentiationInterface(; ad_backend=ADTypes.AutoForwardDiff()) + h̃ = Data.PseudoHamiltonian((x, p, u) -> sum(x .* p) + sum(u .^ 2)) + t = 0.0 + x = [1.0, 2.0] + p = [3.0, 4.0] + u = [5.0, 6.0] + v = nothing + + grad_x, grad_p = Differentiation.pseudo_hamiltonian_gradient( + backend, h̃, t, x, p, u, v + ) + # ∂H̃/∂x = p = [3, 4] + Test.@test grad_x ≈ [3.0, 4.0] + # ∂H̃/∂p = x = [1, 2] + Test.@test grad_p ≈ [1.0, 2.0] + end + + Test.@testset "Unit: pseudo_hamiltonian_gradient via DI (NonAutonomous, Fixed)" begin + backend = Differentiation.DifferentiationInterface(; ad_backend=ADTypes.AutoForwardDiff()) + h̃ = Data.PseudoHamiltonian( + (t, x, p, u) -> t * sum(x .* p) + sum(u .^ 2); + is_autonomous=false, + ) + t = 2.0 + x = [1.0, 2.0] + p = [3.0, 4.0] + u = [5.0, 6.0] + v = nothing + + grad_x, grad_p = Differentiation.pseudo_hamiltonian_gradient( + backend, h̃, t, x, p, u, v + ) + # ∂H̃/∂x = t*p = [6, 8] + Test.@test grad_x ≈ [6.0, 8.0] + # ∂H̃/∂p = t*x = [2, 4] + Test.@test grad_p ≈ [2.0, 4.0] + end + + Test.@testset "Unit: pseudo_hamiltonian_gradient via DI (Autonomous, NonFixed)" begin + backend = Differentiation.DifferentiationInterface(; ad_backend=ADTypes.AutoForwardDiff()) + h̃ = Data.PseudoHamiltonian( + (x, p, u, v) -> sum(x .* p) + sum(u .^ 2) + v^2; + is_variable=true, + ) + t = 0.0 + x = [1.0, 2.0] + p = [3.0, 4.0] + u = [5.0, 6.0] + v = 7.0 + + grad_x, grad_p = Differentiation.pseudo_hamiltonian_gradient( + backend, h̃, t, x, p, u, v + ) + # ∂H̃/∂x = p = [3, 4] + Test.@test grad_x ≈ [3.0, 4.0] + # ∂H̃/∂p = x = [1, 2] + Test.@test grad_p ≈ [1.0, 2.0] + end + + # ============================================================================== + # Unit Tests — pseudo_hamiltonian_control_gradient via DI + # ============================================================================== + + Test.@testset "Unit: pseudo_hamiltonian_control_gradient via DI (Autonomous, Fixed)" begin + backend = Differentiation.DifferentiationInterface(; ad_backend=ADTypes.AutoForwardDiff()) + h̃ = Data.PseudoHamiltonian((x, p, u) -> sum(x .* p) + sum(u .^ 2)) + t = 0.0 + x = [1.0, 2.0] + p = [3.0, 4.0] + u = [5.0, 6.0] + v = nothing + + grad_u = Differentiation.pseudo_hamiltonian_control_gradient( + backend, h̃, t, x, p, u, v + ) + # ∂H̃/∂u = 2u = [10, 12] + Test.@test grad_u ≈ [10.0, 12.0] + end + + Test.@testset "Unit: pseudo_hamiltonian_control_gradient via DI (NonAutonomous, Fixed)" begin + backend = Differentiation.DifferentiationInterface(; ad_backend=ADTypes.AutoForwardDiff()) + h̃ = Data.PseudoHamiltonian( + (t, x, p, u) -> t * sum(x .* p) + sum(u .^ 2); + is_autonomous=false, + ) + t = 2.0 + x = [1.0, 2.0] + p = [3.0, 4.0] + u = [5.0, 6.0] + v = nothing + + grad_u = Differentiation.pseudo_hamiltonian_control_gradient( + backend, h̃, t, x, p, u, v + ) + # ∂H̃/∂u = 2u = [10, 12] + Test.@test grad_u ≈ [10.0, 12.0] + end + + Test.@testset "Unit: pseudo_hamiltonian_control_gradient via DI (scalar u)" begin + backend = Differentiation.DifferentiationInterface(; ad_backend=ADTypes.AutoForwardDiff()) + h̃ = Data.PseudoHamiltonian((x, p, u) -> sum(x .* p) + u^2) + t = 0.0 + x = [1.0, 2.0] + p = [3.0, 4.0] + u = 5.0 + v = nothing + + grad_u = Differentiation.pseudo_hamiltonian_control_gradient( + backend, h̃, t, x, p, u, v + ) + # ∂H̃/∂u = 2u = 10 + Test.@test grad_u ≈ 10.0 + end + + # ============================================================================== + # Export Tests + # ============================================================================== + + Test.@testset "Export: pseudo_hamiltonian_gradient" begin + Test.@test isdefined(Differentiation, :pseudo_hamiltonian_gradient) + end + + Test.@testset "Export: pseudo_hamiltonian_control_gradient" begin + Test.@test isdefined(Differentiation, :pseudo_hamiltonian_control_gradient) + end + end +end + +end # module + +test_pseudo_hamiltonian_gradient() = TestPseudoHamiltonianGradient.test_pseudo_hamiltonian_gradient() diff --git a/test/suite/traits/test_feedback.jl b/test/suite/traits/test_feedback.jl new file mode 100644 index 00000000..71e59c34 --- /dev/null +++ b/test/suite/traits/test_feedback.jl @@ -0,0 +1,153 @@ +module TestFeedback + +using Test: Test +import CTBase.Traits + +const VERBOSE = isdefined(Main, :TestTraits) ? Main.TestTraits.VERBOSE : true +const SHOWTIMING = isdefined(Main, :TestTraits) ? Main.TestTraits.SHOWTIMING : true + +function test_feedback() + Test.@testset "Feedback Trait Tests" verbose=VERBOSE showtiming=SHOWTIMING begin + + # ==================================================================== + # UNIT TESTS - Abstract Type + # ==================================================================== + + Test.@testset "UNIT TESTS - Abstract Type" begin + Test.@testset "AbstractFeedback is exported" begin + Test.@test isdefined(Traits, :AbstractFeedback) + end + + Test.@testset "AbstractFeedback is abstract" begin + Test.@test isabstracttype(Traits.AbstractFeedback) + end + + Test.@testset "AbstractFeedback subtypes AbstractTrait" begin + Test.@test Traits.AbstractFeedback <: Traits.AbstractTrait + end + end + + # ==================================================================== + # UNIT TESTS - Concrete Trait Types + # ==================================================================== + + Test.@testset "UNIT TESTS - Concrete Trait Types" begin + Test.@testset "OpenLoopFeedback" begin + Test.@testset "is exported" begin + Test.@test isdefined(Traits, :OpenLoopFeedback) + end + + Test.@testset "is concrete" begin + Test.@test !isabstracttype(Traits.OpenLoopFeedback) + end + + Test.@testset "instantiates" begin + fb = Traits.OpenLoopFeedback() + Test.@test fb isa Traits.OpenLoopFeedback + end + + Test.@testset "subtypes AbstractFeedback" begin + Test.@test Traits.OpenLoopFeedback <: Traits.AbstractFeedback + end + end + + Test.@testset "ClosedLoopFeedback" begin + Test.@testset "is exported" begin + Test.@test isdefined(Traits, :ClosedLoopFeedback) + end + + Test.@testset "is concrete" begin + Test.@test !isabstracttype(Traits.ClosedLoopFeedback) + end + + Test.@testset "instantiates" begin + fb = Traits.ClosedLoopFeedback() + Test.@test fb isa Traits.ClosedLoopFeedback + end + + Test.@testset "subtypes AbstractFeedback" begin + Test.@test Traits.ClosedLoopFeedback <: Traits.AbstractFeedback + end + end + + Test.@testset "DynClosedLoopFeedback" begin + Test.@testset "is exported" begin + Test.@test isdefined(Traits, :DynClosedLoopFeedback) + end + + Test.@testset "is concrete" begin + Test.@test !isabstracttype(Traits.DynClosedLoopFeedback) + end + + Test.@testset "instantiates" begin + fb = Traits.DynClosedLoopFeedback() + Test.@test fb isa Traits.DynClosedLoopFeedback + end + + Test.@testset "subtypes AbstractFeedback" begin + Test.@test Traits.DynClosedLoopFeedback <: Traits.AbstractFeedback + end + end + end + + # ==================================================================== + # UNIT TESTS - Type Hierarchy + # ==================================================================== + + Test.@testset "UNIT TESTS - Type Hierarchy" begin + Test.@testset "All feedback traits subtype AbstractTrait" begin + Test.@test Traits.OpenLoopFeedback <: Traits.AbstractTrait + Test.@test Traits.ClosedLoopFeedback <: Traits.AbstractTrait + Test.@test Traits.DynClosedLoopFeedback <: Traits.AbstractTrait + end + + Test.@testset "Feedback traits are distinct from dynamics traits" begin + Test.@test !(Traits.OpenLoopFeedback <: Traits.AbstractDynamicsTrait) + Test.@test !(Traits.ClosedLoopFeedback <: Traits.AbstractDynamicsTrait) + Test.@test !(Traits.DynClosedLoopFeedback <: Traits.AbstractDynamicsTrait) + end + + Test.@testset "Feedback traits are distinct from control-dependence traits" begin + Test.@test !(Traits.OpenLoopFeedback <: Traits.ControlDependence) + Test.@test !(Traits.ClosedLoopFeedback <: Traits.ControlDependence) + Test.@test !(Traits.DynClosedLoopFeedback <: Traits.ControlDependence) + end + end + + # ==================================================================== + # UNIT TESTS - feedback accessor + # ==================================================================== + + Test.@testset "UNIT TESTS - feedback accessor" begin + Test.@testset "feedback is exported" begin + Test.@test isdefined(Traits, :feedback) + end + + Test.@testset "feedback is a generic function" begin + Test.@test isa(Traits.feedback, Function) + end + end + + # ==================================================================== + # Exports Verification + # ==================================================================== + + Test.@testset "Exports Verification" begin + Test.@testset "Exported feedback trait types" begin + for sym in ( + :AbstractFeedback, + :OpenLoopFeedback, + :ClosedLoopFeedback, + :DynClosedLoopFeedback, + :feedback, + ) + Test.@test isdefined(Traits, sym) + end + end + end + end +end + +end # module + +test_feedback() = TestFeedback.test_feedback() From 8bc9e4c0073e7d91450d65f27a7ec0741d1909cc Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Sat, 4 Jul 2026 21:28:10 +0200 Subject: [PATCH 2/6] docs: add new source files to api_reference.jl Add feedback.jl to Traits module config. Add abstract_control_law.jl, control_law.jl, abstract_pseudo_hamiltonian.jl, pseudo_hamiltonian.jl to Data module config. Without these entries, the new types and functions would not appear in the generated API reference documentation. --- docs/api_reference.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/api_reference.jl b/docs/api_reference.jl index e6612d57..23b8af53 100644 --- a/docs/api_reference.jl +++ b/docs/api_reference.jl @@ -54,6 +54,10 @@ function generate_api_reference(src_dir::String) joinpath("Data", "hamiltonian.jl"), joinpath("Data", "abstract_hamiltonian_vector_field.jl"), joinpath("Data", "hamiltonian_vector_field.jl"), + joinpath("Data", "abstract_control_law.jl"), + joinpath("Data", "control_law.jl"), + joinpath("Data", "abstract_pseudo_hamiltonian.jl"), + joinpath("Data", "pseudo_hamiltonian.jl"), ), ), ( @@ -175,6 +179,7 @@ function generate_api_reference(src_dir::String) joinpath("Traits", "time_dependence.jl"), joinpath("Traits", "variable_dependence.jl"), joinpath("Traits", "control_dependence.jl"), + joinpath("Traits", "feedback.jl"), ), ), ( From 0e557c9c18fd52d974ab526e3a2fa3914b21c3a9 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Sat, 4 Jul 2026 22:19:32 +0200 Subject: [PATCH 3/6] docs: qualify all @ref links with module prefixes Fix Documenter cross-reference warnings by qualifying all unqualified @ref links with their full module paths (CTBase.Traits.*, CTBase.Data.*, CTBase.Differentiation.*, CTBaseDifferentiationInterface.*). Unqualified links like [`AbstractFeedback`](@ref) could not be resolved by Documenter when the docstring was rendered in a different module context (e.g. Data docs referencing Traits symbols). --- ext/CTBaseDifferentiationInterface.jl | 10 +++++----- src/Data/abstract_control_law.jl | 8 ++++---- src/Data/abstract_pseudo_hamiltonian.jl | 2 +- src/Data/control_law.jl | 2 +- src/Differentiation/abstract_ad_backend.jl | 4 ++-- src/Traits/feedback.jl | 6 +++--- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/ext/CTBaseDifferentiationInterface.jl b/ext/CTBaseDifferentiationInterface.jl index 0c27e121..1333ddc4 100644 --- a/ext/CTBaseDifferentiationInterface.jl +++ b/ext/CTBaseDifferentiationInterface.jl @@ -30,7 +30,7 @@ Return the scalar DI differentiation primitive for a `Number` active argument. Dispatches to `DI.derivative`, which computes `df/dx` for scalar `x`. -See also: [`_derivator(::Type{<:AbstractArray})`](@ref) +See also: [`CTBaseDifferentiationInterface._derivator`](@ref) """ function _derivator(::Type{<:Number}) return DI.derivative @@ -43,7 +43,7 @@ Return the array DI differentiation primitive for an `AbstractArray` active argu Dispatches to `DI.gradient`, which computes `∇f` for array `x`. -See also: [`_derivator(::Type{<:Number})`](@ref) +See also: [`CTBaseDifferentiationInterface._derivator`](@ref) """ function _derivator(::Type{<:AbstractArray}) return DI.gradient @@ -82,7 +82,7 @@ $(TYPEDSIGNATURES) Compute variable gradient ∂H/∂v via DifferentiationInterface.jl. -See the note in [`hamiltonian_gradient`](@ref) on why anonymous closures are used. +See the note in [`CTBase.Differentiation.hamiltonian_gradient`](@ref) on why anonymous closures are used. # Returns - `grad_v` = ∂H/∂v. @@ -107,7 +107,7 @@ Compute pseudo-Hamiltonian gradients (∂H̃/∂x, ∂H̃/∂p) via Differentiat Along a PMP solution, the stationarity condition ∂H̃/∂u = 0 holds, so the Hamiltonian flow only requires ∂H̃/∂x and ∂H̃/∂p. Use -[`pseudo_hamiltonian_control_gradient`](@ref) for ∂H̃/∂u. +[`CTBase.Differentiation.pseudo_hamiltonian_control_gradient`](@ref) for ∂H̃/∂u. Anonymous closures are used deliberately so that ForwardDiff `tagcount` values are assigned at runtime in the correct left-to-right order inside `ForwardDiff.≺`, @@ -266,7 +266,7 @@ Compute the partial derivative or gradient of `f` with respect to the argument a slot `Slot`, using DifferentiationInterface.jl. An anonymous closure captures the constant arguments and places `active_` at `Slot` -via `ntuple` — same rationale as [`hamiltonian_gradient`](@ref) (ForwardDiff tag ordering). +via `ntuple` — same rationale as [`CTBase.Differentiation.hamiltonian_gradient`](@ref) (ForwardDiff tag ordering). `_derivator` dispatches to `DI.gradient` for array `active` and `DI.derivative` for scalar. See also: [`CTBase.Differentiation.pushforward`](@ref). diff --git a/src/Data/abstract_control_law.jl b/src/Data/abstract_control_law.jl index 884c9b8b..3c085444 100644 --- a/src/Data/abstract_control_law.jl +++ b/src/Data/abstract_control_law.jl @@ -9,7 +9,7 @@ Abstract supertype for control laws together with their feedback, time-dependence, and variable-dependence traits. A control law is a function `u(...)` that provides the control input for an -optimal control problem. The feedback trait ([`AbstractFeedback`](@ref)) +optimal control problem. The feedback trait ([`CTBase.Traits.AbstractFeedback`](@ref)) determines which arguments the control law depends on: - **Open-loop**: `u(t[, v])` — depends on time (and variable) only. @@ -110,7 +110,7 @@ end """ $(TYPEDSIGNATURES) -Return the dynamics trait of an open-loop control law, namely [`StateDynamics`](@ref). +Return the dynamics trait of an open-loop control law, namely [`CTBase.Traits.StateDynamics`](@ref). Open-loop and closed-loop control laws do not involve the costate, so they are associated with state dynamics. @@ -122,7 +122,7 @@ Traits.dynamics_trait(::AbstractControlLaw{<:Traits.OpenLoopFeedback}) = Traits. """ $(TYPEDSIGNATURES) -Return the dynamics trait of a closed-loop control law, namely [`StateDynamics`](@ref). +Return the dynamics trait of a closed-loop control law, namely [`CTBase.Traits.StateDynamics`](@ref). See also: [`CTBase.Traits.dynamics_trait`](@ref), [`CTBase.Traits.StateDynamics`](@ref). """ @@ -131,7 +131,7 @@ Traits.dynamics_trait(::AbstractControlLaw{<:Traits.ClosedLoopFeedback}) = Trait """ $(TYPEDSIGNATURES) -Return the dynamics trait of a dynamic closed-loop control law, namely [`HamiltonianDynamics`](@ref). +Return the dynamics trait of a dynamic closed-loop control law, namely [`CTBase.Traits.HamiltonianDynamics`](@ref). Dynamic closed-loop control laws depend on the costate, so they are associated with Hamiltonian dynamics. diff --git a/src/Data/abstract_pseudo_hamiltonian.jl b/src/Data/abstract_pseudo_hamiltonian.jl index 9a78de6a..6056ed61 100644 --- a/src/Data/abstract_pseudo_hamiltonian.jl +++ b/src/Data/abstract_pseudo_hamiltonian.jl @@ -10,7 +10,7 @@ time-dependence and variable-dependence traits. A pseudo-Hamiltonian is a scalar function `H̃(t, x, p, u[, v]) → ℝ` that extends the standard Hamiltonian with an explicit control argument `u`. Unlike -[`AbstractHamiltonian`](@ref), which encodes the control implicitly, a +[`CTBase.Data.AbstractHamiltonian`](@ref), which encodes the control implicitly, a pseudo-Hamiltonian takes the control as an additional argument, enabling dynamic closed-loop flows where the control is computed from the pseudo-Hamiltonian's maximisation condition. diff --git a/src/Data/control_law.jl b/src/Data/control_law.jl index 17c96774..ca524ee9 100644 --- a/src/Data/control_law.jl +++ b/src/Data/control_law.jl @@ -10,7 +10,7 @@ time-dependence, and variable-dependence traits. The function provides the control input `u(...)` for an optimal control problem. The feedback trait determines which arguments the control law depends on (see -[`AbstractControlLaw`](@ref)). +[`CTBase.Data.AbstractControlLaw`](@ref)). # Type Parameters - `F`: concrete type of the wrapped function. diff --git a/src/Differentiation/abstract_ad_backend.jl b/src/Differentiation/abstract_ad_backend.jl index 6ca4e9f0..9cdd3438 100644 --- a/src/Differentiation/abstract_ad_backend.jl +++ b/src/Differentiation/abstract_ad_backend.jl @@ -108,7 +108,7 @@ Compute the pseudo-Hamiltonian gradient (∂H̃/∂x, ∂H̃/∂p) using the bac Along a PMP solution, the stationarity condition ∂H̃/∂u = 0 holds, so the Hamiltonian flow only requires ∂H̃/∂x and ∂H̃/∂p. Use -[`pseudo_hamiltonian_control_gradient`](@ref) to compute ∂H̃/∂u separately +[`CTBase.Differentiation.pseudo_hamiltonian_control_gradient`](@ref) to compute ∂H̃/∂u separately (e.g. for checking the stationarity condition). # Arguments @@ -151,7 +151,7 @@ Compute the pseudo-Hamiltonian control gradient ∂H̃/∂u using the backend. This is typically used to check the PMP stationarity condition ∂H̃/∂u = 0, not for the Hamiltonian flow itself (which only needs ∂H̃/∂x and ∂H̃/∂p; -see [`pseudo_hamiltonian_gradient`](@ref)). +see [`CTBase.Differentiation.pseudo_hamiltonian_gradient`](@ref)). # Arguments - `backend::AbstractADBackend`: The AD backend. diff --git a/src/Traits/feedback.jl b/src/Traits/feedback.jl index 252cde4f..a9f67756 100644 --- a/src/Traits/feedback.jl +++ b/src/Traits/feedback.jl @@ -11,7 +11,7 @@ closed-loop control (state and costate dependence). # Trait Pattern This trait follows the **type-parameter-only** contract (like -[`AbstractDynamicsTrait`](@ref)): the trait value is read from a type parameter +[`CTBase.Traits.AbstractDynamicsTrait`](@ref)): the trait value is read from a type parameter of the concrete data type (e.g. `ControlLaw{F,FB,TD,VD}`) by the `feedback` accessor. No `has_feedback_trait` guard is provided; calling `feedback` on a type that does not implement it yields a standard `MethodError`. @@ -86,8 +86,8 @@ $(TYPEDSIGNATURES) Return the feedback trait of `x`. Methods are defined on concrete types in `Data` (e.g. `AbstractControlLaw`). -The trait value is one of [`OpenLoopFeedback`](@ref), [`ClosedLoopFeedback`](@ref), -or [`DynClosedLoopFeedback`](@ref). +The trait value is one of [`CTBase.Traits.OpenLoopFeedback`](@ref), [`CTBase.Traits.ClosedLoopFeedback`](@ref), +or [`CTBase.Traits.DynClosedLoopFeedback`](@ref). # See also From 7a9f3a7b138c8d78b9d502678d51d912ecd20057 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Sat, 4 Jul 2026 22:22:34 +0200 Subject: [PATCH 4/6] docs: align docstrings with Handbook conventions - Convert # See also sections to See also: inline lines (feedback.jl) - Add # Returns sections to trait accessors and predicates - Add # Arguments sections to time_dependence/variable_dependence accessors - Add # Arguments/# Output to Base.show methods - Add # Arguments/# Returns to typed constructors - Add descriptive sentence to MIME text/plain show methods Follows the control-toolbox Handbook docstring template: https://github.com/control-toolbox/Handbook/blob/main/philosophy/docstrings.md --- src/Data/abstract_control_law.jl | 36 ++++++++++++++++++++++ src/Data/abstract_pseudo_hamiltonian.jl | 21 +++++++++++++ src/Data/control_law.jl | 11 +++++++ src/Data/pseudo_hamiltonian.jl | 25 +++++++++++++++ src/Traits/feedback.jl | 41 +++++++++---------------- 5 files changed, 107 insertions(+), 27 deletions(-) diff --git a/src/Data/abstract_control_law.jl b/src/Data/abstract_control_law.jl index 3c085444..95d41f82 100644 --- a/src/Data/abstract_control_law.jl +++ b/src/Data/abstract_control_law.jl @@ -60,6 +60,9 @@ $(TYPEDSIGNATURES) Indicates that all `AbstractControlLaw` types support time-dependence queries. +# Returns +- `true`: Always returns `true` for control law types. + See also: [`CTBase.Traits.time_dependence`](@ref), [`CTBase.Data.AbstractControlLaw`](@ref). """ function Traits.has_time_dependence_trait(::AbstractControlLaw) @@ -71,6 +74,9 @@ $(TYPEDSIGNATURES) Indicates that all `AbstractControlLaw` types support variable-dependence queries. +# Returns +- `true`: Always returns `true` for control law types. + See also: [`CTBase.Traits.variable_dependence`](@ref), [`CTBase.Data.AbstractControlLaw`](@ref). """ function Traits.has_variable_dependence_trait(::AbstractControlLaw) @@ -82,6 +88,12 @@ $(TYPEDSIGNATURES) Return the time-dependence trait of a control law. +# Arguments +- `cl::AbstractControlLaw`: The control law object. + +# Returns +- `TD`: The time-dependence type (`Autonomous` or `NonAutonomous`). + See also: [`CTBase.Traits.time_dependence`](@ref), [`CTBase.Traits.TimeDependence`](@ref). """ function Traits.time_dependence( @@ -95,6 +107,12 @@ $(TYPEDSIGNATURES) Return the variable-dependence trait of a control law. +# Arguments +- `cl::AbstractControlLaw`: The control law object. + +# Returns +- `VD`: The variable-dependence type (`Fixed` or `NonFixed`). + See also: [`CTBase.Traits.variable_dependence`](@ref), [`CTBase.Traits.VariableDependence`](@ref). """ function Traits.variable_dependence( @@ -115,6 +133,9 @@ Return the dynamics trait of an open-loop control law, namely [`CTBase.Traits.St Open-loop and closed-loop control laws do not involve the costate, so they are associated with state dynamics. +# Returns +- `CTBase.Traits.StateDynamics`: The dynamics trait. + See also: [`CTBase.Traits.dynamics_trait`](@ref), [`CTBase.Traits.StateDynamics`](@ref). """ Traits.dynamics_trait(::AbstractControlLaw{<:Traits.OpenLoopFeedback}) = Traits.StateDynamics @@ -124,6 +145,9 @@ $(TYPEDSIGNATURES) Return the dynamics trait of a closed-loop control law, namely [`CTBase.Traits.StateDynamics`](@ref). +# Returns +- `CTBase.Traits.StateDynamics`: The dynamics trait. + See also: [`CTBase.Traits.dynamics_trait`](@ref), [`CTBase.Traits.StateDynamics`](@ref). """ Traits.dynamics_trait(::AbstractControlLaw{<:Traits.ClosedLoopFeedback}) = Traits.StateDynamics @@ -136,6 +160,9 @@ Return the dynamics trait of a dynamic closed-loop control law, namely [`CTBase. Dynamic closed-loop control laws depend on the costate, so they are associated with Hamiltonian dynamics. +# Returns +- `CTBase.Traits.HamiltonianDynamics`: The dynamics trait. + See also: [`CTBase.Traits.dynamics_trait`](@ref), [`CTBase.Traits.HamiltonianDynamics`](@ref). """ Traits.dynamics_trait(::AbstractControlLaw{<:Traits.DynClosedLoopFeedback}) = Traits.HamiltonianDynamics @@ -149,6 +176,9 @@ $(TYPEDSIGNATURES) Return `true` if the control law is open-loop, `false` otherwise. +# Returns +- `Bool`: `true` if the feedback trait is `OpenLoopFeedback`, `false` otherwise. + See also: [`CTBase.Traits.OpenLoopFeedback`](@ref), [`CTBase.Traits.feedback`](@ref). """ Traits.is_open_loop(::AbstractControlLaw) = false @@ -159,6 +189,9 @@ $(TYPEDSIGNATURES) Return `true` if the control law is closed-loop, `false` otherwise. +# Returns +- `Bool`: `true` if the feedback trait is `ClosedLoopFeedback`, `false` otherwise. + See also: [`CTBase.Traits.ClosedLoopFeedback`](@ref), [`CTBase.Traits.feedback`](@ref). """ Traits.is_closed_loop(::AbstractControlLaw) = false @@ -169,6 +202,9 @@ $(TYPEDSIGNATURES) Return `true` if the control law is dynamic closed-loop, `false` otherwise. +# Returns +- `Bool`: `true` if the feedback trait is `DynClosedLoopFeedback`, `false` otherwise. + See also: [`CTBase.Traits.DynClosedLoopFeedback`](@ref), [`CTBase.Traits.feedback`](@ref). """ Traits.is_dyn_closed_loop(::AbstractControlLaw) = false diff --git a/src/Data/abstract_pseudo_hamiltonian.jl b/src/Data/abstract_pseudo_hamiltonian.jl index 6056ed61..9f502a0e 100644 --- a/src/Data/abstract_pseudo_hamiltonian.jl +++ b/src/Data/abstract_pseudo_hamiltonian.jl @@ -41,6 +41,9 @@ $(TYPEDSIGNATURES) Indicates that all `AbstractPseudoHamiltonian` types support time-dependence queries. +# Returns +- `true`: Always returns `true` for pseudo-Hamiltonian types. + See also: [`CTBase.Traits.time_dependence`](@ref), [`CTBase.Data.AbstractPseudoHamiltonian`](@ref). """ function Traits.has_time_dependence_trait(::AbstractPseudoHamiltonian) @@ -52,6 +55,9 @@ $(TYPEDSIGNATURES) Indicates that all `AbstractPseudoHamiltonian` types support variable-dependence queries. +# Returns +- `true`: Always returns `true` for pseudo-Hamiltonian types. + See also: [`CTBase.Traits.variable_dependence`](@ref), [`CTBase.Data.AbstractPseudoHamiltonian`](@ref). """ function Traits.has_variable_dependence_trait(::AbstractPseudoHamiltonian) @@ -63,6 +69,12 @@ $(TYPEDSIGNATURES) Return the time-dependence trait of a pseudo-Hamiltonian. +# Arguments +- `h̃::AbstractPseudoHamiltonian`: The pseudo-Hamiltonian object. + +# Returns +- `TD`: The time-dependence type (`Autonomous` or `NonAutonomous`). + See also: [`CTBase.Traits.time_dependence`](@ref), [`CTBase.Traits.TimeDependence`](@ref). """ function Traits.time_dependence( @@ -76,6 +88,12 @@ $(TYPEDSIGNATURES) Return the variable-dependence trait of a pseudo-Hamiltonian. +# Arguments +- `h̃::AbstractPseudoHamiltonian`: The pseudo-Hamiltonian object. + +# Returns +- `VD`: The variable-dependence type (`Fixed` or `NonFixed`). + See also: [`CTBase.Traits.variable_dependence`](@ref), [`CTBase.Traits.VariableDependence`](@ref). """ function Traits.variable_dependence( @@ -90,6 +108,9 @@ $(TYPEDSIGNATURES) Return the dynamics trait of an `AbstractPseudoHamiltonian`, namely [`CTBase.Traits.HamiltonianDynamics`](@ref). +# Returns +- `CTBase.Traits.HamiltonianDynamics`: The dynamics trait. + See also: [`CTBase.Traits.dynamics_trait`](@ref), [`CTBase.Data.AbstractPseudoHamiltonian`](@ref). """ Traits.dynamics_trait(::AbstractPseudoHamiltonian) = Traits.HamiltonianDynamics diff --git a/src/Data/control_law.jl b/src/Data/control_law.jl index ca524ee9..0b89e585 100644 --- a/src/Data/control_law.jl +++ b/src/Data/control_law.jl @@ -286,6 +286,10 @@ $(TYPEDSIGNATURES) Display a compact representation of a `ControlLaw` showing its traits and call signatures. +# Arguments +- `io::IO`: The IO stream. +- `cl::ControlLaw`: The control law object. + # Output Displays three lines: - Header with feedback, time, and variable dependence traits @@ -308,6 +312,13 @@ $(TYPEDSIGNATURES) Display a `ControlLaw` in the REPL with the same format as the compact `show`. +This method is called automatically when displaying a control law in the Julia REPL. + +# Arguments +- `io::IO`: The IO stream. +- `mime::MIME"text/plain"`: The MIME type. +- `cl::ControlLaw`: The control law object. + See also: [`CTBase.Data.ControlLaw`](@ref). """ function Base.show(io::IO, ::MIME"text/plain", cl::ControlLaw{F,FB,TD,VD}) where {F,FB,TD,VD} diff --git a/src/Data/pseudo_hamiltonian.jl b/src/Data/pseudo_hamiltonian.jl index 4144caba..e455f5d9 100644 --- a/src/Data/pseudo_hamiltonian.jl +++ b/src/Data/pseudo_hamiltonian.jl @@ -93,6 +93,14 @@ $(TYPEDSIGNATURES) Typed constructor for `PseudoHamiltonian` with explicit trait types. +# Arguments +- `f`: The pseudo-Hamiltonian function. +- `::Type{TD}`: The time-dependence trait type. +- `::Type{VD}`: The variable-dependence trait type. + +# Returns +- `PseudoHamiltonian`: A pseudo-Hamiltonian with the specified traits. + See also: [`CTBase.Data.PseudoHamiltonian`](@ref). """ function PseudoHamiltonian( @@ -140,6 +148,16 @@ $(TYPEDSIGNATURES) Display a compact representation of a `PseudoHamiltonian` showing its traits and call signatures. +# Arguments +- `io::IO`: The IO stream. +- `h̃::PseudoHamiltonian`: The pseudo-Hamiltonian object. + +# Output +Displays three lines: +- Header with time and variable dependence traits +- Natural call signature +- Uniform call signature + See also: [`CTBase.Data.PseudoHamiltonian`](@ref). """ function Base.show(io::IO, ::PseudoHamiltonian{F,TD,VD}) where {F,TD,VD} @@ -156,6 +174,13 @@ $(TYPEDSIGNATURES) Display a `PseudoHamiltonian` in the REPL with the same format as the compact `show`. +This method is called automatically when displaying a pseudo-Hamiltonian in the Julia REPL. + +# Arguments +- `io::IO`: The IO stream. +- `mime::MIME"text/plain"`: The MIME type. +- `h̃::PseudoHamiltonian`: The pseudo-Hamiltonian object. + See also: [`CTBase.Data.PseudoHamiltonian`](@ref). """ function Base.show(io::IO, ::MIME"text/plain", h̃::PseudoHamiltonian{F,TD,VD}) where {F,TD,VD} diff --git a/src/Traits/feedback.jl b/src/Traits/feedback.jl index a9f67756..2cb2ccaa 100644 --- a/src/Traits/feedback.jl +++ b/src/Traits/feedback.jl @@ -16,12 +16,7 @@ of the concrete data type (e.g. `ControlLaw{F,FB,TD,VD}`) by the `feedback` accessor. No `has_feedback_trait` guard is provided; calling `feedback` on a type that does not implement it yields a standard `MethodError`. -# See also - -- [`CTBase.Traits.OpenLoopFeedback`](@ref) -- [`CTBase.Traits.ClosedLoopFeedback`](@ref) -- [`CTBase.Traits.DynClosedLoopFeedback`](@ref) -- [`CTBase.Traits.feedback`](@ref) +See also: [`CTBase.Traits.OpenLoopFeedback`](@ref), [`CTBase.Traits.ClosedLoopFeedback`](@ref), [`CTBase.Traits.DynClosedLoopFeedback`](@ref), [`CTBase.Traits.feedback`](@ref). """ abstract type AbstractFeedback <: AbstractTrait end @@ -35,11 +30,7 @@ An open-loop control law has the form `u(t)` (or `u(t, v)` for variable problems). The trajectory is determined entirely by the pre-specified control function, without feedback from the current state. -# See also - -- [`CTBase.Traits.ClosedLoopFeedback`](@ref) -- [`CTBase.Traits.DynClosedLoopFeedback`](@ref) -- [`CTBase.Traits.AbstractFeedback`](@ref) +See also: [`CTBase.Traits.ClosedLoopFeedback`](@ref), [`CTBase.Traits.DynClosedLoopFeedback`](@ref), [`CTBase.Traits.AbstractFeedback`](@ref). """ struct OpenLoopFeedback <: AbstractFeedback end @@ -53,11 +44,7 @@ A closed-loop control law has the form `u(t, x)` (or `u(t, x, v)` for variable problems). The control is a function of the current state, providing static state feedback without costate information. -# See also - -- [`CTBase.Traits.OpenLoopFeedback`](@ref) -- [`CTBase.Traits.DynClosedLoopFeedback`](@ref) -- [`CTBase.Traits.AbstractFeedback`](@ref) +See also: [`CTBase.Traits.OpenLoopFeedback`](@ref), [`CTBase.Traits.DynClosedLoopFeedback`](@ref), [`CTBase.Traits.AbstractFeedback`](@ref). """ struct ClosedLoopFeedback <: AbstractFeedback end @@ -72,11 +59,7 @@ for variable problems). The control is a function of the full Hamiltonian state, providing dynamic feedback that uses costate information — typically derived from the pseudo-Hamiltonian maximisation condition. -# See also - -- [`CTBase.Traits.OpenLoopFeedback`](@ref) -- [`CTBase.Traits.ClosedLoopFeedback`](@ref) -- [`CTBase.Traits.AbstractFeedback`](@ref) +See also: [`CTBase.Traits.OpenLoopFeedback`](@ref), [`CTBase.Traits.ClosedLoopFeedback`](@ref), [`CTBase.Traits.AbstractFeedback`](@ref). """ struct DynClosedLoopFeedback <: AbstractFeedback end @@ -89,12 +72,7 @@ Methods are defined on concrete types in `Data` (e.g. `AbstractControlLaw`). The trait value is one of [`CTBase.Traits.OpenLoopFeedback`](@ref), [`CTBase.Traits.ClosedLoopFeedback`](@ref), or [`CTBase.Traits.DynClosedLoopFeedback`](@ref). -# See also - -- [`CTBase.Traits.AbstractFeedback`](@ref) -- [`CTBase.Traits.OpenLoopFeedback`](@ref) -- [`CTBase.Traits.ClosedLoopFeedback`](@ref) -- [`CTBase.Traits.DynClosedLoopFeedback`](@ref) +See also: [`CTBase.Traits.AbstractFeedback`](@ref), [`CTBase.Traits.OpenLoopFeedback`](@ref), [`CTBase.Traits.ClosedLoopFeedback`](@ref), [`CTBase.Traits.DynClosedLoopFeedback`](@ref). """ function feedback end @@ -105,6 +83,9 @@ Return `true` if `x` has open-loop feedback. Methods are defined on concrete types in `Data` (e.g. `AbstractControlLaw`). +# Returns +- `Bool`: `true` if the feedback trait is `OpenLoopFeedback`, `false` otherwise. + See also: [`CTBase.Traits.OpenLoopFeedback`](@ref), [`CTBase.Traits.feedback`](@ref). """ function is_open_loop end @@ -116,6 +97,9 @@ Return `true` if `x` has closed-loop feedback. Methods are defined on concrete types in `Data` (e.g. `AbstractControlLaw`). +# Returns +- `Bool`: `true` if the feedback trait is `ClosedLoopFeedback`, `false` otherwise. + See also: [`CTBase.Traits.ClosedLoopFeedback`](@ref), [`CTBase.Traits.feedback`](@ref). """ function is_closed_loop end @@ -127,6 +111,9 @@ Return `true` if `x` has dynamic closed-loop feedback. Methods are defined on concrete types in `Data` (e.g. `AbstractControlLaw`). +# Returns +- `Bool`: `true` if the feedback trait is `DynClosedLoopFeedback`, `false` otherwise. + See also: [`CTBase.Traits.DynClosedLoopFeedback`](@ref), [`CTBase.Traits.feedback`](@ref). """ function is_dyn_closed_loop end From 9fdea26fa1fa515edf113915c495923fa94d38d2 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Sat, 4 Jul 2026 22:27:03 +0200 Subject: [PATCH 5/6] docs: document new features in guide pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - traits.md: add Feedback trait family section (AbstractFeedback, OpenLoopFeedback, ClosedLoopFeedback, DynClosedLoopFeedback) with type-parameter-only contract note, predicates, and dynamics trait mapping; add Feedback to Other families table and accessor/predicate summary - data.md: add PseudoHamiltonian section (construction, calling, dynamics trait); add ControlLaw section (OpenLoop/ClosedLoop/DynClosedLoop constructors, call signatures, feedback→dynamics mapping); update overview table, typed constructors, and See also - differentiation.md: update contract count to nine methods; add pseudo_hamiltonian_gradient and pseudo_hamiltonian_control_gradient section with examples; update extension note and See also --- docs/src/guide/data.md | 130 ++++++++++++++++++++++++++++-- docs/src/guide/differentiation.md | 41 +++++++++- docs/src/guide/traits.md | 49 +++++++++++ 3 files changed, 210 insertions(+), 10 deletions(-) diff --git a/docs/src/guide/data.md b/docs/src/guide/data.md index 72d65a0b..ca7d841a 100644 --- a/docs/src/guide/data.md +++ b/docs/src/guide/data.md @@ -22,10 +22,13 @@ using LinearAlgebra |---|---|---| | [`Data.VectorField`](@ref CTBase.Data.VectorField) | ``X : \mathcal{X} \to \mathbb{R}^n`` | `X(x)` | | [`Data.Hamiltonian`](@ref CTBase.Data.Hamiltonian) | ``H : T^*\mathcal{X} \to \mathbb{R}`` | `H(x, p)` | +| [`Data.PseudoHamiltonian`](@ref CTBase.Data.PseudoHamiltonian) | ``\tilde{H} : T^*\mathcal{X} \times \mathcal{U} \to \mathbb{R}`` | `H̃(x, p, u)` | | [`Data.HamiltonianVectorField`](@ref CTBase.Data.HamiltonianVectorField) | ``\vec{H} : T^*\mathcal{X} \to \mathbb{R}^{2n}`` | `HVF(x, p)` | +| [`Data.ControlLaw`](@ref CTBase.Data.ControlLaw) | ``u : \cdots \to \mathcal{U}`` | `u()` / `u(x)` / `u(x, p)` | -All three share the same trait axes — time dependence, variable dependence and -mutability. See [Traits](traits.md) for the full call-signature tables. +All five share the same trait axes — time dependence and variable dependence. +`VectorField` and `HamiltonianVectorField` also carry a mutability trait; +`ControlLaw` carries a feedback trait (see [Traits](traits.md)). --- @@ -204,6 +207,117 @@ For a plain user-supplied function that does not accept `variable_costate`, pass --- +## PseudoHamiltonian + +A `PseudoHamiltonian` wraps a scalar function ``\tilde{H}(x, p, u) \in \mathbb{R}`` +that extends the standard Hamiltonian with an explicit control argument ``u``. + +```math +\tilde{H} : T^*\mathcal{X} \times \mathcal{U} \to \mathbb{R}, \quad (x, p, u) \mapsto \tilde{H}(x, p, u). +``` + +Unlike [`Data.Hamiltonian`](@ref CTBase.Data.Hamiltonian), which encodes the +control implicitly, a pseudo-Hamiltonian takes the control as an additional +argument. This enables dynamic closed-loop flows where the control is computed +from the pseudo-Hamiltonian's maximisation condition (PMP stationarity: +``\partial \tilde{H} / \partial u = 0``). + +### Construction + +```@example data +# Autonomous, fixed (default): H̃(x, p, u) +ph1 = Data.PseudoHamiltonian((x, p, u) -> dot(p, x) + u^2) + +# Non-autonomous, fixed: H̃(t, x, p, u) +ph2 = Data.PseudoHamiltonian((t, x, p, u) -> t * dot(p, x) + u^2; is_autonomous=false) + +# Autonomous, non-fixed: H̃(x, p, u, v) +ph3 = Data.PseudoHamiltonian((x, p, u, v) -> dot(p, x) + u^2 + v[1]; is_variable=true) + +ph1 +``` + +### Calling + +```@example data +x0, p0, u0 = [1.0, 0.5], [0.3, 0.7], 2.0 + +ph1(x0, p0, u0) # natural call +ph1(0.0, x0, p0, u0, nothing) # uniform call +ph2(0.5, x0, p0, u0) # non-autonomous +``` + +The dynamics trait of a `PseudoHamiltonian` is always +[`Traits.HamiltonianDynamics`](@ref CTBase.Traits.HamiltonianDynamics), since +pseudo-Hamiltonians involve both state and costate. + +--- + +## ControlLaw + +A `ControlLaw` wraps a function ``u(\cdots)`` that provides the control input for +an optimal control problem. The feedback trait (see [Traits](traits.md)) +determines which arguments the control law depends on. + +Three user-facing constructors fix the feedback trait: + +| Constructor | Feedback trait | Natural signature (autonomous/fixed) | +|---|---|---| +| [`Data.OpenLoop`](@ref CTBase.Data.OpenLoop) | `OpenLoopFeedback` | `u()` | +| [`Data.ClosedLoop`](@ref CTBase.Data.ClosedLoop) | `ClosedLoopFeedback` | `u(x)` | +| [`Data.DynClosedLoop`](@ref CTBase.Data.DynClosedLoop) | `DynClosedLoopFeedback` | `u(x, p)` | + +### Construction + +```@example data +# Open-loop, autonomous, fixed (default): u() +u_ol = Data.OpenLoop(() -> 1.0) + +# Closed-loop, autonomous, fixed: u(x) +u_cl = Data.ClosedLoop(x -> -x) + +# Dynamic closed-loop, autonomous, fixed: u(x, p) +u_dcl = Data.DynClosedLoop((x, p) -> -x - p) + +u_ol +``` + +Non-autonomous and variable variants are constructed with the same keywords as +other data types: + +```@example data +u_ol_na = Data.OpenLoop((t, v) -> t * v; is_autonomous=false, is_variable=true) +``` + +### Calling + +Every `ControlLaw` is callable via its **natural** signature (matching the +traits) and via a **uniform** signature that depends on the feedback trait: + +| Feedback | Natural `(Aut, Fixed)` | Uniform | +|---|---|---| +| `OpenLoop` | `u()` | `u(t, v)` | +| `ClosedLoop` | `u(x)` | `u(t, x, v)` | +| `DynClosedLoop` | `u(x, p)` | `u(t, x, p, v)` | + +```@example data +u_ol() # natural call +u_ol(0.0, nothing) # uniform call + +u_cl(x0) # natural call +u_cl(0.0, x0, nothing) # uniform call + +u_dcl(x0, p0) # natural call +u_dcl(0.0, x0, p0, nothing) # uniform call +``` + +The feedback trait determines the dynamics trait: open-loop and closed-loop +control laws carry [`Traits.StateDynamics`](@ref CTBase.Traits.StateDynamics), +while dynamic closed-loop control laws carry +[`Traits.HamiltonianDynamics`](@ref CTBase.Traits.HamiltonianDynamics). + +--- + ## Querying traits Because the trait metadata lives in the type, it can be recovered from any data @@ -251,15 +365,19 @@ Traits.time_dependence(vf_typed) ``` The same positional form exists for [`Data.Hamiltonian`](@ref CTBase.Data.Hamiltonian) -(`Hamiltonian(f, TD, VD)`) and +(`Hamiltonian(f, TD, VD)`), +[`Data.PseudoHamiltonian`](@ref CTBase.Data.PseudoHamiltonian) +(`PseudoHamiltonian(f, TD, VD)`), [`Data.HamiltonianVectorField`](@ref CTBase.Data.HamiltonianVectorField) -(`HamiltonianVectorField(f, TD, VD, MD)`). +(`HamiltonianVectorField(f, TD, VD, MD)`), and +[`Data.ControlLaw`](@ref CTBase.Data.ControlLaw) +(`ControlLaw(f, FB, TD, VD)`). --- ## See also -- [`Data.VectorField`](@ref CTBase.Data.VectorField), [`Data.Hamiltonian`](@ref CTBase.Data.Hamiltonian), [`Data.HamiltonianVectorField`](@ref CTBase.Data.HamiltonianVectorField) — concrete data types. -- [`Data.AbstractVectorField`](@ref CTBase.Data.AbstractVectorField), [`Data.AbstractHamiltonian`](@ref CTBase.Data.AbstractHamiltonian), [`Data.AbstractHamiltonianVectorField`](@ref CTBase.Data.AbstractHamiltonianVectorField) — abstract supertypes. +- [`Data.VectorField`](@ref CTBase.Data.VectorField), [`Data.Hamiltonian`](@ref CTBase.Data.Hamiltonian), [`Data.PseudoHamiltonian`](@ref CTBase.Data.PseudoHamiltonian), [`Data.HamiltonianVectorField`](@ref CTBase.Data.HamiltonianVectorField), [`Data.ControlLaw`](@ref CTBase.Data.ControlLaw) — concrete data types. +- [`Data.AbstractVectorField`](@ref CTBase.Data.AbstractVectorField), [`Data.AbstractHamiltonian`](@ref CTBase.Data.AbstractHamiltonian), [`Data.AbstractPseudoHamiltonian`](@ref CTBase.Data.AbstractPseudoHamiltonian), [`Data.AbstractHamiltonianVectorField`](@ref CTBase.Data.AbstractHamiltonianVectorField), [`Data.AbstractControlLaw`](@ref CTBase.Data.AbstractControlLaw) — abstract supertypes. - [Traits](traits.md) — the three trait axes, the dynamics trait, and call-signature tables. - [Exceptions](exceptions.md) — `PreconditionError` and `IncorrectArgument`, raised by the constructors and the `variable_costate` path. diff --git a/docs/src/guide/differentiation.md b/docs/src/guide/differentiation.md index a5dc2088..02afdabd 100644 --- a/docs/src/guide/differentiation.md +++ b/docs/src/guide/differentiation.md @@ -42,14 +42,15 @@ The concrete strategy stores a single option, `:ad_backend`, holding an `AutoForwardDiff()`). `ADTypes` is a hard dependency of CTBase, so the default is always available. -The contract has seven methods. +The contract has nine methods. [`Differentiation.ad_backend`](@ref CTBase.Differentiation.ad_backend) — the accessor returning the wrapped ADTypes backend — is resolved in core and always available; the -six differentiation primitives below live in an extension. +eight differentiation primitives below live in an extension. !!! note "The differentiation methods live in an extension" The contract methods (`gradient`, `derivative`, `differentiate`, - `pushforward`, `hamiltonian_gradient`, `variable_gradient`) are implemented in + `pushforward`, `hamiltonian_gradient`, `variable_gradient`, + `pseudo_hamiltonian_gradient`, `pseudo_hamiltonian_control_gradient`) are implemented in the `CTBaseDifferentiationInterface` **package extension**. They become available only once `DifferentiationInterface` *and* a concrete AD package (e.g. `ForwardDiff`) are loaded. Without the extension, every contract method @@ -164,6 +165,38 @@ hv = Data.Hamiltonian((x, p, v) -> 0.5 * (sum(abs2, x) + sum(abs2, p) + sum(abs2 Differentiation.variable_gradient(backend, hv, 0.0, [1.0, 2.0], [3.0, 4.0], [5.0, 6.0]) ``` +## Pseudo-Hamiltonian gradients + +The two pseudo-Hamiltonian methods operate directly on a +[`Data.PseudoHamiltonian`](@ref CTBase.Data.PseudoHamiltonian) (see the [Data](data.md) +guide). +[`Differentiation.pseudo_hamiltonian_gradient`](@ref CTBase.Differentiation.pseudo_hamiltonian_gradient) +returns `(∂H̃/∂x, ∂H̃/∂p)`, **non-negated** — the caller applies the signs of +Hamilton's equations: + +```@example diff +# H̃(x, p, u) = ½(‖x‖² + ‖p‖²) + u² → ∂H̃/∂x = x, ∂H̃/∂p = p +ph = Data.PseudoHamiltonian((x, p, u) -> 0.5 * (sum(abs2, x) + sum(abs2, p)) + u^2) +∂x, ∂p = Differentiation.pseudo_hamiltonian_gradient(backend, ph, 0.0, [1.0, 2.0], [3.0, 4.0], 2.0, nothing) +(∂x, ∂p) +``` + +[`Differentiation.pseudo_hamiltonian_control_gradient`](@ref CTBase.Differentiation.pseudo_hamiltonian_control_gradient) +returns `∂H̃/∂u`, which vanishes along optimal trajectories by the Pontryagin +Maximum Principle stationarity condition: + +```@example diff +# ∂H̃/∂u = 2u = 4.0 +Differentiation.pseudo_hamiltonian_control_gradient(backend, ph, 0.0, [1.0, 2.0], [3.0, 4.0], 2.0, nothing) +``` + +For a non-fixed pseudo-Hamiltonian, the `v` argument carries the variable: + +```@example diff +phv = Data.PseudoHamiltonian((x, p, u, v) -> 0.5 * (sum(abs2, x) + sum(abs2, p)) + u^2 + v[1]; is_variable=true) +Differentiation.pseudo_hamiltonian_control_gradient(backend, phv, 0.0, [1.0, 2.0], [3.0, 4.0], 2.0, [5.0]) +``` + ## The contract without the extension Every contract method has a fallback on `AbstractADBackend` that throws @@ -186,5 +219,5 @@ end # hide - [`Differentiation.AbstractADBackend`](@ref CTBase.Differentiation.AbstractADBackend), [`Differentiation.DifferentiationInterface`](@ref CTBase.Differentiation.DifferentiationInterface) — the strategy types. - [Implementing a Strategy](@ref), [Options System](@ref) — the contract these build on. -- [Data](data.md) — `Hamiltonian` wrappers consumed by the gradient methods. +- [Data](data.md) — `Hamiltonian` and `PseudoHamiltonian` wrappers consumed by the gradient methods. - [Exceptions](exceptions.md) — `NotImplemented`, raised by the contract fallbacks. diff --git a/docs/src/guide/traits.md b/docs/src/guide/traits.md index ded5cea0..70b9fbbe 100644 --- a/docs/src/guide/traits.md +++ b/docs/src/guide/traits.md @@ -81,6 +81,53 @@ Does a function allocate a new output, or write into a pre-allocated buffer? | [`Traits.OutOfPlace`](@ref CTBase.Traits.OutOfPlace) | returns a new value | | [`Traits.InPlace`](@ref CTBase.Traits.InPlace) | writes into a buffer (first arg) | +### Feedback + +How does a control law close the loop? The feedback trait encodes which arguments +the control law depends on, and therefore which dynamics trait it implies. + +| Type | Meaning | Control law signature | +|---|---|---| +| [`Traits.OpenLoopFeedback`](@ref CTBase.Traits.OpenLoopFeedback) | time (and variable) only | `u(t[, v])` | +| [`Traits.ClosedLoopFeedback`](@ref CTBase.Traits.ClosedLoopFeedback) | time and state | `u(t, x[, v])` | +| [`Traits.DynClosedLoopFeedback`](@ref CTBase.Traits.DynClosedLoopFeedback) | time, state, and costate | `u(t, x, p[, v])` | + +All three are concrete subtypes of +[`Traits.AbstractFeedback`](@ref CTBase.Traits.AbstractFeedback): + +```@repl traits +Traits.OpenLoopFeedback <: Traits.AbstractFeedback +Traits.ClosedLoopFeedback <: Traits.AbstractFeedback +Traits.DynClosedLoopFeedback <: Traits.AbstractFeedback +``` + +!!! note "Type-parameter-only contract" + Unlike time-, variable-, and control-dependence (which use the two-method + opt-in contract), feedback follows a **type-parameter-only** contract: the + trait value is read from a type parameter of the concrete data type (e.g. + `ControlLaw{F,FB,TD,VD}`) by the + [`Traits.feedback`](@ref CTBase.Traits.feedback) accessor. No + `has_feedback_trait` guard is provided; calling `feedback` on a type that + does not implement it yields a standard `MethodError`. + +The boolean predicates +[`Traits.is_open_loop`](@ref CTBase.Traits.is_open_loop), +[`Traits.is_closed_loop`](@ref CTBase.Traits.is_closed_loop), and +[`Traits.is_dyn_closed_loop`](@ref CTBase.Traits.is_dyn_closed_loop) dispatch +on the feedback type parameter: + +```@repl traits +using CTBase.Data +u = Data.OpenLoop(() -> 1.0) +Traits.feedback(u) +Traits.is_open_loop(u) +``` + +The feedback trait also determines the dynamics trait: open-loop and closed-loop +control laws carry [`Traits.StateDynamics`](@ref CTBase.Traits.StateDynamics) +(no costate), while dynamic closed-loop control laws carry +[`Traits.HamiltonianDynamics`](@ref CTBase.Traits.HamiltonianDynamics). + ### Other families These traits encode properties that are fixed at construction time and carried @@ -93,6 +140,7 @@ below — they are passed directly as type arguments. | Dynamics | [`Traits.StateDynamics`](@ref CTBase.Traits.StateDynamics), [`Traits.HamiltonianDynamics`](@ref CTBase.Traits.HamiltonianDynamics), [`Traits.AugmentedHamiltonianDynamics`](@ref CTBase.Traits.AugmentedHamiltonianDynamics) | | Automatic differentiation | [`Traits.WithAD`](@ref CTBase.Traits.WithAD), [`Traits.WithoutAD`](@ref CTBase.Traits.WithoutAD) | | Variable costate | [`Traits.SupportsVariableCostate`](@ref CTBase.Traits.SupportsVariableCostate), [`Traits.NoVariableCostate`](@ref CTBase.Traits.NoVariableCostate) | +| Feedback | [`Traits.OpenLoopFeedback`](@ref CTBase.Traits.OpenLoopFeedback), [`Traits.ClosedLoopFeedback`](@ref CTBase.Traits.ClosedLoopFeedback), [`Traits.DynClosedLoopFeedback`](@ref CTBase.Traits.DynClosedLoopFeedback) | !!! note "Abstract tags vs. concrete tags" Time-dependence tags (`Autonomous`, `NonAutonomous`) are **abstract** types @@ -189,6 +237,7 @@ end # hide | [`Traits.ad_trait`](@ref CTBase.Traits.ad_trait) | — | | [`Traits.variable_costate_trait`](@ref CTBase.Traits.variable_costate_trait) | — | | [`Traits.dynamics_trait`](@ref CTBase.Traits.dynamics_trait) | — | +| [`Traits.feedback`](@ref CTBase.Traits.feedback) | `is_open_loop`, `is_closed_loop`, `is_dyn_closed_loop` | ## See Also From 6d553505efdd423b944fb2f58f882339f2f621ed Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Sat, 4 Jul 2026 22:28:38 +0200 Subject: [PATCH 6/6] chore: bump version to 0.26.1-beta Update CHANGELOGS.md with new features (Feedback trait family, PseudoHamiltonian, ControlLaw, pseudo-Hamiltonian gradient methods), documentation, and testing entries. Add non-breaking note to BREAKINGS.md. --- BREAKINGS.md | 16 +++++++++++++++ CHANGELOGS.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++ Project.toml | 2 +- 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/BREAKINGS.md b/BREAKINGS.md index 8c80ad59..6216f9fb 100644 --- a/BREAKINGS.md +++ b/BREAKINGS.md @@ -26,6 +26,22 @@ This document outlines all breaking changes introduced in CTBase v0.18.0-beta co - `NotStored` / `NotStoredType` are unchanged and remain extraction-internal to `CTBase.Options` (the defining file was renamed `not_provided.jl` → `not_stored.jl`). +## Non-breaking note (0.26.1-beta) + +- **Traits**: New `Feedback` trait family added for encoding how a control law closes the loop + - **New trait family**: `AbstractFeedback` with tags `OpenLoopFeedback`, `ClosedLoopFeedback`, `DynClosedLoopFeedback` + - **Type-parameter-only contract**: trait value read from a type parameter by the `feedback` accessor; no `has_feedback_trait` guard + - **Derived predicates**: `is_open_loop(obj)`, `is_closed_loop(obj)`, `is_dyn_closed_loop(obj)` + - **No breaking changes**: Purely additive. Existing code unaffected. +- **Data**: New `PseudoHamiltonian` and `ControlLaw` data types added + - **PseudoHamiltonian**: scalar function `H̃(t, x, p, u[, v]) → ℝ` with explicit control argument; `AbstractPseudoHamiltonian` supertype and `PseudoHamiltonian` concrete struct + - **ControlLaw**: control law function `u(⋯) → 𝒰` with feedback trait; `AbstractControlLaw` supertype, `ControlLaw` concrete struct, and `OpenLoop`/`ClosedLoop`/`DynClosedLoop` user-facing constructors + - **No breaking changes**: Purely additive. Existing data types unchanged. +- **Differentiation**: New pseudo-Hamiltonian gradient methods added + - `pseudo_hamiltonian_gradient(backend, h̃, t, x, p, u, v) → (∂H̃/∂x, ∂H̃/∂p)` + - `pseudo_hamiltonian_control_gradient(backend, h̃, t, x, p, u, v) → ∂H̃/∂u` + - **No breaking changes**: Purely additive. Existing gradient methods unchanged. + ## Non-breaking note (0.26.0-beta) - **Traits**: New `ControlDependence` family added for encoding control presence in optimal control problems diff --git a/CHANGELOGS.md b/CHANGELOGS.md index 664382df..b8cc9b36 100644 --- a/CHANGELOGS.md +++ b/CHANGELOGS.md @@ -5,6 +5,62 @@ All notable changes to CTBase will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.26.1-beta] - 2026-07-04 + +### ✨ New Features + +#### **Feedback Trait Family** + +- **New `AbstractFeedback` trait family** in `CTBase.Traits` for encoding how a control law closes the loop: + - **Tags**: `OpenLoopFeedback` (time/variable only), `ClosedLoopFeedback` (time + state), `DynClosedLoopFeedback` (time + state + costate) + - **Type-parameter-only contract**: the trait value is read from a type parameter of the concrete data type by the `feedback` accessor; no `has_feedback_trait` guard + - **Derived predicates**: `is_open_loop(obj)`, `is_closed_loop(obj)`, `is_dyn_closed_loop(obj)` dispatch on the feedback type parameter + - **Dynamics trait mapping**: open-loop and closed-loop → `StateDynamics`; dynamic closed-loop → `HamiltonianDynamics` + +#### **PseudoHamiltonian** + +- **New `PseudoHamiltonian` data type** in `CTBase.Data` for scalar functions `H̃(t, x, p, u[, v]) → ℝ` that extend the standard Hamiltonian with an explicit control argument `u`: + - **Abstract supertype**: `AbstractPseudoHamiltonian{TD,VD}` with trait accessors + - **Concrete struct**: `PseudoHamiltonian{F,TD,VD}` with keyword constructor `PseudoHamiltonian(f; is_autonomous, is_variable)` + - **Natural and uniform call signatures** matching the time/variable dependence traits + - **Dynamics trait**: always `HamiltonianDynamics` + +#### **ControlLaw** + +- **New `ControlLaw` data type** in `CTBase.Data` for control law functions `u(⋯) → 𝒰`: + - **Abstract supertype**: `AbstractControlLaw{FB,TD,VD}` with feedback, time, and variable dependence trait accessors + - **Concrete struct**: `ControlLaw{F,FB,TD,VD}` with typed constructor + - **User-facing constructors**: `OpenLoop(f; ...)`, `ClosedLoop(f; ...)`, `DynClosedLoop(f; ...)` fixing the feedback trait + - **Feedback-dependent call signatures**: natural and uniform signatures vary by feedback trait + - **Dynamics trait**: determined by feedback (open/closed-loop → `StateDynamics`; dynamic closed-loop → `HamiltonianDynamics`) + +#### **Pseudo-Hamiltonian Gradient Methods** + +- **New AD contract methods** in `CTBase.Differentiation` for pseudo-Hamiltonian gradients: + - `pseudo_hamiltonian_gradient(backend, h̃, t, x, p, u, v) → (∂H̃/∂x, ∂H̃/∂p)`: non-negated partial derivatives + - `pseudo_hamiltonian_control_gradient(backend, h̃, t, x, p, u, v) → ∂H̃/∂u`: control gradient (vanishes along optimal trajectories by PMP stationarity) + - **Rationale**: splitting `∂H̃/∂u` from `(∂H̃/∂x, ∂H̃/∂p)` reflects the distinct role of the stationarity condition and avoids computing the control gradient when only the state/costate gradients are needed + - **Extension implementation**: `CTBaseDifferentiationInterface` extension provides concrete implementations using `DifferentiationInterface.jl` + - **Fallback**: `NotImplemented` on `AbstractADBackend` without the extension + +### 📚 Documentation + +- **Guide pages updated**: + - `traits.md`: added Feedback trait family section with type-parameter-only contract note, predicates, and dynamics trait mapping; added Feedback to Other families table and accessor/predicate summary + - `data.md`: added PseudoHamiltonian section (construction, calling, dynamics trait) and ControlLaw section (constructors, call signatures, feedback→dynamics mapping); updated overview table, typed constructors, and See also + - `differentiation.md`: updated contract count to nine methods; added Pseudo-Hamiltonian gradients section with examples; updated extension note and See also +- **Docstring compliance**: aligned all docstrings in `feedback.jl`, `abstract_control_law.jl`, `control_law.jl`, `abstract_pseudo_hamiltonian.jl`, `pseudo_hamiltonian.jl`, `abstract_ad_backend.jl`, and `CTBaseDifferentiationInterface.jl` with the Handbook conventions (`See also:` inline lines, `# Returns`/`# Arguments` sections, `# Output` for show methods) +- **API reference**: added `feedback.jl`, `abstract_control_law.jl`, `control_law.jl`, `abstract_pseudo_hamiltonian.jl`, `pseudo_hamiltonian.jl` to auto-generated API pages + +### 🧪 Testing + +- Added tests for `pseudo_hamiltonian_gradient` (return signature `(grad_x, grad_p)`) and `pseudo_hamiltonian_control_gradient` (return `grad_u`), including `NotImplemented` error tests and `DifferentiationInterface` extension tests +- Added `pseudo_hamiltonian_control_gradient` to export-list checks in `test_differentiation_module.jl` + +### ✅ Compatibility + +- **No breaking changes**: purely additive. Existing code unaffected. See [BREAKINGS.md](BREAKINGS.md). + ## [0.26.0-beta] - 2026-06-28 ### ✨ New Features diff --git a/Project.toml b/Project.toml index d4d2c2c5..71ca62e8 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "CTBase" uuid = "54762871-cc72-4466-b8e8-f6c8b58076cd" -version = "0.26.0-beta" +version = "0.26.1-beta" authors = ["Olivier Cots ", "Jean-Baptiste Caillau "] [deps]