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
18 changes: 18 additions & 0 deletions BREAKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@

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

## [0.14.0-beta] - 2026-06-28

### No Breaking Changes

This release adds the control-dependence trait on `Model` and migrates the
`is_control_free` / `has_control` predicates to `CTBase.Traits`.

#### Additive / Compatibility

- **`is_control_free` / `has_control`**: now generic functions owned by `CTBase.Traits`,
re-exported by CTModels. `CTModels.Models.is_control_free(ocp)` and
`CTModels.Models.has_control(ocp)` keep working unchanged; behaviour is preserved
(`is_control_free` is now additionally type-stable).
- **New trait contract on `Model`**: `has_control_dependence_trait` / `control_dependence`
return `ControlFree` / `WithControl`. Purely additive.
- **CTBase compat**: bumped to `0.26`.
- **No user action required**.

## [0.13.2-beta] - 2026-06-25

### No Breaking Changes
Expand Down
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,39 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.14.0-beta] - 2026-06-28

### ✨ New Features

- **Control-dependence trait on `Model`**: `Model` now implements the
`CTBase.Traits.ControlDependence` contract (`has_control_dependence_trait` /
`control_dependence`), reporting `ControlFree` for an `EmptyControlModel` and
`WithControl` otherwise — read from the control model **type**, not the dimension.
This enables trait-based dispatch on control presence in downstream packages (e.g.
CTFlows routing `Flow(ocp)`).

### 🔄 Refactoring

- **`is_control_free` / `has_control` migrated to `CTBase.Traits`**: these predicates are
now generic functions owned by `CTBase.Traits`; CTModels re-exports them (so
`CTModels.Models.is_control_free` / `has_control` keep working unchanged). As a result
`is_control_free` is now **type-stable** (derived from the control model type instead of
`control_dimension(ocp) == 0`).

### 📦 Dependencies

- **CTBase compat bumped to `0.26`** (adds the `ControlDependence` trait family). Docs
environment compat bumped accordingly.

### 📚 Documentation

- Added a *Control dependence* subsection to the model *Types and traits* guide.

### ✅ Compatibility

- **No breaking changes**: the predicate names and behaviour are preserved via re-export.
See [BREAKING.md](BREAKING.md).

## [0.13.3-beta] - 2026-06-26

### 📦 Dependencies
Expand Down
4 changes: 2 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "CTModels"
uuid = "34c4fa32-2049-4079-8329-de33c2a22e2d"
version = "0.13.3-beta"
version = "0.14.0-beta"
authors = ["Olivier Cots <olivier.cots@toulouse-inp.fr>"]

[deps]
Expand All @@ -25,7 +25,7 @@ CTModelsPlots = "Plots"

[compat]
Aqua = "0.8"
CTBase = "0.25"
CTBase = "0.26"
DocStringExtensions = "0.9"
JLD2 = "0.6"
JSON3 = "1"
Expand Down
2 changes: 1 addition & 1 deletion docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"

[compat]
CTBase = "0.25"
CTBase = "0.26"
Documenter = "1"
JLD2 = "0.6"
JSON3 = "1"
Expand Down
23 changes: 21 additions & 2 deletions docs/src/model/types_and_traits.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ evm = CTModels.EmptyVariableModel()
(CTModels.dimension(sm), CTModels.name(sm), evm isa CTModels.Components.AbstractVariableModel)
```

## The two trait axes
## The trait axes

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

### Time dependence

Expand All @@ -68,6 +68,25 @@ ocp = CTModels.build(pre)
CTModels.is_autonomous(ocp)
```

### Control dependence

Whether the problem carries a control input is the **type** of the
[`AbstractControlModel`](@ref CTModels.Components.AbstractControlModel) inside the
[`Model`](@ref CTModels.Models.Model): an
[`EmptyControlModel`](@ref CTModels.Components.EmptyControlModel) means *control-free*, any
other control model means *with control*. This is exposed through the
`CTBase.Traits.ControlDependence` axis (values `ControlFree` / `WithControl`), shared
ecosystem-wide, with the extractors [`is_control_free`](@ref CTModels.Models.is_control_free)
and [`has_control`](@ref CTModels.Models.has_control):

```@example types
(CTModels.is_control_free(ocp), CTModels.has_control(ocp))
```

Like time dependence, the predicates are generic functions owned by `CTBase.Traits`; the
`Model` only declares the trait and reports its value (read from the control model type, not
from the control dimension).

### Time structure

Whether each end of the interval is fixed or free is the **type** of the corresponding
Expand Down
13 changes: 10 additions & 3 deletions src/Models/Models.jl
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,17 @@ module Models
import CTBase.Core
import CTBase.Exceptions
import CTBase.Traits
# Time/variable-dependence predicates are generic functions owned by CTBase.Traits;
# CTModels only provides the `Model` trait contract (see model.jl) and re-exports them.
# Time/variable/control-dependence predicates are generic functions owned by
# CTBase.Traits; CTModels only provides the `Model` trait contract (see model.jl)
# and re-exports them.
import CTBase.Traits:
is_autonomous, is_nonautonomous, is_variable, is_nonvariable, has_variable
is_autonomous,
is_nonautonomous,
is_variable,
is_nonvariable,
has_variable,
is_control_free,
has_control
import DocStringExtensions: TYPEDEF, TYPEDSIGNATURES

using ..Components
Expand Down
48 changes: 13 additions & 35 deletions src/Models/model.jl
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,20 @@ end
# ------------------------------------------------------------------------------ #

# ------------------------------------------------------------------------------ #
# Trait contract — time & variable dependence
# Trait contract — time, variable & control dependence
#
# CTModels no longer defines the boolean predicates `is_autonomous`,
# `is_nonautonomous`, `is_variable`, `is_nonvariable`, `has_variable`: these are
# generic functions owned by `CTBase.Traits`. A `Model` only declares that it has
# the traits and reports the trait values; the predicates then follow generically.
# `is_nonautonomous`, `is_variable`, `is_nonvariable`, `has_variable`,
# `is_control_free`, `has_control`: these are generic functions owned by
# `CTBase.Traits`. A `Model` only declares that it has the traits and reports the
# trait values; the predicates then follow generically.
#
# - time dependence is read from the `TD` type parameter,
# - variable dependence is read from the *type* of the variable model
# (`EmptyVariableModel` ⟹ Fixed, any other ⟹ NonFixed) — not from the dimension.
# (`EmptyVariableModel` ⟹ Fixed, any other ⟹ NonFixed) — not from the dimension,
# - control dependence is read from the *type* of the control model
# (`EmptyControlModel` ⟹ ControlFree, any other ⟹ WithControl) — not from the
# dimension.
# ------------------------------------------------------------------------------ #

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

"""
$(TYPEDSIGNATURES)

Check whether the problem is control-free (no control input).

# Arguments
- `ocp::Model`: The optimal control problem.

# Returns
- `Bool`: `true` if the problem has no control input, `false` otherwise.

See also: [`CTModels.Models.has_control`](@ref), [`CTModels.Models.control_dimension`](@ref).
"""
function is_control_free(ocp::Model)::Bool
return control_dimension(ocp) == 0
end
Traits.has_control_dependence_trait(::Model) = true

"""
$(TYPEDSIGNATURES)

Check whether the problem has control input.

# Arguments
- `ocp::Model`: The optimal control problem.

# Returns
- `Bool`: `true` if the problem has control input, `false` otherwise.

See also: [`CTModels.Models.is_control_free`](@ref).
"""
has_control(ocp::Model)::Bool = !is_control_free(ocp)
Traits.control_dependence(ocp::Model) = _control_dependence(ocp.control)
_control_dependence(::EmptyControlModel) = Traits.ControlFree
_control_dependence(::AbstractControlModel) = Traits.WithControl

"""
$(TYPEDSIGNATURES)
Expand Down
9 changes: 8 additions & 1 deletion test/suite/models/test_variable_control_checks.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module TestVariableControlChecks

import Test: Test
import CTBase.Traits: Traits
import CTModels.Building: Building
import CTModels.Models: Models

Expand Down Expand Up @@ -78,6 +79,9 @@ function test_variable_control_checks()

model = Building.build(ocp)
Test.@test Models.is_control_free(model) === false
Test.@test Models.has_control(model) === true
Test.@test Traits.control_dependence(model) === Traits.WithControl
Test.@test Traits.has_control_dependence_trait(model) === true
end

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

model = Building.build(ocp)
Test.@test Models.is_control_free(model) === true
Test.@test Models.has_control(model) === false
Test.@test Traits.control_dependence(model) === Traits.ControlFree
Test.@test Traits.has_control_dependence_trait(model) === true
end
end

Expand Down Expand Up @@ -153,7 +160,7 @@ function test_variable_control_checks()

Test.@testset "Exports Verification" begin
Test.@testset "Exported Functions" begin
for f in (:is_variable, :is_control_free)
for f in (:is_variable, :is_control_free, :has_control)
Test.@test isdefined(Models, f)
end
end
Expand Down
Loading