Skip to content

Commit 9df51b3

Browse files
authored
Merge pull request #471 from control-toolbox/feat/control-dependence-trait
feat(traits): add ControlDependence family + refactor strict-contract helpers
2 parents 4098ca1 + abc1438 commit 9df51b3

12 files changed

Lines changed: 512 additions & 75 deletions

BREAKINGS.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ 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.25.0-beta)
30+
31+
- **Traits**: New `ControlDependence` family added for encoding control presence in optimal control problems
32+
- **New trait family**: `ControlDependence` with tags `ControlFree` and `WithControl`
33+
- **Opt-in contract**: types implement `has_control_dependence_trait` and `control_dependence`
34+
- **Derived predicates**: `is_control_free(obj)` and `has_control(obj)`
35+
- **Internal refactoring**: shared helpers for strict-contract error handling reduce duplication
36+
- **No breaking changes**: Purely additive. Existing code unaffected. New trait enables dispatch in downstream packages (CTModels, CTFlows).
37+
2938
## Non-breaking note (0.24.0-beta)
3039

3140
- **Differentiation module**: Added comprehensive AD backend infrastructure for computing gradients

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
## Unreleased (0.25.0-beta)
6+
7+
### Added
8+
9+
- **Traits**: New `ControlDependence` trait family for encoding control presence in optimal control problems
10+
- Tags: `ControlFree` (no control) and `WithControl` (with control input)
11+
- Opt-in contract: implement `has_control_dependence_trait` and `control_dependence`
12+
- Derived predicates: `is_control_free` and `has_control`
13+
- Enables downstream dispatch in CTFlows and CTModels for control-problem routing
14+
15+
### Changed
16+
17+
- **Traits**: Refactored strict-contract machinery to eliminate duplication
18+
- New internal helpers: `_throw_missing_trait` and `_throw_trait_not_implemented`
19+
- Generalised `_caller_function_name` stacktrace filter for any `has_<family>_trait` predicate
20+
- All strictfamilies (time-dependence, variable-dependence, mutability, control-dependence) now share the same error handling
21+
- Collapsed `has_variable` duplicate to direct alias of `is_variable`
22+
23+
- **Documentation**: Updated Traits guide to document the two trait templates (strict opt-in vs default-valued capability)
24+
25+
### Fixed
26+
27+
- **Traits**: API reference now includes `control_dependence.jl` in auto-generated documentation
28+
29+
---
30+
31+
## Previous versions
32+
33+
See git history and release notes for earlier versions.

docs/api_reference.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ function generate_api_reference(src_dir::String)
174174
joinpath("Traits", "mutability.jl"),
175175
joinpath("Traits", "time_dependence.jl"),
176176
joinpath("Traits", "variable_dependence.jl"),
177+
joinpath("Traits", "control_dependence.jl"),
177178
),
178179
),
179180
(

docs/src/guide/traits.md

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,22 @@ Traits.Fixed <: Traits.VariableDependence
5656
Traits.NonFixed <: Traits.VariableDependence
5757
```
5858

59+
### Control dependence
60+
61+
Does the optimal control problem carry a control input ``u``?
62+
63+
| Type | Meaning |
64+
|---|---|
65+
| [`Traits.ControlFree`](@ref CTBase.Traits.ControlFree) | no control: ``ẋ = f(t, x, v)`` |
66+
| [`Traits.WithControl`](@ref CTBase.Traits.WithControl) | control ``u``: ``ẋ = f(t, x, u, v)`` |
67+
68+
Both are concrete subtypes of [`Traits.ControlDependence`](@ref CTBase.Traits.ControlDependence):
69+
70+
```@repl traits
71+
Traits.ControlFree <: Traits.ControlDependence
72+
Traits.WithControl <: Traits.ControlDependence
73+
```
74+
5975
### Mutability
6076

6177
Does a function allocate a new output, or write into a pre-allocated buffer?
@@ -78,15 +94,39 @@ below — they are passed directly as type arguments.
7894
| Automatic differentiation | [`Traits.WithAD`](@ref CTBase.Traits.WithAD), [`Traits.WithoutAD`](@ref CTBase.Traits.WithoutAD) |
7995
| Variable costate | [`Traits.SupportsVariableCostate`](@ref CTBase.Traits.SupportsVariableCostate), [`Traits.NoVariableCostate`](@ref CTBase.Traits.NoVariableCostate) |
8096

81-
## The trait contract
82-
83-
A type opts in to a trait by implementing **two methods**: one declaring that it
84-
*has* the trait, and one returning the trait *value*. The boolean predicates
85-
([`Traits.is_autonomous`](@ref CTBase.Traits.is_autonomous),
86-
[`Traits.is_variable`](@ref CTBase.Traits.is_variable), …) then follow
87-
generically — they are not implemented per type.
88-
89-
For time and variable dependence:
97+
!!! note "Abstract tags vs. concrete tags"
98+
Time-dependence tags (`Autonomous`, `NonAutonomous`) are **abstract** types
99+
because they are only ever used as type-parameter *values* (e.g. `Model{Autonomous}`)
100+
— never instantiated. All other tags (`Fixed`/`NonFixed`, `ControlFree`/`WithControl`,
101+
`InPlace`/`OutOfPlace`, …) are **concrete** empty singletons. In practice every tag
102+
is compared as a type (`control_dependence(obj) === Traits.ControlFree`), so the
103+
two forms are used interchangeably; the distinction is historical, not semantic.
104+
105+
## The trait contract — two templates
106+
107+
Trait families follow one of **two contracts**, and the choice is dictated by a
108+
single question: **does a safe default value exist?**
109+
110+
- **Strict opt-in (no safe default).** Time-, variable-, and control-dependence,
111+
and mutability. An object is *either* autonomous or not, *either* control-free or
112+
not — guessing silently would be a correctness bug, so there is no default. A type
113+
must opt in by implementing **two methods**: one declaring that it *has* the trait,
114+
one returning the trait *value*. The boolean predicates
115+
([`Traits.is_autonomous`](@ref CTBase.Traits.is_autonomous),
116+
[`Traits.is_control_free`](@ref CTBase.Traits.is_control_free), …) then follow
117+
generically. If a type does not opt in, the predicates throw rather than guess.
118+
- **Default-valued capability (safe default exists).** Automatic differentiation
119+
and variable costate are *capabilities*: a conservative "no" is always safe. The
120+
extractor returns a default for any object
121+
([`Traits.ad_trait`](@ref CTBase.Traits.ad_trait) ```` `WithoutAD`,
122+
[`Traits.variable_costate_trait`](@ref CTBase.Traits.variable_costate_trait) ````
123+
`NoVariableCostate`) and concrete types override it — no `has_*` method, no
124+
predicates.
125+
126+
The "Other families" above (integration mode, dynamics) use neither contract: they
127+
are read directly from a concrete type's parameters.
128+
129+
For the strict opt-in families — time, variable and control dependence:
90130

91131
```@repl traits
92132
struct MyObject end
@@ -97,11 +137,16 @@ Traits.time_dependence(::MyObject) = Traits.NonAutonomous
97137
Traits.has_variable_dependence_trait(::MyObject) = true
98138
Traits.variable_dependence(::MyObject) = Traits.Fixed
99139
140+
Traits.has_control_dependence_trait(::MyObject) = true
141+
Traits.control_dependence(::MyObject) = Traits.WithControl
142+
100143
obj = MyObject()
101144
Traits.is_autonomous(obj)
102145
Traits.is_nonautonomous(obj)
103146
Traits.is_variable(obj)
104147
Traits.is_nonvariable(obj)
148+
Traits.is_control_free(obj)
149+
Traits.has_control(obj)
105150
```
106151

107152
For mutability, the same pattern applies with `has_mutability_trait` and `mutability`:
@@ -139,6 +184,7 @@ end # hide
139184
|---|---|
140185
| [`Traits.time_dependence`](@ref CTBase.Traits.time_dependence) | `is_autonomous`, `is_nonautonomous` |
141186
| [`Traits.variable_dependence`](@ref CTBase.Traits.variable_dependence) | `is_variable`, `is_nonvariable`, `has_variable` |
187+
| [`Traits.control_dependence`](@ref CTBase.Traits.control_dependence) | `is_control_free`, `has_control` |
142188
| [`Traits.mutability`](@ref CTBase.Traits.mutability) | `is_inplace`, `is_outofplace` |
143189
| [`Traits.ad_trait`](@ref CTBase.Traits.ad_trait) ||
144190
| [`Traits.variable_costate_trait`](@ref CTBase.Traits.variable_costate_trait) ||

src/Traits/Traits.jl

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ no runtime cost.
1212
- **abstract.jl**: Root abstract type `AbstractTrait` and family abstractions
1313
- **time_dependence.jl**: Time-dependence traits and the opt-in contract
1414
- **variable_dependence.jl**: Variable-dependence traits and the opt-in contract
15+
- **control_dependence.jl**: Control-dependence traits and the opt-in contract
1516
- **mutability.jl**: Mutability traits and the opt-in contract
1617
- **mode.jl**: Integration-mode traits (`EndPointMode`, `TrajectoryMode`)
1718
- **dynamics.jl**: Dynamics-type traits (`StateDynamics`, `HamiltonianDynamics`, `AugmentedHamiltonianDynamics`)
@@ -25,23 +26,39 @@ no runtime cost.
2526
2627
- **Time dependence**: `TimeDependence`, `Autonomous`, `NonAutonomous`
2728
- **Variable dependence**: `VariableDependence`, `Fixed`, `NonFixed`
29+
- **Control dependence**: `ControlDependence`, `ControlFree`, `WithControl`
2830
- **Mutability**: `InPlace`, `OutOfPlace`
2931
- **Integration mode**: `EndPointMode`, `TrajectoryMode`
3032
- **Dynamics**: `StateDynamics`, `HamiltonianDynamics`, `AugmentedHamiltonianDynamics`
3133
- **Automatic differentiation**: `WithAD`, `WithoutAD`
3234
- **Variable costate**: `SupportsVariableCostate`, `NoVariableCostate`
3335
34-
## Trait contract
35-
36-
For time-dependence, variable-dependence, and mutability, a type opts in by
37-
implementing two methods — `has_<family>_trait` returning `true`, and an accessor
38-
(`time_dependence`, `variable_dependence`, `mutability`) returning the trait type.
39-
Boolean predicates (`is_autonomous`, `is_variable`, `is_inplace`, …) are then
40-
derived generically.
41-
42-
The remaining families (`EndPointMode`, `StateDynamics`, `WithAD`,
43-
`SupportsVariableCostate`) are used as type parameters only and do not use the
44-
two-method contract.
36+
## Trait contracts — two templates
37+
38+
Trait families follow one of two contracts. The choice is dictated by a single
39+
question: **does a safe default value exist?**
40+
41+
**1. Strict opt-in (no safe default).** Time-dependence, variable-dependence,
42+
control-dependence, and mutability. Guessing a value silently would be a
43+
correctness bug (an object is *either* autonomous or not), so there is no default:
44+
a type must opt in by implementing two methods — `has_<family>_trait` returning
45+
`true`, and an accessor (`time_dependence`, `variable_dependence`,
46+
`control_dependence`, `mutability`) returning the trait type. The fallbacks throw
47+
loudly ([`CTBase.Exceptions.IncorrectArgument`](@ref) /
48+
[`CTBase.Exceptions.NotImplemented`](@ref)) via the shared helpers
49+
`_throw_missing_trait` / `_throw_trait_not_implemented`. Boolean predicates
50+
(`is_autonomous`, `is_variable`, `is_control_free`, `is_inplace`, …) are derived
51+
generically.
52+
53+
**2. Default-valued capability (safe default exists).** Automatic differentiation
54+
(`ad_trait(::Any) = WithoutAD`) and variable-costate
55+
(`variable_costate_trait(::Any) = NoVariableCostate`). These are *capabilities*: a
56+
conservative "no" is always safe, so the extractor returns a default for any object
57+
and concrete types override it. No `has_<family>_trait` guard, no derived predicates.
58+
59+
The remaining families (`EndPointMode`, `StateDynamics`) are used as type
60+
parameters only (read from a concrete type's parameters, e.g.
61+
`AbstractSystem{TD,VD,D}`) and use neither contract.
4562
"""
4663
module Traits
4764

@@ -65,6 +82,7 @@ include(joinpath(@__DIR__, "variable_costate.jl"))
6582
include(joinpath(@__DIR__, "mutability.jl"))
6683
include(joinpath(@__DIR__, "time_dependence.jl"))
6784
include(joinpath(@__DIR__, "variable_dependence.jl"))
85+
include(joinpath(@__DIR__, "control_dependence.jl"))
6886

6987
# ==============================================================================
7088
# Module exports
@@ -82,10 +100,13 @@ export InPlace, OutOfPlace
82100
export WithAD, WithoutAD
83101
export SupportsVariableCostate, NoVariableCostate
84102
export VariableDependence, Fixed, NonFixed
103+
export ControlDependence, ControlFree, WithControl
85104
export ad_trait, variable_costate_trait, dynamics_trait
86105
export is_inplace, is_outofplace
87106
export is_autonomous, is_nonautonomous, is_variable, is_nonvariable, has_variable
107+
export is_control_free, has_control
88108
export has_time_dependence_trait, time_dependence, has_mutability_trait, mutability
89109
export has_variable_dependence_trait, variable_dependence
110+
export has_control_dependence_trait, control_dependence
90111

91112
end # module Traits

src/Traits/control_dependence.jl

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
"""
2+
$(TYPEDEF)
3+
4+
Abstract supertype for control-dependence traits.
5+
6+
Encodes, at the type level, whether an optimal control problem (or any object
7+
that opts into the trait) carries a control input.
8+
9+
# Trait Pattern
10+
11+
Objects that have a control-dependence trait must implement two methods:
12+
- `has_control_dependence_trait(obj::MyType) = true`: Indicates the type has this trait
13+
- `control_dependence(obj::MyType)`: Returns the specific trait value (`ControlFree` or `WithControl`)
14+
15+
Once these are implemented, the object automatically gains:
16+
- `is_control_free(obj)`: Returns true if `control_dependence(obj)` is `ControlFree`
17+
- `has_control(obj)`: Returns true if `control_dependence(obj)` is `WithControl`
18+
19+
If `has_control_dependence_trait` is not implemented or returns `false`,
20+
calling `is_control_free`, `has_control`, or `control_dependence` will throw an error
21+
indicating the object does not support control-dependence queries.
22+
23+
See also: [`CTBase.Traits.ControlFree`](@ref), [`CTBase.Traits.WithControl`](@ref).
24+
"""
25+
abstract type ControlDependence <: AbstractTrait end
26+
27+
"""
28+
$(TYPEDEF)
29+
30+
Trait indicating the problem has no control input (control-free).
31+
32+
A control-free optimal control problem has dynamics `ẋ = f(t, x, v)` with no
33+
control argument; the trajectory is determined by the state (and costate)
34+
equations alone, without a control law.
35+
36+
See also: [`CTBase.Traits.WithControl`](@ref), [`CTBase.Traits.ControlDependence`](@ref).
37+
"""
38+
struct ControlFree <: ControlDependence end
39+
40+
"""
41+
$(TYPEDEF)
42+
43+
Trait indicating the problem has a control input.
44+
45+
A problem with control has dynamics `ẋ = f(t, x, u, v)` depending on a control
46+
`u`; closing the loop (e.g. for a flow) requires a control law `u(t, x, p)`.
47+
48+
See also: [`CTBase.Traits.ControlFree`](@ref), [`CTBase.Traits.ControlDependence`](@ref).
49+
"""
50+
struct WithControl <: ControlDependence end
51+
52+
# =============================================================================
53+
# Check has trait
54+
# =============================================================================
55+
56+
"""
57+
$(TYPEDSIGNATURES)
58+
59+
Check if the object has the control-dependence trait.
60+
61+
This fallback method throws an error indicating the object does not support
62+
control-dependence queries. Concrete types that have the trait should implement
63+
`has_control_dependence_trait(obj::MyType) = true`.
64+
65+
The calling function name is automatically detected from the stacktrace
66+
for better error messages.
67+
68+
# Arguments
69+
- `obj::Any`: The object to check.
70+
71+
# Throws
72+
- [`CTBase.Exceptions.IncorrectArgument`](@ref): Always, indicating the object does not have the trait.
73+
74+
See also: [`CTBase.Traits.ControlDependence`](@ref), [`CTBase.Traits.control_dependence`](@ref).
75+
"""
76+
function has_control_dependence_trait(obj::Any)
77+
return _throw_missing_trait(
78+
obj, :has_control_dependence_trait, :control_dependence, "control-dependence"
79+
)
80+
end
81+
82+
"""
83+
$(TYPEDSIGNATURES)
84+
85+
Return the control-dependence trait value for the object.
86+
87+
This fallback method throws an error indicating the method is not implemented.
88+
Concrete types that have the trait should implement `control_dependence(obj::MyType)`
89+
to return the specific trait value (`ControlFree` or `WithControl`).
90+
91+
# Arguments
92+
- `obj::Any`: The object to query.
93+
94+
# Throws
95+
- [`CTBase.Exceptions.NotImplemented`](@ref): Always, indicating the method must be implemented.
96+
97+
See also: [`CTBase.Traits.ControlDependence`](@ref), [`CTBase.Traits.has_control_dependence_trait`](@ref).
98+
"""
99+
function control_dependence(obj::Any)
100+
has_control_dependence_trait(obj)
101+
return _throw_trait_not_implemented(
102+
obj, :control_dependence, "control-dependence", "ControlFree or WithControl"
103+
)
104+
end
105+
106+
# =============================================================================
107+
# Trait accessors
108+
# =============================================================================
109+
110+
"""
111+
$(TYPEDSIGNATURES)
112+
113+
Return true if the object is control-free (has no control input).
114+
115+
Checks that the object has the control-dependence trait, then returns true
116+
if `control_dependence(obj)` is `ControlFree`.
117+
118+
# Arguments
119+
- `obj::Any`: The object to check.
120+
121+
# Returns
122+
- `Bool`: true if the object has no control input.
123+
124+
# Throws
125+
- [`CTBase.Exceptions.IncorrectArgument`](@ref): If the object does not support control-dependence queries.
126+
- [`CTBase.Exceptions.NotImplemented`](@ref): If `control_dependence` is not implemented for the object type.
127+
128+
See also: [`CTBase.Traits.ControlDependence`](@ref), [`CTBase.Traits.has_control`](@ref).
129+
"""
130+
function is_control_free(obj::Any)
131+
has_control_dependence_trait(obj)
132+
return control_dependence(obj) === ControlFree
133+
end
134+
135+
"""
136+
$(TYPEDSIGNATURES)
137+
138+
Return true if the object has a control input.
139+
140+
Checks that the object has the control-dependence trait, then returns true
141+
if `control_dependence(obj)` is `WithControl`.
142+
143+
# Arguments
144+
- `obj::Any`: The object to check.
145+
146+
# Returns
147+
- `Bool`: true if the object has a control input.
148+
149+
# Throws
150+
- [`CTBase.Exceptions.IncorrectArgument`](@ref): If the object does not support control-dependence queries.
151+
- [`CTBase.Exceptions.NotImplemented`](@ref): If `control_dependence` is not implemented for the object type.
152+
153+
See also: [`CTBase.Traits.ControlDependence`](@ref), [`CTBase.Traits.is_control_free`](@ref).
154+
"""
155+
function has_control(obj::Any)
156+
has_control_dependence_trait(obj)
157+
return control_dependence(obj) === WithControl
158+
end

0 commit comments

Comments
 (0)