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] 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"), ), ), ( 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 diff --git a/ext/CTBaseDifferentiationInterface.jl b/ext/CTBaseDifferentiationInterface.jl index a71c5802..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. @@ -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 +[`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.≺`, +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 # ============================================================================= @@ -184,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/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..95d41f82 --- /dev/null +++ b/src/Data/abstract_control_law.jl @@ -0,0 +1,211 @@ +# ============================================================================= +# 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 ([`CTBase.Traits.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. + +# 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) + return true +end + +""" +$(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) + return true +end + +""" +$(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( + ::AbstractControlLaw{<:Any,TD,<:Traits.VariableDependence} +) where {TD<:Traits.TimeDependence} + return TD +end + +""" +$(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( + ::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 [`CTBase.Traits.StateDynamics`](@ref). + +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 + +""" +$(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 + +""" +$(TYPEDSIGNATURES) + +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. + +# 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 + +# ============================================================================= +# Feedback predicates — dispatch on the FB type parameter +# ============================================================================= + +""" +$(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 +Traits.is_open_loop(::AbstractControlLaw{<:Traits.OpenLoopFeedback}) = true + +""" +$(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 +Traits.is_closed_loop(::AbstractControlLaw{<:Traits.ClosedLoopFeedback}) = true + +""" +$(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 +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..9f502a0e --- /dev/null +++ b/src/Data/abstract_pseudo_hamiltonian.jl @@ -0,0 +1,116 @@ +# ============================================================================= +# 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 +[`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. + +# 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. + +# 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) + return true +end + +""" +$(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) + return true +end + +""" +$(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( + ::AbstractPseudoHamiltonian{TD,<:Traits.VariableDependence} +) where {TD<:Traits.TimeDependence} + return TD +end + +""" +$(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( + ::AbstractPseudoHamiltonian{<:Traits.TimeDependence,VD} +) where {VD<:Traits.VariableDependence} + return VD +end + +""" +$(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 new file mode 100644 index 00000000..0b89e585 --- /dev/null +++ b/src/Data/control_law.jl @@ -0,0 +1,326 @@ +# ============================================================================= +# 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 +[`CTBase.Data.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. + +# Arguments +- `io::IO`: The IO stream. +- `cl::ControlLaw`: The control law object. + +# 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`. + +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} + 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..e455f5d9 --- /dev/null +++ b/src/Data/pseudo_hamiltonian.jl @@ -0,0 +1,188 @@ +# ============================================================================= +# 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. + +# 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( + 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. + +# 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} + 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`. + +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} + 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..9cdd3438 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 +[`CTBase.Differentiation.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 [`CTBase.Differentiation.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..2cb2ccaa --- /dev/null +++ b/src/Traits/feedback.jl @@ -0,0 +1,119 @@ +""" +$(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 +[`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`. + +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 [`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). +""" +function feedback end + +""" +$(TYPEDSIGNATURES) + +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 + +""" +$(TYPEDSIGNATURES) + +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 + +""" +$(TYPEDSIGNATURES) + +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 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()