Skip to content

Commit 6051f5d

Browse files
authored
Merge pull request #472 from control-toolbox/feat/pseudo-hamiltonian-and-control-law
feat: ControlLaw, PseudoHamiltonian, feedback trait, and pseudo-Hamiltonian AD contracts
2 parents 275d2c7 + 6d55350 commit 6051f5d

24 files changed

Lines changed: 2455 additions & 18 deletions

BREAKINGS.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,22 @@ This document outlines all breaking changes introduced in CTBase v0.18.0-beta co
2626
- `NotStored` / `NotStoredType` are unchanged and remain extraction-internal to
2727
`CTBase.Options` (the defining file was renamed `not_provided.jl``not_stored.jl`).
2828

29+
## Non-breaking note (0.26.1-beta)
30+
31+
- **Traits**: New `Feedback` trait family added for encoding how a control law closes the loop
32+
- **New trait family**: `AbstractFeedback` with tags `OpenLoopFeedback`, `ClosedLoopFeedback`, `DynClosedLoopFeedback`
33+
- **Type-parameter-only contract**: trait value read from a type parameter by the `feedback` accessor; no `has_feedback_trait` guard
34+
- **Derived predicates**: `is_open_loop(obj)`, `is_closed_loop(obj)`, `is_dyn_closed_loop(obj)`
35+
- **No breaking changes**: Purely additive. Existing code unaffected.
36+
- **Data**: New `PseudoHamiltonian` and `ControlLaw` data types added
37+
- **PseudoHamiltonian**: scalar function `H̃(t, x, p, u[, v]) → ℝ` with explicit control argument; `AbstractPseudoHamiltonian` supertype and `PseudoHamiltonian` concrete struct
38+
- **ControlLaw**: control law function `u(⋯) → 𝒰` with feedback trait; `AbstractControlLaw` supertype, `ControlLaw` concrete struct, and `OpenLoop`/`ClosedLoop`/`DynClosedLoop` user-facing constructors
39+
- **No breaking changes**: Purely additive. Existing data types unchanged.
40+
- **Differentiation**: New pseudo-Hamiltonian gradient methods added
41+
- `pseudo_hamiltonian_gradient(backend, h̃, t, x, p, u, v) → (∂H̃/∂x, ∂H̃/∂p)`
42+
- `pseudo_hamiltonian_control_gradient(backend, h̃, t, x, p, u, v) → ∂H̃/∂u`
43+
- **No breaking changes**: Purely additive. Existing gradient methods unchanged.
44+
2945
## Non-breaking note (0.26.0-beta)
3046

3147
- **Traits**: New `ControlDependence` family added for encoding control presence in optimal control problems

CHANGELOGS.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,62 @@ All notable changes to CTBase will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.26.1-beta] - 2026-07-04
9+
10+
### ✨ New Features
11+
12+
#### **Feedback Trait Family**
13+
14+
- **New `AbstractFeedback` trait family** in `CTBase.Traits` for encoding how a control law closes the loop:
15+
- **Tags**: `OpenLoopFeedback` (time/variable only), `ClosedLoopFeedback` (time + state), `DynClosedLoopFeedback` (time + state + costate)
16+
- **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
17+
- **Derived predicates**: `is_open_loop(obj)`, `is_closed_loop(obj)`, `is_dyn_closed_loop(obj)` dispatch on the feedback type parameter
18+
- **Dynamics trait mapping**: open-loop and closed-loop → `StateDynamics`; dynamic closed-loop → `HamiltonianDynamics`
19+
20+
#### **PseudoHamiltonian**
21+
22+
- **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`:
23+
- **Abstract supertype**: `AbstractPseudoHamiltonian{TD,VD}` with trait accessors
24+
- **Concrete struct**: `PseudoHamiltonian{F,TD,VD}` with keyword constructor `PseudoHamiltonian(f; is_autonomous, is_variable)`
25+
- **Natural and uniform call signatures** matching the time/variable dependence traits
26+
- **Dynamics trait**: always `HamiltonianDynamics`
27+
28+
#### **ControlLaw**
29+
30+
- **New `ControlLaw` data type** in `CTBase.Data` for control law functions `u(⋯) → 𝒰`:
31+
- **Abstract supertype**: `AbstractControlLaw{FB,TD,VD}` with feedback, time, and variable dependence trait accessors
32+
- **Concrete struct**: `ControlLaw{F,FB,TD,VD}` with typed constructor
33+
- **User-facing constructors**: `OpenLoop(f; ...)`, `ClosedLoop(f; ...)`, `DynClosedLoop(f; ...)` fixing the feedback trait
34+
- **Feedback-dependent call signatures**: natural and uniform signatures vary by feedback trait
35+
- **Dynamics trait**: determined by feedback (open/closed-loop → `StateDynamics`; dynamic closed-loop → `HamiltonianDynamics`)
36+
37+
#### **Pseudo-Hamiltonian Gradient Methods**
38+
39+
- **New AD contract methods** in `CTBase.Differentiation` for pseudo-Hamiltonian gradients:
40+
- `pseudo_hamiltonian_gradient(backend, h̃, t, x, p, u, v) → (∂H̃/∂x, ∂H̃/∂p)`: non-negated partial derivatives
41+
- `pseudo_hamiltonian_control_gradient(backend, h̃, t, x, p, u, v) → ∂H̃/∂u`: control gradient (vanishes along optimal trajectories by PMP stationarity)
42+
- **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
43+
- **Extension implementation**: `CTBaseDifferentiationInterface` extension provides concrete implementations using `DifferentiationInterface.jl`
44+
- **Fallback**: `NotImplemented` on `AbstractADBackend` without the extension
45+
46+
### 📚 Documentation
47+
48+
- **Guide pages updated**:
49+
- `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
50+
- `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
51+
- `differentiation.md`: updated contract count to nine methods; added Pseudo-Hamiltonian gradients section with examples; updated extension note and See also
52+
- **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)
53+
- **API reference**: added `feedback.jl`, `abstract_control_law.jl`, `control_law.jl`, `abstract_pseudo_hamiltonian.jl`, `pseudo_hamiltonian.jl` to auto-generated API pages
54+
55+
### 🧪 Testing
56+
57+
- 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
58+
- Added `pseudo_hamiltonian_control_gradient` to export-list checks in `test_differentiation_module.jl`
59+
60+
### ✅ Compatibility
61+
62+
- **No breaking changes**: purely additive. Existing code unaffected. See [BREAKINGS.md](BREAKINGS.md).
63+
864
## [0.26.0-beta] - 2026-06-28
965

1066
### ✨ New Features

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "CTBase"
22
uuid = "54762871-cc72-4466-b8e8-f6c8b58076cd"
3-
version = "0.26.0-beta"
3+
version = "0.26.1-beta"
44
authors = ["Olivier Cots <olivier.cots@irit.fr>", "Jean-Baptiste Caillau <caillau@univ-cotedazur.fr>"]
55

66
[deps]

docs/api_reference.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ function generate_api_reference(src_dir::String)
5454
joinpath("Data", "hamiltonian.jl"),
5555
joinpath("Data", "abstract_hamiltonian_vector_field.jl"),
5656
joinpath("Data", "hamiltonian_vector_field.jl"),
57+
joinpath("Data", "abstract_control_law.jl"),
58+
joinpath("Data", "control_law.jl"),
59+
joinpath("Data", "abstract_pseudo_hamiltonian.jl"),
60+
joinpath("Data", "pseudo_hamiltonian.jl"),
5761
),
5862
),
5963
(
@@ -175,6 +179,7 @@ function generate_api_reference(src_dir::String)
175179
joinpath("Traits", "time_dependence.jl"),
176180
joinpath("Traits", "variable_dependence.jl"),
177181
joinpath("Traits", "control_dependence.jl"),
182+
joinpath("Traits", "feedback.jl"),
178183
),
179184
),
180185
(

docs/src/guide/data.md

Lines changed: 124 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,13 @@ using LinearAlgebra
2222
|---|---|---|
2323
| [`Data.VectorField`](@ref CTBase.Data.VectorField) | ``X : \mathcal{X} \to \mathbb{R}^n`` | `X(x)` |
2424
| [`Data.Hamiltonian`](@ref CTBase.Data.Hamiltonian) | ``H : T^*\mathcal{X} \to \mathbb{R}`` | `H(x, p)` |
25+
| [`Data.PseudoHamiltonian`](@ref CTBase.Data.PseudoHamiltonian) | ``\tilde{H} : T^*\mathcal{X} \times \mathcal{U} \to \mathbb{R}`` | `H̃(x, p, u)` |
2526
| [`Data.HamiltonianVectorField`](@ref CTBase.Data.HamiltonianVectorField) | ``\vec{H} : T^*\mathcal{X} \to \mathbb{R}^{2n}`` | `HVF(x, p)` |
27+
| [`Data.ControlLaw`](@ref CTBase.Data.ControlLaw) | ``u : \cdots \to \mathcal{U}`` | `u()` / `u(x)` / `u(x, p)` |
2628

27-
All three share the same trait axes — time dependence, variable dependence and
28-
mutability. See [Traits](traits.md) for the full call-signature tables.
29+
All five share the same trait axes — time dependence and variable dependence.
30+
`VectorField` and `HamiltonianVectorField` also carry a mutability trait;
31+
`ControlLaw` carries a feedback trait (see [Traits](traits.md)).
2932

3033
---
3134

@@ -204,6 +207,117 @@ For a plain user-supplied function that does not accept `variable_costate`, pass
204207

205208
---
206209

210+
## PseudoHamiltonian
211+
212+
A `PseudoHamiltonian` wraps a scalar function ``\tilde{H}(x, p, u) \in \mathbb{R}``
213+
that extends the standard Hamiltonian with an explicit control argument ``u``.
214+
215+
```math
216+
\tilde{H} : T^*\mathcal{X} \times \mathcal{U} \to \mathbb{R}, \quad (x, p, u) \mapsto \tilde{H}(x, p, u).
217+
```
218+
219+
Unlike [`Data.Hamiltonian`](@ref CTBase.Data.Hamiltonian), which encodes the
220+
control implicitly, a pseudo-Hamiltonian takes the control as an additional
221+
argument. This enables dynamic closed-loop flows where the control is computed
222+
from the pseudo-Hamiltonian's maximisation condition (PMP stationarity:
223+
``\partial \tilde{H} / \partial u = 0``).
224+
225+
### Construction
226+
227+
```@example data
228+
# Autonomous, fixed (default): H̃(x, p, u)
229+
ph1 = Data.PseudoHamiltonian((x, p, u) -> dot(p, x) + u^2)
230+
231+
# Non-autonomous, fixed: H̃(t, x, p, u)
232+
ph2 = Data.PseudoHamiltonian((t, x, p, u) -> t * dot(p, x) + u^2; is_autonomous=false)
233+
234+
# Autonomous, non-fixed: H̃(x, p, u, v)
235+
ph3 = Data.PseudoHamiltonian((x, p, u, v) -> dot(p, x) + u^2 + v[1]; is_variable=true)
236+
237+
ph1
238+
```
239+
240+
### Calling
241+
242+
```@example data
243+
x0, p0, u0 = [1.0, 0.5], [0.3, 0.7], 2.0
244+
245+
ph1(x0, p0, u0) # natural call
246+
ph1(0.0, x0, p0, u0, nothing) # uniform call
247+
ph2(0.5, x0, p0, u0) # non-autonomous
248+
```
249+
250+
The dynamics trait of a `PseudoHamiltonian` is always
251+
[`Traits.HamiltonianDynamics`](@ref CTBase.Traits.HamiltonianDynamics), since
252+
pseudo-Hamiltonians involve both state and costate.
253+
254+
---
255+
256+
## ControlLaw
257+
258+
A `ControlLaw` wraps a function ``u(\cdots)`` that provides the control input for
259+
an optimal control problem. The feedback trait (see [Traits](traits.md))
260+
determines which arguments the control law depends on.
261+
262+
Three user-facing constructors fix the feedback trait:
263+
264+
| Constructor | Feedback trait | Natural signature (autonomous/fixed) |
265+
|---|---|---|
266+
| [`Data.OpenLoop`](@ref CTBase.Data.OpenLoop) | `OpenLoopFeedback` | `u()` |
267+
| [`Data.ClosedLoop`](@ref CTBase.Data.ClosedLoop) | `ClosedLoopFeedback` | `u(x)` |
268+
| [`Data.DynClosedLoop`](@ref CTBase.Data.DynClosedLoop) | `DynClosedLoopFeedback` | `u(x, p)` |
269+
270+
### Construction
271+
272+
```@example data
273+
# Open-loop, autonomous, fixed (default): u()
274+
u_ol = Data.OpenLoop(() -> 1.0)
275+
276+
# Closed-loop, autonomous, fixed: u(x)
277+
u_cl = Data.ClosedLoop(x -> -x)
278+
279+
# Dynamic closed-loop, autonomous, fixed: u(x, p)
280+
u_dcl = Data.DynClosedLoop((x, p) -> -x - p)
281+
282+
u_ol
283+
```
284+
285+
Non-autonomous and variable variants are constructed with the same keywords as
286+
other data types:
287+
288+
```@example data
289+
u_ol_na = Data.OpenLoop((t, v) -> t * v; is_autonomous=false, is_variable=true)
290+
```
291+
292+
### Calling
293+
294+
Every `ControlLaw` is callable via its **natural** signature (matching the
295+
traits) and via a **uniform** signature that depends on the feedback trait:
296+
297+
| Feedback | Natural `(Aut, Fixed)` | Uniform |
298+
|---|---|---|
299+
| `OpenLoop` | `u()` | `u(t, v)` |
300+
| `ClosedLoop` | `u(x)` | `u(t, x, v)` |
301+
| `DynClosedLoop` | `u(x, p)` | `u(t, x, p, v)` |
302+
303+
```@example data
304+
u_ol() # natural call
305+
u_ol(0.0, nothing) # uniform call
306+
307+
u_cl(x0) # natural call
308+
u_cl(0.0, x0, nothing) # uniform call
309+
310+
u_dcl(x0, p0) # natural call
311+
u_dcl(0.0, x0, p0, nothing) # uniform call
312+
```
313+
314+
The feedback trait determines the dynamics trait: open-loop and closed-loop
315+
control laws carry [`Traits.StateDynamics`](@ref CTBase.Traits.StateDynamics),
316+
while dynamic closed-loop control laws carry
317+
[`Traits.HamiltonianDynamics`](@ref CTBase.Traits.HamiltonianDynamics).
318+
319+
---
320+
207321
## Querying traits
208322

209323
Because the trait metadata lives in the type, it can be recovered from any data
@@ -251,15 +365,19 @@ Traits.time_dependence(vf_typed)
251365
```
252366

253367
The same positional form exists for [`Data.Hamiltonian`](@ref CTBase.Data.Hamiltonian)
254-
(`Hamiltonian(f, TD, VD)`) and
368+
(`Hamiltonian(f, TD, VD)`),
369+
[`Data.PseudoHamiltonian`](@ref CTBase.Data.PseudoHamiltonian)
370+
(`PseudoHamiltonian(f, TD, VD)`),
255371
[`Data.HamiltonianVectorField`](@ref CTBase.Data.HamiltonianVectorField)
256-
(`HamiltonianVectorField(f, TD, VD, MD)`).
372+
(`HamiltonianVectorField(f, TD, VD, MD)`), and
373+
[`Data.ControlLaw`](@ref CTBase.Data.ControlLaw)
374+
(`ControlLaw(f, FB, TD, VD)`).
257375

258376
---
259377

260378
## See also
261379

262-
- [`Data.VectorField`](@ref CTBase.Data.VectorField), [`Data.Hamiltonian`](@ref CTBase.Data.Hamiltonian), [`Data.HamiltonianVectorField`](@ref CTBase.Data.HamiltonianVectorField) — concrete data types.
263-
- [`Data.AbstractVectorField`](@ref CTBase.Data.AbstractVectorField), [`Data.AbstractHamiltonian`](@ref CTBase.Data.AbstractHamiltonian), [`Data.AbstractHamiltonianVectorField`](@ref CTBase.Data.AbstractHamiltonianVectorField) — abstract supertypes.
380+
- [`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.
381+
- [`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.
264382
- [Traits](traits.md) — the three trait axes, the dynamics trait, and call-signature tables.
265383
- [Exceptions](exceptions.md)`PreconditionError` and `IncorrectArgument`, raised by the constructors and the `variable_costate` path.

docs/src/guide/differentiation.md

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,15 @@ The concrete strategy stores a single option, `:ad_backend`, holding an
4242
`AutoForwardDiff()`). `ADTypes` is a hard dependency of CTBase, so the default is
4343
always available.
4444

45-
The contract has seven methods.
45+
The contract has nine methods.
4646
[`Differentiation.ad_backend`](@ref CTBase.Differentiation.ad_backend) — the accessor
4747
returning the wrapped ADTypes backend — is resolved in core and always available; the
48-
six differentiation primitives below live in an extension.
48+
eight differentiation primitives below live in an extension.
4949

5050
!!! note "The differentiation methods live in an extension"
5151
The contract methods (`gradient`, `derivative`, `differentiate`,
52-
`pushforward`, `hamiltonian_gradient`, `variable_gradient`) are implemented in
52+
`pushforward`, `hamiltonian_gradient`, `variable_gradient`,
53+
`pseudo_hamiltonian_gradient`, `pseudo_hamiltonian_control_gradient`) are implemented in
5354
the `CTBaseDifferentiationInterface` **package extension**. They become
5455
available only once `DifferentiationInterface` *and* a concrete AD package
5556
(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
164165
Differentiation.variable_gradient(backend, hv, 0.0, [1.0, 2.0], [3.0, 4.0], [5.0, 6.0])
165166
```
166167

168+
## Pseudo-Hamiltonian gradients
169+
170+
The two pseudo-Hamiltonian methods operate directly on a
171+
[`Data.PseudoHamiltonian`](@ref CTBase.Data.PseudoHamiltonian) (see the [Data](data.md)
172+
guide).
173+
[`Differentiation.pseudo_hamiltonian_gradient`](@ref CTBase.Differentiation.pseudo_hamiltonian_gradient)
174+
returns `(∂H̃/∂x, ∂H̃/∂p)`, **non-negated** — the caller applies the signs of
175+
Hamilton's equations:
176+
177+
```@example diff
178+
# H̃(x, p, u) = ½(‖x‖² + ‖p‖²) + u² → ∂H̃/∂x = x, ∂H̃/∂p = p
179+
ph = Data.PseudoHamiltonian((x, p, u) -> 0.5 * (sum(abs2, x) + sum(abs2, p)) + u^2)
180+
∂x, ∂p = Differentiation.pseudo_hamiltonian_gradient(backend, ph, 0.0, [1.0, 2.0], [3.0, 4.0], 2.0, nothing)
181+
(∂x, ∂p)
182+
```
183+
184+
[`Differentiation.pseudo_hamiltonian_control_gradient`](@ref CTBase.Differentiation.pseudo_hamiltonian_control_gradient)
185+
returns `∂H̃/∂u`, which vanishes along optimal trajectories by the Pontryagin
186+
Maximum Principle stationarity condition:
187+
188+
```@example diff
189+
# ∂H̃/∂u = 2u = 4.0
190+
Differentiation.pseudo_hamiltonian_control_gradient(backend, ph, 0.0, [1.0, 2.0], [3.0, 4.0], 2.0, nothing)
191+
```
192+
193+
For a non-fixed pseudo-Hamiltonian, the `v` argument carries the variable:
194+
195+
```@example diff
196+
phv = Data.PseudoHamiltonian((x, p, u, v) -> 0.5 * (sum(abs2, x) + sum(abs2, p)) + u^2 + v[1]; is_variable=true)
197+
Differentiation.pseudo_hamiltonian_control_gradient(backend, phv, 0.0, [1.0, 2.0], [3.0, 4.0], 2.0, [5.0])
198+
```
199+
167200
## The contract without the extension
168201

169202
Every contract method has a fallback on `AbstractADBackend` that throws
@@ -186,5 +219,5 @@ end # hide
186219

187220
- [`Differentiation.AbstractADBackend`](@ref CTBase.Differentiation.AbstractADBackend), [`Differentiation.DifferentiationInterface`](@ref CTBase.Differentiation.DifferentiationInterface) — the strategy types.
188221
- [Implementing a Strategy](@ref), [Options System](@ref) — the contract these build on.
189-
- [Data](data.md)`Hamiltonian` wrappers consumed by the gradient methods.
222+
- [Data](data.md)`Hamiltonian` and `PseudoHamiltonian` wrappers consumed by the gradient methods.
190223
- [Exceptions](exceptions.md)`NotImplemented`, raised by the contract fallbacks.

0 commit comments

Comments
 (0)