From cb90b408173b5fed346c920d6987bb60bbbc9c7b Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Sun, 28 Jun 2026 15:22:00 +0200 Subject: [PATCH] Implement ControlDependence trait on Model; migrate is_control_free/has_control to CTBase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Model now implements the CTBase.Traits.ControlDependence contract (has_control_dependence_trait + control_dependence), reading ControlFree from an EmptyControlModel and WithControl otherwise — from the control model TYPE, mirroring the existing variable-dependence design (not from control_dimension) - is_control_free / has_control are now generic functions owned by CTBase.Traits; CTModels re-exports them so CTModels.Models.is_control_free / has_control are unchanged. is_control_free is now type-stable (no runtime control_dimension == 0) - Bump CTBase compat 0.25 -> 0.26 (and docs env compat); bump version 0.13.3-beta -> 0.14.0-beta - Extend control-checks tests with trait-path assertions (control_dependence, has_control) - Add a Control dependence subsection to the model Types-and-traits guide - Update CHANGELOG.md and BREAKING.md (purely additive, no breaking changes) Tests: models/components/display suites 313/313 pass; docs build succeeds (exit 0). Co-Authored-By: Claude Opus 4.8 --- BREAKING.md | 18 +++++++ CHANGELOG.md | 33 +++++++++++++ Project.toml | 4 +- docs/Project.toml | 2 +- docs/src/model/types_and_traits.md | 23 ++++++++- src/Models/Models.jl | 13 +++-- src/Models/model.jl | 48 +++++-------------- .../models/test_variable_control_checks.jl | 9 +++- 8 files changed, 106 insertions(+), 44 deletions(-) diff --git a/BREAKING.md b/BREAKING.md index be461435..24e83b8e 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3afd8972..22f3ff77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Project.toml b/Project.toml index 5b3d89bb..e4736c38 100644 --- a/Project.toml +++ b/Project.toml @@ -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 "] [deps] @@ -25,7 +25,7 @@ CTModelsPlots = "Plots" [compat] Aqua = "0.8" -CTBase = "0.25" +CTBase = "0.26" DocStringExtensions = "0.9" JLD2 = "0.6" JSON3 = "1" diff --git a/docs/Project.toml b/docs/Project.toml index 0c925462..ef28ea6a 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -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" diff --git a/docs/src/model/types_and_traits.md b/docs/src/model/types_and_traits.md index e86f3a7c..89c740b6 100644 --- a/docs/src/model/types_and_traits.md +++ b/docs/src/model/types_and_traits.md @@ -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 @@ -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 diff --git a/src/Models/Models.jl b/src/Models/Models.jl index c00fbcc9..e1b3a52a 100644 --- a/src/Models/Models.jl +++ b/src/Models/Models.jl @@ -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 diff --git a/src/Models/model.jl b/src/Models/model.jl index f7171b76..2c9b22bb 100644 --- a/src/Models/model.jl +++ b/src/Models/model.jl @@ -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 @@ -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) diff --git a/test/suite/models/test_variable_control_checks.jl b/test/suite/models/test_variable_control_checks.jl index 0d3b4905..67c9f788 100644 --- a/test/suite/models/test_variable_control_checks.jl +++ b/test/suite/models/test_variable_control_checks.jl @@ -1,6 +1,7 @@ module TestVariableControlChecks import Test: Test +import CTBase.Traits: Traits import CTModels.Building: Building import CTModels.Models: Models @@ -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 @@ -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 @@ -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