Skip to content

Commit 9555669

Browse files
authored
Merge pull request #304 from control-toolbox/empty-vs-set
Add user-facing Model predicates and restrict predicates to Model only
2 parents e2233e2 + fa7553e commit 9555669

23 files changed

Lines changed: 300 additions & 302 deletions

BREAKING.md

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

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

7-
## [0.9.15] - 2026-04-18
7+
## [0.9.15-beta] - 2026-04-18
88

99
### Breaking Changes: Dual Dimension Function Renaming
1010

@@ -32,39 +32,44 @@ The old function names were misleading because they returned the dimension of du
3232

3333
The functions `dim_*_constraints_box(ocp::Model)` (for Model, not Solution) remain unchanged and still refer to constraint dimension in the model.
3434

35-
### Non-Breaking Changes
35+
### Breaking Changes: PreModel Predicate Removal
3636

37-
This release also introduces consistent variable and control checking functions without breaking existing functionality:
37+
The following predicate methods have been removed for `PreModel` and are now exclusive to `Model`:
3838

39-
#### New Functions (Non-Breaking)
39+
- `is_autonomous(ocp::PreModel)` - removed
40+
- `is_variable(ocp::PreModel)` - removed
41+
- `is_control_free(ocp::PreModel)` - removed
4042

41-
- **New functions**: Added `is_variable()` and `is_control_free()` for checking problem properties
42-
- **Dual methods**: Both functions have methods for `PreModel` and `Model` types
43-
- **Consistent API**: These functions follow the same pattern as `is_autonomous()`
44-
- **Runtime dimension checks**: Unlike time dependence (type-parameterized), variable and control use runtime dimension checks
43+
These methods remain available for `Model` instances.
4544

46-
#### What Changed
45+
#### Migration Guide
4746

4847
```julia
49-
# New functions available (non-breaking)
50-
is_variable(ocp) # Returns true if variable_dimension > 0
51-
is_control_free(ocp) # Returns true if control_dimension == 0
52-
53-
# Works for both PreModel and Model
54-
ocp = PreModel()
55-
state!(ocp, 2)
56-
control!(ocp, 1)
57-
variable!(ocp, 2)
58-
59-
is_variable(ocp) # Returns true
60-
is_control_free(ocp) # Returns false
48+
# Before (PreModel access)
49+
pre = PreModel()
50+
state!(pre, 2)
51+
control!(pre, 1)
52+
variable!(pre, 2)
53+
time_dependence!(pre; autonomous=true)
54+
55+
# These no longer work:
56+
is_autonomous(pre) # MethodError
57+
is_variable(pre) # MethodError
58+
is_control_free(pre) # MethodError
59+
60+
# After (use direct field access or internal predicates)
61+
pre.autonomous # true/false
62+
!CTModels.OCP.__is_variable_empty(pre) # true/false
63+
CTModels.OCP.__is_control_empty(pre) # true/false
6164
```
6265

63-
#### Migration
66+
#### Rationale
6467

65-
- **No action required**: Existing code continues to work unchanged
66-
- **Optional enhancement**: Can use new functions for more readable code instead of inline dimension comparisons
67-
- **Same API**: No changes to existing user-facing API; behavior is fully backward compatible
68+
Predicate methods are now exclusive to immutable `Model` types to enforce a clear separation between mutable construction (`PreModel`) and immutable problem definition (`Model`). Internal predicates (`__is_*_empty`) are used for construction-time checks.
69+
70+
#### Note
71+
72+
The predicate methods `is_autonomous(model)`, `is_variable(model)`, and `is_control_free(model)` for `Model` remain unchanged and continue to work as before.
6873

6974
## [0.9.14] - 2026-04-12
7075

CHANGELOG.md

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ 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.9.15] - 2026-04-18
10+
## [0.9.15-beta] - 2026-04-18
1111

1212
### 🚀 Enhancements
1313

@@ -60,37 +60,43 @@ expr = expression(pre.definition) # Returns the Expr
6060
model = build(pre) # Works even without definition
6161
```
6262

63-
#### Consistent Variable and Control Checking Functions
63+
#### User-Facing Model Predicates
6464

65-
- **New functions**: Added `is_variable()` and `is_control_free()` for checking problem properties
66-
- **Dual methods**: Both functions have methods for `PreModel` and `Model` types
67-
- **Consistent API**: These functions follow the same pattern as `is_autonomous()`
68-
- **Runtime dimension checks**: Unlike time dependence (type-parameterized), variable and control use runtime dimension checks
69-
- **Display integration**: Updated display code to use the new functions instead of inline comparisons
65+
- **New predicates**: Added user-friendly predicate methods for `Model` instances
66+
- **Exclusive to Model**: Predicates are only available for immutable `Model`, not `PreModel`
67+
- **Consistent naming**: Follows pattern `has_*` for presence checks, `is_*` for property checks
68+
- **New exports**: `has_variable`, `has_control`, `has_abstract_definition`, `is_abstractly_defined`, `is_nonautonomous`, `is_nonvariable`
7069

7170
#### API Enhancements
7271

7372
```julia
7473
# Check if problem has optimisation variables
75-
is_variable(ocp::PreModel) # Returns true if variable_dimension > 0
76-
is_variable(ocp::Model) # Returns true if variable_dimension > 0
74+
has_variable(model) # Alias for is_variable(model)
75+
is_nonvariable(model) # Opposite of is_variable(model)
7776

78-
# Check if problem is control-free (no control input)
79-
is_control_free(ocp::PreModel) # Returns true if control_dimension == 0
80-
is_control_free(ocp::Model) # Returns true if control_dimension == 0
77+
# Check if problem has control input
78+
has_control(model) # Opposite of is_control_free(model)
79+
80+
# Check if problem has abstract definition
81+
has_abstract_definition(model) # Checks if definition is non-empty
82+
is_abstractly_defined(model) # Alias for has_abstract_definition
83+
84+
# Check time dependence
85+
is_nonautonomous(model) # Opposite of is_autonomous(model)
8186
```
8287

8388
### 📊 API Changes
8489

85-
- **New exports**: `is_variable` and `is_control_free` are now exported from CTModels
86-
- **Display code**: Internal display functions now use the new checking functions instead of inline dimension comparisons
90+
- **Breaking**: `is_variable(ocp::PreModel)`, `is_control_free(ocp::PreModel)`, `is_autonomous(ocp::PreModel)` removed
91+
- **New exports**: `has_variable`, `has_control`, `has_abstract_definition`, `is_abstractly_defined`, `is_nonautonomous`, `is_nonvariable`
92+
- **Display code**: Internal display functions use `__is_*_empty` predicates for PreModel, public predicates for Model
8793

8894
### 🔧 Internal Changes
8995

90-
- **New methods**: Added `is_variable()` and `is_control_free()` methods in `time_dependence.jl` and `model.jl`
91-
- **Display refactoring**: Replaced inline `v_dim > 0` with `is_variable(ocp)` in `print.jl`
92-
- **Display refactoring**: Replaced inline `u_dim > 0` with `!is_control_free(ocp)` in `print.jl`
93-
- **New tests**: Added comprehensive test suite in `test_variable_control_checks.jl` with 20 tests
96+
- **Predicate refactoring**: Removed `__is_*_set` methods for `Model` (only `__is_*_empty` remains)
97+
- **PreModel access**: Display code uses direct field access (`ocp.autonomous`) and internal predicates (`__is_variable_empty`, `__is_control_empty`)
98+
- **Model access**: Public predicates (`is_variable`, `is_control_free`, `is_autonomous`) work for Model only
99+
- **Test updates**: Migrated tests to use internal predicates for PreModel, public predicates for Model
94100

95101
## [0.9.14] - 2026-04-12
96102

Project.toml

Lines changed: 1 addition & 1 deletion
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.9.15"
3+
version = "0.10.0"
44
authors = ["Olivier Cots <olivier.cots@toulouse-inp.fr>"]
55

66
[deps]

src/CTModels.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,4 @@ using .Serialization
9191
include(joinpath(@__DIR__, "Init", "Init.jl"))
9292
using .Init
9393

94-
end
94+
end

src/Display/Display.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ import ..OCP: Model, PreModel, Solution, AbstractSolution
3939
import ..OCP: AbstractDefinition, Definition, EmptyDefinition
4040

4141
# Import internal helpers from OCP for display
42-
import ..OCP: __is_empty, __is_definition_set, definition, __is_consistent
42+
import ..OCP: __is_empty, definition, __is_consistent
43+
import ..OCP: __is_variable_empty, __is_control_empty
4344
import ..OCP: state_dimension, control_dimension, variable_dimension
4445
import ..OCP: time_name, initial_time_name, final_time_name
4546
import ..OCP: dimension, name, state_name, control_name, variable_name

src/Display/pre_model.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ function Base.show(io::IO, ::MIME"text/plain", ocp::PreModel)
3838
vi_names = components(ocp.variable)
3939

4040
# dependencies
41-
is_variable_dependent = is_variable(ocp)
42-
is_time_dependent = !is_autonomous(ocp)
43-
is_control_free_ocp = is_control_free(ocp)
41+
is_variable_dependent = v_dim > 0
42+
is_time_dependent = !ocp.autonomous
43+
is_control_free_ocp = u_dim == 0
4444

4545
# cost
4646
has_a_lagrange_cost = has_lagrange_cost(ocp.objective)

src/OCP/Building/model.jl

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ function build(pre_ocp::PreModel; build_examodel=nothing)::Model
493493
objective = pre_ocp.objective
494494
constraints = build(pre_ocp.constraints)
495495
definition = pre_ocp.definition
496-
TD = is_autonomous(pre_ocp) ? Autonomous : NonAutonomous
496+
TD = pre_ocp.autonomous ? Autonomous : NonAutonomous
497497

498498
# create the model
499499
model = Model{TD}(
@@ -619,6 +619,114 @@ function is_control_free(ocp::Model)::Bool
619619
return control_dimension(ocp) == 0
620620
end
621621

622+
"""
623+
$(TYPEDSIGNATURES)
624+
625+
Check whether the problem has optimisation variables.
626+
627+
# Arguments
628+
- `ocp::Model`: The optimal control problem model.
629+
630+
# Returns
631+
- `Bool`: `true` if the problem has optimisation variables (variable dimension > 0), `false` otherwise.
632+
633+
# Example
634+
```julia-repl
635+
julia> has_variable(model) # returns true if variables are present
636+
```
637+
"""
638+
has_variable(ocp::Model)::Bool = is_variable(ocp)
639+
640+
"""
641+
$(TYPEDSIGNATURES)
642+
643+
Check whether the problem has control input.
644+
645+
# Arguments
646+
- `ocp::Model`: The optimal control problem model.
647+
648+
# Returns
649+
- `Bool`: `true` if the problem has control input (control dimension > 0), `false` otherwise.
650+
651+
# Example
652+
```julia-repl
653+
julia> has_control(model) # returns true if control is present
654+
```
655+
"""
656+
has_control(ocp::Model)::Bool = !is_control_free(ocp)
657+
658+
"""
659+
$(TYPEDSIGNATURES)
660+
661+
Check whether the problem has an abstract definition.
662+
663+
# Arguments
664+
- `ocp::Model`: The optimal control problem model.
665+
666+
# Returns
667+
- `Bool`: `true` if the model has a non-empty abstract definition, `false` otherwise.
668+
669+
# Example
670+
```julia-repl
671+
julia> has_abstract_definition(model) # returns true if definition was attached
672+
```
673+
"""
674+
has_abstract_definition(ocp::Model)::Bool = !__is_definition_empty(definition(ocp))
675+
676+
"""
677+
$(TYPEDSIGNATURES)
678+
679+
Check whether the problem is abstractly defined.
680+
681+
# Arguments
682+
- `ocp::Model`: The optimal control problem model.
683+
684+
# Returns
685+
- `Bool`: `true` if the model has a non-empty abstract definition, `false` otherwise.
686+
687+
# Example
688+
```julia-repl
689+
julia> is_abstractly_defined(model) # returns true if definition was attached
690+
```
691+
"""
692+
is_abstractly_defined(ocp::Model)::Bool = has_abstract_definition(ocp)
693+
694+
"""
695+
$(TYPEDSIGNATURES)
696+
697+
Check whether the problem is non-autonomous (time-dependent).
698+
699+
# Arguments
700+
- `ocp::Model`: The optimal control problem model.
701+
702+
# Returns
703+
- `Bool`: `true` if the system is non-autonomous (time-dependent), `false` otherwise.
704+
705+
# Example
706+
```julia-repl
707+
julia> is_nonautonomous(model) # returns true if time-dependent
708+
```
709+
"""
710+
is_nonautonomous(ocp::Model)::Bool = !is_autonomous(ocp)
711+
712+
"""
713+
$(TYPEDSIGNATURES)
714+
715+
Check whether the problem has no optimisation variables.
716+
717+
# Arguments
718+
- `ocp::Model`: The optimal control problem model.
719+
720+
# Returns
721+
- `Bool`: `true` if the problem has no optimisation variables (variable dimension == 0), `false` otherwise.
722+
723+
# Example
724+
```julia-repl
725+
julia> is_nonvariable(model) # returns true if no variables
726+
```
727+
"""
728+
is_nonvariable(ocp::Model)::Bool = !is_variable(ocp)
729+
622730
# State
623731
"""
624732
$(TYPEDSIGNATURES)

src/OCP/Components/constraints.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ function constraint!(
319319

320320
# checks: control must be set for :control constraint type
321321
if type == :control
322-
@ensure __is_control_set(ocp) Exceptions.PreconditionError(
322+
@ensure !__is_control_empty(ocp) Exceptions.PreconditionError(
323323
"Control must be set for type=:control constraints",
324324
reason="control has not been defined yet but constraint type requires it",
325325
suggestion="Call control!(ocp, dimension) before adding :control constraints, or use a different constraint type",
@@ -328,7 +328,7 @@ function constraint!(
328328
end
329329

330330
# checks: variable must be set if using type=:variable
331-
@ensure (type != :variable || __is_variable_set(ocp)) Exceptions.PreconditionError(
331+
@ensure (type != :variable || !__is_variable_empty(ocp)) Exceptions.PreconditionError(
332332
"Variable must be set for type=:variable constraints",
333333
reason="OCP has no variable defined but constraint type requires it",
334334
suggestion="Call variable!(ocp, dimension) before adding variable constraints, or use a different constraint type",

src/OCP/Components/control.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ function control!(
5959
)::Nothing where {T1<:Union{String,Symbol},T2<:Union{String,Symbol}}
6060

6161
# checks using @ensure
62-
@ensure !__is_control_set(ocp) Exceptions.PreconditionError(
62+
@ensure __is_control_empty(ocp) Exceptions.PreconditionError(
6363
"Control already set",
6464
reason="control has already been defined for this OCP",
6565
suggestion="Create a new OCP instance or use the existing control definition",

src/OCP/Components/times.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,14 @@ function time!(
5656
context="time! function - duplicate definition check",
5757
)
5858

59-
@ensure __is_variable_set(ocp) || (isnothing(ind0) && isnothing(indf)) Exceptions.PreconditionError(
59+
@ensure !__is_variable_empty(ocp) || (isnothing(ind0) && isnothing(indf)) Exceptions.PreconditionError(
6060
"Variable must be set for free time",
6161
reason="variable is required when t0 or tf is free (ind0/indf provided)",
6262
suggestion="Call variable!(ocp, dimension) before time! with free time parameters, or use fixed times (t0, tf)",
6363
context="time! function - free time validation",
6464
)
6565

66-
if __is_variable_set(ocp)
66+
if !__is_variable_empty(ocp)
6767
q = dimension(ocp.variable)
6868

6969
@ensure isnothing(ind0) || (1 ind0 q) Exceptions.IncorrectArgument(

0 commit comments

Comments
 (0)