Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions BREAKINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ 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.25.0-beta)

- **Traits**: New `ControlDependence` family added for encoding control presence in optimal control problems
- **New trait family**: `ControlDependence` with tags `ControlFree` and `WithControl`
- **Opt-in contract**: types implement `has_control_dependence_trait` and `control_dependence`
- **Derived predicates**: `is_control_free(obj)` and `has_control(obj)`
- **Internal refactoring**: shared helpers for strict-contract error handling reduce duplication
- **No breaking changes**: Purely additive. Existing code unaffected. New trait enables dispatch in downstream packages (CTModels, CTFlows).

## Non-breaking note (0.24.0-beta)

- **Differentiation module**: Added comprehensive AD backend infrastructure for computing gradients
Expand Down
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Changelog

All notable changes to this project will be documented in this file.

## Unreleased (0.25.0-beta)

### Added

- **Traits**: New `ControlDependence` trait family for encoding control presence in optimal control problems
- Tags: `ControlFree` (no control) and `WithControl` (with control input)
- Opt-in contract: implement `has_control_dependence_trait` and `control_dependence`
- Derived predicates: `is_control_free` and `has_control`
- Enables downstream dispatch in CTFlows and CTModels for control-problem routing

### Changed

- **Traits**: Refactored strict-contract machinery to eliminate duplication
- New internal helpers: `_throw_missing_trait` and `_throw_trait_not_implemented`
- Generalised `_caller_function_name` stacktrace filter for any `has_<family>_trait` predicate
- All strictfamilies (time-dependence, variable-dependence, mutability, control-dependence) now share the same error handling
- Collapsed `has_variable` duplicate to direct alias of `is_variable`

- **Documentation**: Updated Traits guide to document the two trait templates (strict opt-in vs default-valued capability)

### Fixed

- **Traits**: API reference now includes `control_dependence.jl` in auto-generated documentation

---

## Previous versions

See git history and release notes for earlier versions.
1 change: 1 addition & 0 deletions docs/api_reference.jl
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ function generate_api_reference(src_dir::String)
joinpath("Traits", "mutability.jl"),
joinpath("Traits", "time_dependence.jl"),
joinpath("Traits", "variable_dependence.jl"),
joinpath("Traits", "control_dependence.jl"),
),
),
(
Expand Down
64 changes: 55 additions & 9 deletions docs/src/guide/traits.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,22 @@ Traits.Fixed <: Traits.VariableDependence
Traits.NonFixed <: Traits.VariableDependence
```

### Control dependence

Does the optimal control problem carry a control input ``u``?

| Type | Meaning |
|---|---|
| [`Traits.ControlFree`](@ref CTBase.Traits.ControlFree) | no control: ``ẋ = f(t, x, v)`` |
| [`Traits.WithControl`](@ref CTBase.Traits.WithControl) | control ``u``: ``ẋ = f(t, x, u, v)`` |

Both are concrete subtypes of [`Traits.ControlDependence`](@ref CTBase.Traits.ControlDependence):

```@repl traits
Traits.ControlFree <: Traits.ControlDependence
Traits.WithControl <: Traits.ControlDependence
```

### Mutability

Does a function allocate a new output, or write into a pre-allocated buffer?
Expand All @@ -78,15 +94,39 @@ below — they are passed directly as type arguments.
| 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) |

## The trait contract

A type opts in to a trait by implementing **two methods**: one declaring that it
*has* the trait, and one returning the trait *value*. The boolean predicates
([`Traits.is_autonomous`](@ref CTBase.Traits.is_autonomous),
[`Traits.is_variable`](@ref CTBase.Traits.is_variable), …) then follow
generically — they are not implemented per type.

For time and variable dependence:
!!! note "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`](@ref CTBase.Traits.is_autonomous),
[`Traits.is_control_free`](@ref CTBase.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`](@ref CTBase.Traits.ad_trait) ``→`` `WithoutAD`,
[`Traits.variable_costate_trait`](@ref CTBase.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:

```@repl traits
struct MyObject end
Expand All @@ -97,11 +137,16 @@ Traits.time_dependence(::MyObject) = Traits.NonAutonomous
Traits.has_variable_dependence_trait(::MyObject) = true
Traits.variable_dependence(::MyObject) = Traits.Fixed

Traits.has_control_dependence_trait(::MyObject) = true
Traits.control_dependence(::MyObject) = Traits.WithControl

obj = MyObject()
Traits.is_autonomous(obj)
Traits.is_nonautonomous(obj)
Traits.is_variable(obj)
Traits.is_nonvariable(obj)
Traits.is_control_free(obj)
Traits.has_control(obj)
```

For mutability, the same pattern applies with `has_mutability_trait` and `mutability`:
Expand Down Expand Up @@ -139,6 +184,7 @@ end # hide
|---|---|
| [`Traits.time_dependence`](@ref CTBase.Traits.time_dependence) | `is_autonomous`, `is_nonautonomous` |
| [`Traits.variable_dependence`](@ref CTBase.Traits.variable_dependence) | `is_variable`, `is_nonvariable`, `has_variable` |
| [`Traits.control_dependence`](@ref CTBase.Traits.control_dependence) | `is_control_free`, `has_control` |
| [`Traits.mutability`](@ref CTBase.Traits.mutability) | `is_inplace`, `is_outofplace` |
| [`Traits.ad_trait`](@ref CTBase.Traits.ad_trait) | — |
| [`Traits.variable_costate_trait`](@ref CTBase.Traits.variable_costate_trait) | — |
Expand Down
43 changes: 32 additions & 11 deletions src/Traits/Traits.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ no runtime cost.
- **abstract.jl**: Root abstract type `AbstractTrait` and family abstractions
- **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
- **mutability.jl**: Mutability traits and the opt-in contract
- **mode.jl**: Integration-mode traits (`EndPointMode`, `TrajectoryMode`)
- **dynamics.jl**: Dynamics-type traits (`StateDynamics`, `HamiltonianDynamics`, `AugmentedHamiltonianDynamics`)
Expand All @@ -25,23 +26,39 @@ no runtime cost.

- **Time dependence**: `TimeDependence`, `Autonomous`, `NonAutonomous`
- **Variable dependence**: `VariableDependence`, `Fixed`, `NonFixed`
- **Control dependence**: `ControlDependence`, `ControlFree`, `WithControl`
- **Mutability**: `InPlace`, `OutOfPlace`
- **Integration mode**: `EndPointMode`, `TrajectoryMode`
- **Dynamics**: `StateDynamics`, `HamiltonianDynamics`, `AugmentedHamiltonianDynamics`
- **Automatic differentiation**: `WithAD`, `WithoutAD`
- **Variable costate**: `SupportsVariableCostate`, `NoVariableCostate`

## Trait contract

For time-dependence, variable-dependence, and mutability, a type opts in by
implementing two methods — `has_<family>_trait` returning `true`, and an accessor
(`time_dependence`, `variable_dependence`, `mutability`) returning the trait type.
Boolean predicates (`is_autonomous`, `is_variable`, `is_inplace`, …) are then
derived generically.

The remaining families (`EndPointMode`, `StateDynamics`, `WithAD`,
`SupportsVariableCostate`) are used as type parameters only and do not use the
two-method contract.
## Trait contracts — two templates

Trait families follow one of two contracts. The choice is dictated by a single
question: **does a safe default value exist?**

**1. Strict opt-in (no safe default).** Time-dependence, variable-dependence,
control-dependence, and mutability. Guessing a value silently would be a
correctness bug (an object is *either* autonomous or not), so there is no default:
a type must opt in by implementing two methods — `has_<family>_trait` returning
`true`, and an accessor (`time_dependence`, `variable_dependence`,
`control_dependence`, `mutability`) returning the trait type. The fallbacks throw
loudly ([`CTBase.Exceptions.IncorrectArgument`](@ref) /
[`CTBase.Exceptions.NotImplemented`](@ref)) via the shared helpers
`_throw_missing_trait` / `_throw_trait_not_implemented`. Boolean predicates
(`is_autonomous`, `is_variable`, `is_control_free`, `is_inplace`, …) are derived
generically.

**2. Default-valued capability (safe default exists).** Automatic differentiation
(`ad_trait(::Any) = WithoutAD`) and variable-costate
(`variable_costate_trait(::Any) = NoVariableCostate`). These are *capabilities*: a
conservative "no" is always safe, so the extractor returns a default for any object
and concrete types override it. No `has_<family>_trait` guard, no derived predicates.

The remaining families (`EndPointMode`, `StateDynamics`) are used as type
parameters only (read from a concrete type's parameters, e.g.
`AbstractSystem{TD,VD,D}`) and use neither contract.
"""
module Traits

Expand All @@ -65,6 +82,7 @@ include(joinpath(@__DIR__, "variable_costate.jl"))
include(joinpath(@__DIR__, "mutability.jl"))
include(joinpath(@__DIR__, "time_dependence.jl"))
include(joinpath(@__DIR__, "variable_dependence.jl"))
include(joinpath(@__DIR__, "control_dependence.jl"))

# ==============================================================================
# Module exports
Expand All @@ -82,10 +100,13 @@ export InPlace, OutOfPlace
export WithAD, WithoutAD
export SupportsVariableCostate, NoVariableCostate
export VariableDependence, Fixed, NonFixed
export ControlDependence, ControlFree, WithControl
export ad_trait, variable_costate_trait, dynamics_trait
export is_inplace, is_outofplace
export is_autonomous, is_nonautonomous, is_variable, is_nonvariable, has_variable
export is_control_free, has_control
export has_time_dependence_trait, time_dependence, has_mutability_trait, mutability
export has_variable_dependence_trait, variable_dependence
export has_control_dependence_trait, control_dependence

end # module Traits
158 changes: 158 additions & 0 deletions src/Traits/control_dependence.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"""
$(TYPEDEF)

Abstract supertype for control-dependence traits.

Encodes, at the type level, whether an optimal control problem (or any object
that opts into the trait) carries a control input.

# Trait Pattern

Objects that have a control-dependence trait must implement two methods:
- `has_control_dependence_trait(obj::MyType) = true`: Indicates the type has this trait
- `control_dependence(obj::MyType)`: Returns the specific trait value (`ControlFree` or `WithControl`)

Once these are implemented, the object automatically gains:
- `is_control_free(obj)`: Returns true if `control_dependence(obj)` is `ControlFree`
- `has_control(obj)`: Returns true if `control_dependence(obj)` is `WithControl`

If `has_control_dependence_trait` is not implemented or returns `false`,
calling `is_control_free`, `has_control`, or `control_dependence` will throw an error
indicating the object does not support control-dependence queries.

See also: [`CTBase.Traits.ControlFree`](@ref), [`CTBase.Traits.WithControl`](@ref).
"""
abstract type ControlDependence <: AbstractTrait end

"""
$(TYPEDEF)

Trait indicating the problem has no control input (control-free).

A control-free optimal control problem has dynamics `ẋ = f(t, x, v)` with no
control argument; the trajectory is determined by the state (and costate)
equations alone, without a control law.

See also: [`CTBase.Traits.WithControl`](@ref), [`CTBase.Traits.ControlDependence`](@ref).
"""
struct ControlFree <: ControlDependence end

"""
$(TYPEDEF)

Trait indicating the problem has a control input.

A problem with control has dynamics `ẋ = f(t, x, u, v)` depending on a control
`u`; closing the loop (e.g. for a flow) requires a control law `u(t, x, p)`.

See also: [`CTBase.Traits.ControlFree`](@ref), [`CTBase.Traits.ControlDependence`](@ref).
"""
struct WithControl <: ControlDependence end

# =============================================================================
# Check has trait
# =============================================================================

"""
$(TYPEDSIGNATURES)

Check if the object has the control-dependence trait.

This fallback method throws an error indicating the object does not support
control-dependence queries. Concrete types that have the trait should implement
`has_control_dependence_trait(obj::MyType) = true`.

The calling function name is automatically detected from the stacktrace
for better error messages.

# Arguments
- `obj::Any`: The object to check.

# Throws
- [`CTBase.Exceptions.IncorrectArgument`](@ref): Always, indicating the object does not have the trait.

See also: [`CTBase.Traits.ControlDependence`](@ref), [`CTBase.Traits.control_dependence`](@ref).
"""
function has_control_dependence_trait(obj::Any)
return _throw_missing_trait(
obj, :has_control_dependence_trait, :control_dependence, "control-dependence"
)
end

"""
$(TYPEDSIGNATURES)

Return the control-dependence trait value for the object.

This fallback method throws an error indicating the method is not implemented.
Concrete types that have the trait should implement `control_dependence(obj::MyType)`
to return the specific trait value (`ControlFree` or `WithControl`).

# Arguments
- `obj::Any`: The object to query.

# Throws
- [`CTBase.Exceptions.NotImplemented`](@ref): Always, indicating the method must be implemented.

See also: [`CTBase.Traits.ControlDependence`](@ref), [`CTBase.Traits.has_control_dependence_trait`](@ref).
"""
function control_dependence(obj::Any)
has_control_dependence_trait(obj)
return _throw_trait_not_implemented(
obj, :control_dependence, "control-dependence", "ControlFree or WithControl"
)
end

# =============================================================================
# Trait accessors
# =============================================================================

"""
$(TYPEDSIGNATURES)

Return true if the object is control-free (has no control input).

Checks that the object has the control-dependence trait, then returns true
if `control_dependence(obj)` is `ControlFree`.

# Arguments
- `obj::Any`: The object to check.

# Returns
- `Bool`: true if the object has no control input.

# Throws
- [`CTBase.Exceptions.IncorrectArgument`](@ref): If the object does not support control-dependence queries.
- [`CTBase.Exceptions.NotImplemented`](@ref): If `control_dependence` is not implemented for the object type.

See also: [`CTBase.Traits.ControlDependence`](@ref), [`CTBase.Traits.has_control`](@ref).
"""
function is_control_free(obj::Any)
has_control_dependence_trait(obj)
return control_dependence(obj) === ControlFree
end

"""
$(TYPEDSIGNATURES)

Return true if the object has a control input.

Checks that the object has the control-dependence trait, then returns true
if `control_dependence(obj)` is `WithControl`.

# Arguments
- `obj::Any`: The object to check.

# Returns
- `Bool`: true if the object has a control input.

# Throws
- [`CTBase.Exceptions.IncorrectArgument`](@ref): If the object does not support control-dependence queries.
- [`CTBase.Exceptions.NotImplemented`](@ref): If `control_dependence` is not implemented for the object type.

See also: [`CTBase.Traits.ControlDependence`](@ref), [`CTBase.Traits.is_control_free`](@ref).
"""
function has_control(obj::Any)
has_control_dependence_trait(obj)
return control_dependence(obj) === WithControl
end
Loading