Skip to content

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
julia> using CTBase

julia> import CTBase.Traits

A 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 ?

TypeMeaning
Traits.Autonomous is not an argument
Traits.NonAutonomous must be supplied

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
julia> Traits.Autonomous <: Traits.TimeDependence
true

julia> Traits.NonAutonomous <: Traits.TimeDependence
true

Variable dependence

Does the object depend on an extra parameter (e.g. a free final time or a design variable)?

TypeMeaning
Traits.Fixedno argument
Traits.NonFixed must be supplied

Both are concrete subtypes of Traits.VariableDependence:

julia
julia> Traits.Fixed <: Traits.VariableDependence
true

julia> Traits.NonFixed <: Traits.VariableDependence
true

Control dependence

Does the optimal control problem carry a control input ?

TypeMeaning
Traits.ControlFreeno control:  
Traits.WithControlcontrol :  

Both are concrete subtypes of Traits.ControlDependence:

julia
julia> Traits.ControlFree <: Traits.ControlDependence
true

julia> Traits.WithControl <: Traits.ControlDependence
true

Mutability

Does a function allocate a new output, or write into a pre-allocated buffer?

TypeMeaning
Traits.OutOfPlacereturns a new value
Traits.InPlacewrites 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.

TypeMeaningControl law signature
Traits.OpenLoopFeedbacktime (and variable) onlyu(t[, v])
Traits.ClosedLoopFeedbacktime and stateu(t, x[, v])
Traits.DynClosedLoopFeedbacktime, state, and costateu(t, x, p[, v])

All three are concrete subtypes of Traits.AbstractFeedback:

julia
julia> Traits.OpenLoopFeedback <: Traits.AbstractFeedback
true

julia> Traits.ClosedLoopFeedback <: Traits.AbstractFeedback
true

julia> Traits.DynClosedLoopFeedback <: Traits.AbstractFeedback
true

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 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
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)
true

The 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.

TypeMeaningConstraint signature
Traits.StateConstraintKindstate onlyg(x)
Traits.ControlConstraintKindcontrol onlyg(u)
Traits.MixedConstraintKindstate and controlg(x, u)

All three are concrete subtypes of Traits.AbstractConstraintKind:

julia
julia> Traits.StateConstraintKind <: Traits.AbstractConstraintKind
true

julia> Traits.ControlConstraintKind <: Traits.AbstractConstraintKind
true

julia> Traits.MixedConstraintKind <: Traits.AbstractConstraintKind
true

Type-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
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)
true

Other 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.

FamilyValues
Integration modeTraits.EndPointMode, Traits.TrajectoryMode
DynamicsTraits.StateDynamics, Traits.HamiltonianDynamics, Traits.AugmentedHamiltonianDynamics
Automatic differentiationTraits.WithAD, Traits.WithoutAD
Variable costateTraits.SupportsVariableCostate, Traits.NoVariableCostate
FeedbackTraits.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_trait WithoutAD, Traits.variable_costate_trait NoVariableCostate) and concrete types override it — no has_* 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
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)
true

For mutability, the same pattern applies with has_mutability_trait and mutability:

julia
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())
false

If a type does not declare a trait, the predicates throw an informative error rather than returning a wrong default:

julia
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 functionBoolean predicates
Traits.time_dependenceis_autonomous, is_nonautonomous
Traits.variable_dependenceis_variable, is_nonvariable, has_variable
Traits.control_dependenceis_control_free, has_control
Traits.mutabilityis_inplace, is_outofplace
Traits.ad_trait
Traits.variable_costate_trait
Traits.dynamics_trait
Traits.feedbackis_open_loop, is_closed_loop, is_dyn_closed_loop
Traits.constraint_kindis_state_constraint, is_control_constraint, is_mixed_constraint

See Also