Traits: compile-time properties
CTBase.Traits provides a small set of compile-time traits shared across the control-toolbox ecosystem. A trait is an abstract type used as a type parameter or returned by an accessor function so that behaviour can be selected by dispatch, with no runtime cost.
julia> using CTBase
julia> import CTBase.TraitsA typical use case is encoding a property of a callable object — does it take a time argument? does it depend on an extra variable? is it evaluated in-place? — so that a wrapper type can select the correct call path at compile time, without runtime conditionals.
Trait families
Time dependence
Does the object depend on time
| Type | Meaning |
|---|---|
Traits.Autonomous | |
Traits.NonAutonomous |
Both are abstract subtypes of Traits.TimeDependence. Because they are abstract, they can only appear as type parameters — they are not instantiated, only dispatched upon.
julia> Traits.Autonomous <: Traits.TimeDependence
true
julia> Traits.NonAutonomous <: Traits.TimeDependence
trueVariable dependence
Does the object depend on an extra parameter
| Type | Meaning |
|---|---|
Traits.Fixed | no |
Traits.NonFixed |
Both are concrete subtypes of Traits.VariableDependence:
julia> Traits.Fixed <: Traits.VariableDependence
true
julia> Traits.NonFixed <: Traits.VariableDependence
trueControl dependence
Does the optimal control problem carry a control input
| Type | Meaning |
|---|---|
Traits.ControlFree | no control: |
Traits.WithControl | control |
Both are concrete subtypes of Traits.ControlDependence:
julia> Traits.ControlFree <: Traits.ControlDependence
true
julia> Traits.WithControl <: Traits.ControlDependence
trueMutability
Does a function allocate a new output, or write into a pre-allocated buffer?
| Type | Meaning |
|---|---|
Traits.OutOfPlace | returns a new value |
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 | time (and variable) only | u(t[, v]) |
Traits.ClosedLoopFeedback | time and state | u(t, x[, v]) |
Traits.DynClosedLoopFeedback | time, state, and costate | u(t, x, p[, v]) |
All three are concrete subtypes of Traits.AbstractFeedback:
julia> Traits.OpenLoopFeedback <: Traits.AbstractFeedback
true
julia> Traits.ClosedLoopFeedback <: Traits.AbstractFeedback
true
julia> Traits.DynClosedLoopFeedback <: Traits.AbstractFeedback
trueType-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 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, Traits.is_closed_loop, and Traits.is_dyn_closed_loop dispatch on the feedback type parameter:
julia> using CTBase.Data
julia> u = Data.OpenLoop(() -> 1.0)
ControlLaw: open-loop, autonomous, fixed (no variable)
natural call: u()
uniform call: u(t, v)
julia> Traits.feedback(u)
CTBase.Traits.OpenLoopFeedback
julia> Traits.is_open_loop(u)
trueThe feedback trait also determines the dynamics trait: open-loop and closed-loop control laws carry Traits.StateDynamics (no costate), while dynamic closed-loop control laws carry Traits.HamiltonianDynamics.
Constraint kind
Which primal variables does a path constraint g(...) depend on? The constraint-kind trait encodes this at the type level.
| Type | Meaning | Constraint signature |
|---|---|---|
Traits.StateConstraintKind | state only | g(x) |
Traits.ControlConstraintKind | control only | g(u) |
Traits.MixedConstraintKind | state and control | g(x, u) |
All three are concrete subtypes of Traits.AbstractConstraintKind:
julia> Traits.StateConstraintKind <: Traits.AbstractConstraintKind
true
julia> Traits.ControlConstraintKind <: Traits.AbstractConstraintKind
true
julia> Traits.MixedConstraintKind <: Traits.AbstractConstraintKind
trueType-parameter-only contract
Like feedback, constraint kind follows a type-parameter-only contract: the trait value is read from a type parameter of the concrete data type (e.g. PathConstraint{F,K,TD,VD}) by the Traits.constraint_kind accessor. No has_constraint_kind_trait guard is provided; calling constraint_kind on a type that does not implement it yields a standard MethodError.
The boolean predicates Traits.is_state_constraint, Traits.is_control_constraint, and Traits.is_mixed_constraint dispatch on the constraint-kind type parameter:
julia> using CTBase.Data
julia> g = Data.StateConstraint(x -> x[1])
PathConstraint: state, autonomous, fixed (no variable)
natural call: g(x)
uniform call: g(t, x, u, v)
julia> Traits.constraint_kind(g)
CTBase.Traits.StateConstraintKind
julia> Traits.is_state_constraint(g)
trueOther families
These traits encode properties that are fixed at construction time and carried as type parameters. They are not opted into via the two-method contract described below — they are passed directly as type arguments.
| Family | Values |
|---|---|
| Integration mode | Traits.EndPointMode, Traits.TrajectoryMode |
| Dynamics | Traits.StateDynamics, Traits.HamiltonianDynamics, Traits.AugmentedHamiltonianDynamics |
| Automatic differentiation | Traits.WithAD, Traits.WithoutAD |
| Variable costate | Traits.SupportsVariableCostate, Traits.NoVariableCostate |
| Feedback | Traits.OpenLoopFeedback, Traits.ClosedLoopFeedback, Traits.DynClosedLoopFeedback |
Abstract tags vs. concrete tags
Time-dependence tags (Autonomous, NonAutonomous) are abstract types because they are only ever used as type-parameter values (e.g. Model{Autonomous}) — never instantiated. All other tags (Fixed/NonFixed, ControlFree/WithControl, InPlace/OutOfPlace, …) are concrete empty singletons. In practice every tag is compared as a type (control_dependence(obj) === Traits.ControlFree), so the two forms are used interchangeably; the distinction is historical, not semantic.
The trait contract — two templates
Trait families follow one of two contracts, and the choice is dictated by a single question: does a safe default value exist?
Strict opt-in (no safe default). Time-, variable-, and control-dependence, and mutability. An object is either autonomous or not, either control-free or not — guessing silently would be a correctness bug, so there is no default. A type must opt in by implementing two methods: one declaring that it has the trait, one returning the trait value. The boolean predicates (
Traits.is_autonomous,Traits.is_control_free, …) then follow generically. If a type does not opt in, the predicates throw rather than guess.Default-valued capability (safe default exists). Automatic differentiation and variable costate are capabilities: a conservative "no" is always safe. The extractor returns a default for any object (
Traits.ad_traitWithoutAD,Traits.variable_costate_traitNoVariableCostate) and concrete types override it — nohas_*method, no predicates.
The "Other families" above (integration mode, dynamics) use neither contract: they are read directly from a concrete type's parameters.
For the strict opt-in families — time, variable and control dependence:
julia> struct MyObject end
julia> Traits.has_time_dependence_trait(::MyObject) = true
julia> Traits.time_dependence(::MyObject) = Traits.NonAutonomous
julia> Traits.has_variable_dependence_trait(::MyObject) = true
julia> Traits.variable_dependence(::MyObject) = Traits.Fixed
julia> Traits.has_control_dependence_trait(::MyObject) = true
julia> Traits.control_dependence(::MyObject) = Traits.WithControl
julia> obj = MyObject()
Main.MyObject()
julia> Traits.is_autonomous(obj)
false
julia> Traits.is_nonautonomous(obj)
true
julia> Traits.is_variable(obj)
false
julia> Traits.is_nonvariable(obj)
true
julia> Traits.is_control_free(obj)
false
julia> Traits.has_control(obj)
trueFor mutability, the same pattern applies with has_mutability_trait and mutability:
julia> struct MyMutableObject end
julia> Traits.has_mutability_trait(::MyMutableObject) = true
julia> Traits.mutability(::MyMutableObject) = Traits.InPlace
julia> Traits.is_inplace(MyMutableObject())
true
julia> Traits.is_outofplace(MyMutableObject())
falseIf a type does not declare a trait, the predicates throw an informative error rather than returning a wrong default:
julia> Traits.is_autonomous(3.14)
IncorrectArgument → top-level scope, REPL[1]:2
│
│ Cannot call is_autonomous on object of type Float64: no time-dependence trait
│
│ Context Time-dependence trait not available
│ Hint Implement has_time_dependence_trait(obj::Float64) = true and time_dependence(obj::Float64) to enable time-dependence trait support.
└─Trait types are shared
Because trait types (e.g. Traits.Autonomous) are defined in a single place, a type that carries Traits.Autonomous as a type parameter and an object that implements time_dependence returning Traits.Autonomous are compatible by construction — no conversion or mapping needed.
Accessor / predicate summary
| Trait value function | Boolean predicates |
|---|---|
Traits.time_dependence | is_autonomous, is_nonautonomous |
Traits.variable_dependence | is_variable, is_nonvariable, has_variable |
Traits.control_dependence | is_control_free, has_control |
Traits.mutability | is_inplace, is_outofplace |
Traits.ad_trait | — |
Traits.variable_costate_trait | — |
Traits.dynamics_trait | — |
Traits.feedback | is_open_loop, is_closed_loop, is_dyn_closed_loop |
Traits.constraint_kind | is_state_constraint, is_control_constraint, is_mixed_constraint |
See Also
- Exceptions guide — understanding
IncorrectArgumentandNotImplemented.