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

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

## [0.9.15] - 2026-04-18
## [0.9.15-beta] - 2026-04-18

### Breaking Changes: Dual Dimension Function Renaming

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

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

### Non-Breaking Changes
### Breaking Changes: PreModel Predicate Removal

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

#### New Functions (Non-Breaking)
- `is_autonomous(ocp::PreModel)` - removed
- `is_variable(ocp::PreModel)` - removed
- `is_control_free(ocp::PreModel)` - removed

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

#### What Changed
#### Migration Guide

```julia
# New functions available (non-breaking)
is_variable(ocp) # Returns true if variable_dimension > 0
is_control_free(ocp) # Returns true if control_dimension == 0

# Works for both PreModel and Model
ocp = PreModel()
state!(ocp, 2)
control!(ocp, 1)
variable!(ocp, 2)

is_variable(ocp) # Returns true
is_control_free(ocp) # Returns false
# Before (PreModel access)
pre = PreModel()
state!(pre, 2)
control!(pre, 1)
variable!(pre, 2)
time_dependence!(pre; autonomous=true)

# These no longer work:
is_autonomous(pre) # MethodError
is_variable(pre) # MethodError
is_control_free(pre) # MethodError

# After (use direct field access or internal predicates)
pre.autonomous # true/false
!CTModels.OCP.__is_variable_empty(pre) # true/false
CTModels.OCP.__is_control_empty(pre) # true/false
```

#### Migration
#### Rationale

- **No action required**: Existing code continues to work unchanged
- **Optional enhancement**: Can use new functions for more readable code instead of inline dimension comparisons
- **Same API**: No changes to existing user-facing API; behavior is fully backward compatible
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.

#### Note

The predicate methods `is_autonomous(model)`, `is_variable(model)`, and `is_control_free(model)` for `Model` remain unchanged and continue to work as before.

## [0.9.14] - 2026-04-12

Expand Down
42 changes: 24 additions & 18 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ 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.9.15] - 2026-04-18
## [0.9.15-beta] - 2026-04-18

### 🚀 Enhancements

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

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

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

#### API Enhancements

```julia
# Check if problem has optimisation variables
is_variable(ocp::PreModel) # Returns true if variable_dimension > 0
is_variable(ocp::Model) # Returns true if variable_dimension > 0
has_variable(model) # Alias for is_variable(model)
is_nonvariable(model) # Opposite of is_variable(model)

# Check if problem is control-free (no control input)
is_control_free(ocp::PreModel) # Returns true if control_dimension == 0
is_control_free(ocp::Model) # Returns true if control_dimension == 0
# Check if problem has control input
has_control(model) # Opposite of is_control_free(model)

# Check if problem has abstract definition
has_abstract_definition(model) # Checks if definition is non-empty
is_abstractly_defined(model) # Alias for has_abstract_definition

# Check time dependence
is_nonautonomous(model) # Opposite of is_autonomous(model)
```

### 📊 API Changes

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

### 🔧 Internal Changes

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

## [0.9.14] - 2026-04-12

Expand Down
2 changes: 1 addition & 1 deletion 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.9.15"
version = "0.10.0"
authors = ["Olivier Cots <olivier.cots@toulouse-inp.fr>"]

[deps]
Expand Down
2 changes: 1 addition & 1 deletion src/CTModels.jl
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,4 @@ using .Serialization
include(joinpath(@__DIR__, "Init", "Init.jl"))
using .Init

end
end
3 changes: 2 additions & 1 deletion src/Display/Display.jl
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ import ..OCP: Model, PreModel, Solution, AbstractSolution
import ..OCP: AbstractDefinition, Definition, EmptyDefinition

# Import internal helpers from OCP for display
import ..OCP: __is_empty, __is_definition_set, definition, __is_consistent
import ..OCP: __is_empty, definition, __is_consistent
import ..OCP: __is_variable_empty, __is_control_empty
import ..OCP: state_dimension, control_dimension, variable_dimension
import ..OCP: time_name, initial_time_name, final_time_name
import ..OCP: dimension, name, state_name, control_name, variable_name
Expand Down
6 changes: 3 additions & 3 deletions src/Display/pre_model.jl
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ function Base.show(io::IO, ::MIME"text/plain", ocp::PreModel)
vi_names = components(ocp.variable)

# dependencies
is_variable_dependent = is_variable(ocp)
is_time_dependent = !is_autonomous(ocp)
is_control_free_ocp = is_control_free(ocp)
is_variable_dependent = v_dim > 0
is_time_dependent = !ocp.autonomous
is_control_free_ocp = u_dim == 0

# cost
has_a_lagrange_cost = has_lagrange_cost(ocp.objective)
Expand Down
110 changes: 109 additions & 1 deletion src/OCP/Building/model.jl
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@ function build(pre_ocp::PreModel; build_examodel=nothing)::Model
objective = pre_ocp.objective
constraints = build(pre_ocp.constraints)
definition = pre_ocp.definition
TD = is_autonomous(pre_ocp) ? Autonomous : NonAutonomous
TD = pre_ocp.autonomous ? Autonomous : NonAutonomous

# create the model
model = Model{TD}(
Expand Down Expand Up @@ -619,6 +619,114 @@ function is_control_free(ocp::Model)::Bool
return control_dimension(ocp) == 0
end

"""
$(TYPEDSIGNATURES)

Check whether the problem has optimisation variables.

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

# Returns
- `Bool`: `true` if the problem has optimisation variables (variable dimension > 0), `false` otherwise.

# Example
```julia-repl
julia> has_variable(model) # returns true if variables are present
```
"""
has_variable(ocp::Model)::Bool = is_variable(ocp)

"""
$(TYPEDSIGNATURES)

Check whether the problem has control input.

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

# Returns
- `Bool`: `true` if the problem has control input (control dimension > 0), `false` otherwise.

# Example
```julia-repl
julia> has_control(model) # returns true if control is present
```
"""
has_control(ocp::Model)::Bool = !is_control_free(ocp)

"""
$(TYPEDSIGNATURES)

Check whether the problem has an abstract definition.

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

# Returns
- `Bool`: `true` if the model has a non-empty abstract definition, `false` otherwise.

# Example
```julia-repl
julia> has_abstract_definition(model) # returns true if definition was attached
```
"""
has_abstract_definition(ocp::Model)::Bool = !__is_definition_empty(definition(ocp))

"""
$(TYPEDSIGNATURES)

Check whether the problem is abstractly defined.

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

# Returns
- `Bool`: `true` if the model has a non-empty abstract definition, `false` otherwise.

# Example
```julia-repl
julia> is_abstractly_defined(model) # returns true if definition was attached
```
"""
is_abstractly_defined(ocp::Model)::Bool = has_abstract_definition(ocp)

"""
$(TYPEDSIGNATURES)

Check whether the problem is non-autonomous (time-dependent).

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

# Returns
- `Bool`: `true` if the system is non-autonomous (time-dependent), `false` otherwise.

# Example
```julia-repl
julia> is_nonautonomous(model) # returns true if time-dependent
```
"""
is_nonautonomous(ocp::Model)::Bool = !is_autonomous(ocp)

"""
$(TYPEDSIGNATURES)

Check whether the problem has no optimisation variables.

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

# Returns
- `Bool`: `true` if the problem has no optimisation variables (variable dimension == 0), `false` otherwise.

# Example
```julia-repl
julia> is_nonvariable(model) # returns true if no variables
```
"""
is_nonvariable(ocp::Model)::Bool = !is_variable(ocp)

# State
"""
$(TYPEDSIGNATURES)
Expand Down
4 changes: 2 additions & 2 deletions src/OCP/Components/constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ function constraint!(

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

# checks: variable must be set if using type=:variable
@ensure (type != :variable || __is_variable_set(ocp)) Exceptions.PreconditionError(
@ensure (type != :variable || !__is_variable_empty(ocp)) Exceptions.PreconditionError(
"Variable must be set for type=:variable constraints",
reason="OCP has no variable defined but constraint type requires it",
suggestion="Call variable!(ocp, dimension) before adding variable constraints, or use a different constraint type",
Expand Down
2 changes: 1 addition & 1 deletion src/OCP/Components/control.jl
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ function control!(
)::Nothing where {T1<:Union{String,Symbol},T2<:Union{String,Symbol}}

# checks using @ensure
@ensure !__is_control_set(ocp) Exceptions.PreconditionError(
@ensure __is_control_empty(ocp) Exceptions.PreconditionError(
"Control already set",
reason="control has already been defined for this OCP",
suggestion="Create a new OCP instance or use the existing control definition",
Expand Down
4 changes: 2 additions & 2 deletions src/OCP/Components/times.jl
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,14 @@ function time!(
context="time! function - duplicate definition check",
)

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

if __is_variable_set(ocp)
if !__is_variable_empty(ocp)
q = dimension(ocp.variable)

@ensure isnothing(ind0) || (1 ≤ ind0 ≤ q) Exceptions.IncorrectArgument(
Expand Down
2 changes: 1 addition & 1 deletion src/OCP/Components/variable.jl
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function variable!(
name::T1=__variable_name(q),
components_names::Vector{T2}=__variable_components(q, string(name)),
)::Nothing where {T1<:Union{String,Symbol},T2<:Union{String,Symbol}}
@ensure !__is_variable_set(ocp) Exceptions.PreconditionError(
@ensure __is_variable_empty(ocp) Exceptions.PreconditionError(
"Variable already set",
reason="variable has already been defined for this OCP",
suggestion="Create a new OCP instance or use the existing variable definition",
Expand Down
Loading
Loading