Skip to content

Commit 199a9ec

Browse files
authored
Merge pull request #354 from control-toolbox/feat/control-dependence-trait
feat(traits): implement ControlDependence on Model; migrate is_control_free/has_control to CTBase
2 parents 4ea2153 + cb90b40 commit 199a9ec

8 files changed

Lines changed: 106 additions & 44 deletions

File tree

BREAKING.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,24 @@
44

55
This document describes breaking changes in CTModels releases and how to migrate your code.
66

7+
## [0.14.0-beta] - 2026-06-28
8+
9+
### No Breaking Changes
10+
11+
This release adds the control-dependence trait on `Model` and migrates the
12+
`is_control_free` / `has_control` predicates to `CTBase.Traits`.
13+
14+
#### Additive / Compatibility
15+
16+
- **`is_control_free` / `has_control`**: now generic functions owned by `CTBase.Traits`,
17+
re-exported by CTModels. `CTModels.Models.is_control_free(ocp)` and
18+
`CTModels.Models.has_control(ocp)` keep working unchanged; behaviour is preserved
19+
(`is_control_free` is now additionally type-stable).
20+
- **New trait contract on `Model`**: `has_control_dependence_trait` / `control_dependence`
21+
return `ControlFree` / `WithControl`. Purely additive.
22+
- **CTBase compat**: bumped to `0.26`.
23+
- **No user action required**.
24+
725
## [0.13.2-beta] - 2026-06-25
826

927
### No Breaking Changes

CHANGELOG.md

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

10+
## [0.14.0-beta] - 2026-06-28
11+
12+
### ✨ New Features
13+
14+
- **Control-dependence trait on `Model`**: `Model` now implements the
15+
`CTBase.Traits.ControlDependence` contract (`has_control_dependence_trait` /
16+
`control_dependence`), reporting `ControlFree` for an `EmptyControlModel` and
17+
`WithControl` otherwise — read from the control model **type**, not the dimension.
18+
This enables trait-based dispatch on control presence in downstream packages (e.g.
19+
CTFlows routing `Flow(ocp)`).
20+
21+
### 🔄 Refactoring
22+
23+
- **`is_control_free` / `has_control` migrated to `CTBase.Traits`**: these predicates are
24+
now generic functions owned by `CTBase.Traits`; CTModels re-exports them (so
25+
`CTModels.Models.is_control_free` / `has_control` keep working unchanged). As a result
26+
`is_control_free` is now **type-stable** (derived from the control model type instead of
27+
`control_dimension(ocp) == 0`).
28+
29+
### 📦 Dependencies
30+
31+
- **CTBase compat bumped to `0.26`** (adds the `ControlDependence` trait family). Docs
32+
environment compat bumped accordingly.
33+
34+
### 📚 Documentation
35+
36+
- Added a *Control dependence* subsection to the model *Types and traits* guide.
37+
38+
### ✅ Compatibility
39+
40+
- **No breaking changes**: the predicate names and behaviour are preserved via re-export.
41+
See [BREAKING.md](BREAKING.md).
42+
1043
## [0.13.3-beta] - 2026-06-26
1144

1245
### 📦 Dependencies

Project.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "CTModels"
22
uuid = "34c4fa32-2049-4079-8329-de33c2a22e2d"
3-
version = "0.13.3-beta"
3+
version = "0.14.0-beta"
44
authors = ["Olivier Cots <olivier.cots@toulouse-inp.fr>"]
55

66
[deps]
@@ -25,7 +25,7 @@ CTModelsPlots = "Plots"
2525

2626
[compat]
2727
Aqua = "0.8"
28-
CTBase = "0.25"
28+
CTBase = "0.26"
2929
DocStringExtensions = "0.9"
3030
JLD2 = "0.6"
3131
JSON3 = "1"

docs/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391"
77
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
88

99
[compat]
10-
CTBase = "0.25"
10+
CTBase = "0.26"
1111
Documenter = "1"
1212
JLD2 = "0.6"
1313
JSON3 = "1"

docs/src/model/types_and_traits.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ evm = CTModels.EmptyVariableModel()
4141
(CTModels.dimension(sm), CTModels.name(sm), evm isa CTModels.Components.AbstractVariableModel)
4242
```
4343

44-
## The two trait axes
44+
## The trait axes
4545

46-
Two orthogonal yes/no axes are **not** modelled as separate types but as traits.
46+
Orthogonal yes/no axes are **not** modelled as separate types but as traits.
4747

4848
### Time dependence
4949

@@ -68,6 +68,25 @@ ocp = CTModels.build(pre)
6868
CTModels.is_autonomous(ocp)
6969
```
7070

71+
### Control dependence
72+
73+
Whether the problem carries a control input is the **type** of the
74+
[`AbstractControlModel`](@ref CTModels.Components.AbstractControlModel) inside the
75+
[`Model`](@ref CTModels.Models.Model): an
76+
[`EmptyControlModel`](@ref CTModels.Components.EmptyControlModel) means *control-free*, any
77+
other control model means *with control*. This is exposed through the
78+
`CTBase.Traits.ControlDependence` axis (values `ControlFree` / `WithControl`), shared
79+
ecosystem-wide, with the extractors [`is_control_free`](@ref CTModels.Models.is_control_free)
80+
and [`has_control`](@ref CTModels.Models.has_control):
81+
82+
```@example types
83+
(CTModels.is_control_free(ocp), CTModels.has_control(ocp))
84+
```
85+
86+
Like time dependence, the predicates are generic functions owned by `CTBase.Traits`; the
87+
`Model` only declares the trait and reports its value (read from the control model type, not
88+
from the control dimension).
89+
7190
### Time structure
7291

7392
Whether each end of the interval is fixed or free is the **type** of the corresponding

src/Models/Models.jl

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,17 @@ module Models
3333
import CTBase.Core
3434
import CTBase.Exceptions
3535
import CTBase.Traits
36-
# Time/variable-dependence predicates are generic functions owned by CTBase.Traits;
37-
# CTModels only provides the `Model` trait contract (see model.jl) and re-exports them.
36+
# Time/variable/control-dependence predicates are generic functions owned by
37+
# CTBase.Traits; CTModels only provides the `Model` trait contract (see model.jl)
38+
# and re-exports them.
3839
import CTBase.Traits:
39-
is_autonomous, is_nonautonomous, is_variable, is_nonvariable, has_variable
40+
is_autonomous,
41+
is_nonautonomous,
42+
is_variable,
43+
is_nonvariable,
44+
has_variable,
45+
is_control_free,
46+
has_control
4047
import DocStringExtensions: TYPEDEF, TYPEDSIGNATURES
4148

4249
using ..Components

src/Models/model.jl

Lines changed: 13 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -96,16 +96,20 @@ end
9696
# ------------------------------------------------------------------------------ #
9797

9898
# ------------------------------------------------------------------------------ #
99-
# Trait contract — time & variable dependence
99+
# Trait contract — time, variable & control dependence
100100
#
101101
# CTModels no longer defines the boolean predicates `is_autonomous`,
102-
# `is_nonautonomous`, `is_variable`, `is_nonvariable`, `has_variable`: these are
103-
# generic functions owned by `CTBase.Traits`. A `Model` only declares that it has
104-
# the traits and reports the trait values; the predicates then follow generically.
102+
# `is_nonautonomous`, `is_variable`, `is_nonvariable`, `has_variable`,
103+
# `is_control_free`, `has_control`: these are generic functions owned by
104+
# `CTBase.Traits`. A `Model` only declares that it has the traits and reports the
105+
# trait values; the predicates then follow generically.
105106
#
106107
# - time dependence is read from the `TD` type parameter,
107108
# - variable dependence is read from the *type* of the variable model
108-
# (`EmptyVariableModel` ⟹ Fixed, any other ⟹ NonFixed) — not from the dimension.
109+
# (`EmptyVariableModel` ⟹ Fixed, any other ⟹ NonFixed) — not from the dimension,
110+
# - control dependence is read from the *type* of the control model
111+
# (`EmptyControlModel` ⟹ ControlFree, any other ⟹ WithControl) — not from the
112+
# dimension.
109113
# ------------------------------------------------------------------------------ #
110114

111115
Traits.has_time_dependence_trait(::Model) = true
@@ -118,37 +122,11 @@ Traits.variable_dependence(ocp::Model) = _variable_dependence(ocp.variable)
118122
_variable_dependence(::EmptyVariableModel) = Traits.Fixed
119123
_variable_dependence(::AbstractVariableModel) = Traits.NonFixed
120124

121-
"""
122-
$(TYPEDSIGNATURES)
123-
124-
Check whether the problem is control-free (no control input).
125-
126-
# Arguments
127-
- `ocp::Model`: The optimal control problem.
128-
129-
# Returns
130-
- `Bool`: `true` if the problem has no control input, `false` otherwise.
131-
132-
See also: [`CTModels.Models.has_control`](@ref), [`CTModels.Models.control_dimension`](@ref).
133-
"""
134-
function is_control_free(ocp::Model)::Bool
135-
return control_dimension(ocp) == 0
136-
end
125+
Traits.has_control_dependence_trait(::Model) = true
137126

138-
"""
139-
$(TYPEDSIGNATURES)
140-
141-
Check whether the problem has control input.
142-
143-
# Arguments
144-
- `ocp::Model`: The optimal control problem.
145-
146-
# Returns
147-
- `Bool`: `true` if the problem has control input, `false` otherwise.
148-
149-
See also: [`CTModels.Models.is_control_free`](@ref).
150-
"""
151-
has_control(ocp::Model)::Bool = !is_control_free(ocp)
127+
Traits.control_dependence(ocp::Model) = _control_dependence(ocp.control)
128+
_control_dependence(::EmptyControlModel) = Traits.ControlFree
129+
_control_dependence(::AbstractControlModel) = Traits.WithControl
152130

153131
"""
154132
$(TYPEDSIGNATURES)

test/suite/models/test_variable_control_checks.jl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module TestVariableControlChecks
22

33
import Test: Test
4+
import CTBase.Traits: Traits
45
import CTModels.Building: Building
56
import CTModels.Models: Models
67

@@ -78,6 +79,9 @@ function test_variable_control_checks()
7879

7980
model = Building.build(ocp)
8081
Test.@test Models.is_control_free(model) === false
82+
Test.@test Models.has_control(model) === true
83+
Test.@test Traits.control_dependence(model) === Traits.WithControl
84+
Test.@test Traits.has_control_dependence_trait(model) === true
8185
end
8286

8387
Test.@testset "Model without control" begin
@@ -98,6 +102,9 @@ function test_variable_control_checks()
98102

99103
model = Building.build(ocp)
100104
Test.@test Models.is_control_free(model) === true
105+
Test.@test Models.has_control(model) === false
106+
Test.@test Traits.control_dependence(model) === Traits.ControlFree
107+
Test.@test Traits.has_control_dependence_trait(model) === true
101108
end
102109
end
103110

@@ -153,7 +160,7 @@ function test_variable_control_checks()
153160

154161
Test.@testset "Exports Verification" begin
155162
Test.@testset "Exported Functions" begin
156-
for f in (:is_variable, :is_control_free)
163+
for f in (:is_variable, :is_control_free, :has_control)
157164
Test.@test isdefined(Models, f)
158165
end
159166
end

0 commit comments

Comments
 (0)