From bfc0198474043e72d715e8e3d746f52c14860f8c Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Mon, 20 Apr 2026 12:48:40 +0200 Subject: [PATCH 1/6] Refactor predicates: strict separation of __is_*_set and __is_*_empty - Remove all __is_*_set(::Model) methods (trivial/unused) - Remove __is_*_set(::PreModel) for optional fields (control, variable, definition) - Add __is_*_empty(ocp::PreModel) overloads delegating to component - Rewrite __is_empty(::PreModel) to use overloads for optional fields - Remove __is_complete(::PreModel) (redundant with __is_consistent) - Migrate all call sites from __is_*_set to !__is_*_empty for optional fields - Update test assertions to reflect new predicate semantics - Remove dead import __is_definition_set from Display.jl This establishes clear semantic separation: - __is_*_set: only for mandatory fields on PreModel - __is_*_empty: only for optional fields (works on PreModel via overload) --- src/Display/Display.jl | 2 +- src/OCP/Components/constraints.jl | 4 +- src/OCP/Components/control.jl | 2 +- src/OCP/Components/times.jl | 4 +- src/OCP/Components/variable.jl | 2 +- src/OCP/Types/components.jl | 4 +- src/OCP/Types/model.jl | 108 +++++-------------------- src/OCP/Validation/name_validation.jl | 20 ++--- test/suite/ocp/test_control.jl | 4 +- test/suite/ocp/test_control_zero.jl | 11 --- test/suite/ocp/test_definition.jl | 12 +-- test/suite/ocp/test_ocp_model_types.jl | 27 +++---- test/suite/ocp/test_variable.jl | 4 +- 13 files changed, 58 insertions(+), 146 deletions(-) diff --git a/src/Display/Display.jl b/src/Display/Display.jl index 59a6592e..8e99963b 100644 --- a/src/Display/Display.jl +++ b/src/Display/Display.jl @@ -39,7 +39,7 @@ 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: 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 diff --git a/src/OCP/Components/constraints.jl b/src/OCP/Components/constraints.jl index b7d30b66..4ad91eda 100644 --- a/src/OCP/Components/constraints.jl +++ b/src/OCP/Components/constraints.jl @@ -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", @@ -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", diff --git a/src/OCP/Components/control.jl b/src/OCP/Components/control.jl index 778acfe4..9642ef88 100644 --- a/src/OCP/Components/control.jl +++ b/src/OCP/Components/control.jl @@ -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", diff --git a/src/OCP/Components/times.jl b/src/OCP/Components/times.jl index 8d01fd8f..1cc12bcc 100644 --- a/src/OCP/Components/times.jl +++ b/src/OCP/Components/times.jl @@ -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( diff --git a/src/OCP/Components/variable.jl b/src/OCP/Components/variable.jl index ab3323aa..9809a538 100644 --- a/src/OCP/Components/variable.jl +++ b/src/OCP/Components/variable.jl @@ -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", diff --git a/src/OCP/Types/components.jl b/src/OCP/Types/components.jl index d3858b88..cdce5cd0 100644 --- a/src/OCP/Types/components.jl +++ b/src/OCP/Types/components.jl @@ -191,8 +191,8 @@ The methods `name`, `components`, and `dimension` are defined for this type and julia> using CTModels julia> pre = CTModels.PreModel() -julia> CTModels.OCP.__is_control_set(pre) # false — still EmptyControlModel -false +julia> CTModels.OCP.__is_control_empty(pre.control) # true — still EmptyControlModel +true julia> CTModels.OCP.control_dimension(pre) # 0 0 diff --git a/src/OCP/Types/model.jl b/src/OCP/Types/model.jl index 9b2ddd4e..5720a466 100644 --- a/src/OCP/Types/model.jl +++ b/src/OCP/Types/model.jl @@ -107,67 +107,6 @@ struct Model{ end end -""" -$(TYPEDSIGNATURES) - -Return `true` since times are always set in a built [`Model`](@ref CTModels.OCP.Model). -""" -__is_times_set(ocp::Model)::Bool = true - -""" -$(TYPEDSIGNATURES) - -Return `true` since state is always set in a built [`Model`](@ref CTModels.OCP.Model). -""" -__is_state_set(ocp::Model)::Bool = true - -""" -$(TYPEDSIGNATURES) - -Return `true` since the control field is always structurally set in a built -[`Model`](@ref CTModels.OCP.Model) (i.e. it is never `nothing`). - -Note: this does not imply a positive control dimension. The model may hold an -[`EmptyControlModel`](@ref) when the user did not call `control!`, in which case -`control_dimension(ocp) == 0`. -""" -__is_control_set(ocp::Model)::Bool = true - -""" -$(TYPEDSIGNATURES) - -Return `true` since variable is always set in a built [`Model`](@ref CTModels.OCP.Model). -""" -__is_variable_set(ocp::Model)::Bool = true - -""" -$(TYPEDSIGNATURES) - -Return `true` since dynamics is always set in a built [`Model`](@ref CTModels.OCP.Model). -""" -__is_dynamics_set(ocp::Model)::Bool = true - -""" -$(TYPEDSIGNATURES) - -Return `true` since objective is always set in a built [`Model`](@ref CTModels.OCP.Model). -""" -__is_objective_set(ocp::Model)::Bool = true - -""" -$(TYPEDSIGNATURES) - -Return `true` if `d` is an [`EmptyDefinition`](@ref). -""" -__is_definition_empty(d) = d isa EmptyDefinition - -""" -$(TYPEDSIGNATURES) - -Return `true` since definition is always structurally set in a built -[`Model`](@ref CTModels.OCP.Model). -""" -__is_definition_set(ocp::Model)::Bool = true """ $(TYPEDEF) @@ -252,9 +191,9 @@ __is_control_empty(c) = c isa EmptyControlModel """ $(TYPEDSIGNATURES) -Return `true` if a non-empty control has been set in the `PreModel`. +Return `true` if the control field of the `PreModel` is an `EmptyControlModel`. """ -__is_control_set(ocp::PreModel)::Bool = !__is_control_empty(ocp.control) +__is_control_empty(ocp::PreModel)::Bool = __is_control_empty(ocp.control) """ $(TYPEDSIGNATURES) @@ -266,31 +205,37 @@ __is_variable_empty(v) = v isa EmptyVariableModel """ $(TYPEDSIGNATURES) -Return `true` if a non-empty variable has been set in the `PreModel`. +Return `true` if the variable field of the `PreModel` is an `EmptyVariableModel`. """ -__is_variable_set(ocp::PreModel)::Bool = !__is_variable_empty(ocp.variable) +__is_variable_empty(ocp::PreModel)::Bool = __is_variable_empty(ocp.variable) """ $(TYPEDSIGNATURES) -Return `true` if dynamics have been set in the `PreModel`. +Return `true` if `d` is an [`EmptyDefinition`](@ref). """ -__is_dynamics_set(ocp::PreModel)::Bool = __is_set(ocp.dynamics) +__is_definition_empty(d) = d isa EmptyDefinition """ $(TYPEDSIGNATURES) -Return `true` if objective has been set in the `PreModel`. +Return `true` if the definition field of the `PreModel` is an [`EmptyDefinition`](@ref). """ -__is_objective_set(ocp::PreModel)::Bool = __is_set(ocp.objective) +__is_definition_empty(ocp::PreModel)::Bool = __is_definition_empty(ocp.definition) + +""" +$(TYPEDSIGNATURES) + +Return `true` if dynamics have been set in the `PreModel`. +""" +__is_dynamics_set(ocp::PreModel)::Bool = __is_set(ocp.dynamics) """ $(TYPEDSIGNATURES) -Return `true` if a non-empty definition has been set in the `PreModel` -(i.e. [`definition!`](@ref) was called with a [`Definition`](@ref)). +Return `true` if objective has been set in the `PreModel`. """ -__is_definition_set(ocp::PreModel)::Bool = !__is_definition_empty(ocp.definition) +__is_objective_set(ocp::PreModel)::Bool = __is_set(ocp.objective) """ $(TYPEDSIGNATURES) @@ -373,29 +318,16 @@ end """ $(TYPEDSIGNATURES) -Return true if the PreModel can be built into a Model. -""" -function __is_complete(ocp::PreModel)::Bool - return __is_times_set(ocp) && - __is_state_set(ocp) && - __is_dynamics_complete(ocp) && - __is_objective_set(ocp) && - __is_autonomous_set(ocp) -end - -""" -$(TYPEDSIGNATURES) - Return true if nothing has been set. """ function __is_empty(ocp::PreModel)::Bool return !__is_times_set(ocp) && !__is_state_set(ocp) && - !__is_control_set(ocp) && !__is_dynamics_set(ocp) && !__is_objective_set(ocp) && - !__is_definition_set(ocp) && - !__is_variable_set(ocp) && !__is_autonomous_set(ocp) && + __is_control_empty(ocp) && + __is_variable_empty(ocp) && + __is_definition_empty(ocp) && Base.isempty(ocp.constraints) end diff --git a/src/OCP/Validation/name_validation.jl b/src/OCP/Validation/name_validation.jl index 784fab44..3cd326d2 100644 --- a/src/OCP/Validation/name_validation.jl +++ b/src/OCP/Validation/name_validation.jl @@ -44,18 +44,16 @@ function __collect_used_names(ocp::PreModel)::Vector{String} end # Control name and components - if __is_control_set(ocp) + if !__is_control_empty(ocp) push!(names, name(ocp.control)) append!(names, components(ocp.control)) end # Variable name and components (if not empty) - if __is_variable_set(ocp) + if !__is_variable_empty(ocp) var_model = ocp.variable - if !isa(var_model, EmptyVariableModel) - push!(names, name(var_model)) - append!(names, components(var_model)) - end + push!(names, name(var_model)) + append!(names, components(var_model)) end # Return unique names (to handle case where name == component for scalars) @@ -103,15 +101,13 @@ function __has_name_conflict( if exclude_component == :state && __is_state_set(ocp) filter!(x -> x != name(ocp.state), existing_names) filter!(x -> x ∉ components(ocp.state), existing_names) - elseif exclude_component == :control && __is_control_set(ocp) + elseif exclude_component == :control && !__is_control_empty(ocp) filter!(x -> x != name(ocp.control), existing_names) filter!(x -> x ∉ components(ocp.control), existing_names) - elseif exclude_component == :variable && __is_variable_set(ocp) + elseif exclude_component == :variable && !__is_variable_empty(ocp) var_model = ocp.variable - if !isa(var_model, EmptyVariableModel) - filter!(x -> x != name(var_model), existing_names) - filter!(x -> x ∉ components(var_model), existing_names) - end + filter!(x -> x != name(var_model), existing_names) + filter!(x -> x ∉ components(var_model), existing_names) elseif exclude_component == :time && __is_times_set(ocp) filter!(x -> x != time_name(ocp.times), existing_names) end diff --git a/test/suite/ocp/test_control.jl b/test/suite/ocp/test_control.jl index a9ecdd7b..551fc0c5 100644 --- a/test/suite/ocp/test_control.jl +++ b/test/suite/ocp/test_control.jl @@ -26,9 +26,9 @@ function test_control() # some checks ocp = CTModels.PreModel() Test.@test ocp.control isa CTModels.OCP.EmptyControlModel - Test.@test !CTModels.OCP.__is_control_set(ocp) + Test.@test CTModels.OCP.__is_control_empty(ocp) CTModels.control!(ocp, 1) - Test.@test CTModels.OCP.__is_control_set(ocp) + Test.@test !CTModels.OCP.__is_control_empty(ocp) # control! ocp = CTModels.PreModel() diff --git a/test/suite/ocp/test_control_zero.jl b/test/suite/ocp/test_control_zero.jl index fb97cc2a..74a01835 100644 --- a/test/suite/ocp/test_control_zero.jl +++ b/test/suite/ocp/test_control_zero.jl @@ -56,17 +56,6 @@ function test_control_zero() Test.@test OCP.__is_control_empty(cm) == false end - Test.@testset "Helper Functions - __is_control_set" begin - # PreModel with EmptyControlModel should return false - pre = OCP.PreModel() - Test.@test OCP.__is_control_set(pre) == false - - # PreModel after control! should return true - pre2 = OCP.PreModel() - OCP.control!(pre2, 1) - Test.@test OCP.__is_control_set(pre2) == true - end - # ==================================================================== # UNIT TESTS - Phase 2: Relaxed Preconditions # ==================================================================== diff --git a/test/suite/ocp/test_definition.jl b/test/suite/ocp/test_definition.jl index b679b930..3f3eea1c 100644 --- a/test/suite/ocp/test_definition.jl +++ b/test/suite/ocp/test_definition.jl @@ -36,7 +36,7 @@ function test_definition() Test.@testset "PreModel default definition is EmptyDefinition" begin pre = CTModels.PreModel() Test.@test pre.definition isa CTModels.EmptyDefinition - Test.@test !CTModels.OCP.__is_definition_set(pre) + Test.@test CTModels.OCP.__is_definition_empty(pre) end # ==================================================================== @@ -49,7 +49,7 @@ function test_definition() CTModels.definition!(pre, expr) Test.@test pre.definition isa CTModels.Definition Test.@test pre.definition.expr === expr - Test.@test CTModels.OCP.__is_definition_set(pre) + Test.@test !CTModels.OCP.__is_definition_empty(pre) end Test.@testset "definition! accepts AbstractDefinition directly" begin @@ -57,14 +57,14 @@ function test_definition() d = CTModels.Definition(:(y = 2)) CTModels.definition!(pre, d) Test.@test pre.definition === d - Test.@test CTModels.OCP.__is_definition_set(pre) + Test.@test !CTModels.OCP.__is_definition_empty(pre) end Test.@testset "definition! with EmptyDefinition leaves predicate false" begin pre = CTModels.PreModel() CTModels.definition!(pre, CTModels.EmptyDefinition()) Test.@test pre.definition isa CTModels.EmptyDefinition - Test.@test !CTModels.OCP.__is_definition_set(pre) + Test.@test CTModels.OCP.__is_definition_empty(pre) end # ==================================================================== @@ -114,7 +114,7 @@ function test_definition() model = CTModels.build(pre) Test.@test CTModels.definition(model) isa CTModels.EmptyDefinition - Test.@test CTModels.OCP.__is_definition_set(model) + Test.@test CTModels.OCP.__is_definition_empty(model.definition) Test.@test CTModels.expression(model) isa Expr Test.@test CTModels.expression(model).head == :block end @@ -141,7 +141,7 @@ function test_definition() model = CTModels.build(pre) Test.@test CTModels.definition(model) isa CTModels.Definition Test.@test CTModels.definition(model).expr === expr - Test.@test CTModels.OCP.__is_definition_set(model) + Test.@test !CTModels.OCP.__is_definition_empty(model.definition) Test.@test CTModels.expression(model) === expr end end diff --git a/test/suite/ocp/test_ocp_model_types.jl b/test/suite/ocp/test_ocp_model_types.jl index 5b050e67..61009a8e 100644 --- a/test/suite/ocp/test_ocp_model_types.jl +++ b/test/suite/ocp/test_ocp_model_types.jl @@ -66,13 +66,9 @@ function test_ocp_model_types() typeof(build_examodel), } - Test.@test CTModels.OCP.__is_times_set(ocp) - Test.@test CTModels.OCP.__is_state_set(ocp) - Test.@test CTModels.OCP.__is_control_set(ocp) - Test.@test CTModels.OCP.__is_variable_set(ocp) - Test.@test CTModels.OCP.__is_dynamics_set(ocp) - Test.@test CTModels.OCP.__is_objective_set(ocp) - Test.@test CTModels.OCP.__is_definition_set(ocp) + Test.@test !CTModels.OCP.__is_control_empty(ocp.control) + Test.@test !CTModels.OCP.__is_variable_empty(ocp.variable) + Test.@test CTModels.OCP.__is_definition_empty(ocp.definition) end Test.@testset "__is_* predicates on PreModel" begin @@ -82,10 +78,10 @@ function test_ocp_model_types() Test.@test CTModels.OCP.__is_empty(ocp) Test.@test !CTModels.OCP.__is_times_set(ocp) Test.@test !CTModels.OCP.__is_state_set(ocp) - Test.@test !CTModels.OCP.__is_control_set(ocp) + Test.@test CTModels.OCP.__is_control_empty(ocp) Test.@test !CTModels.OCP.__is_dynamics_set(ocp) Test.@test !CTModels.OCP.__is_objective_set(ocp) - Test.@test !CTModels.OCP.__is_definition_set(ocp) + Test.@test CTModels.OCP.__is_definition_empty(ocp) times = CTModels.TimesModel( CTModels.FixedTimeModel(0.0, "t₀"), CTModels.FixedTimeModel(1.0, "t_f"), "t" @@ -106,20 +102,19 @@ function test_ocp_model_types() Test.@test CTModels.OCP.__is_times_set(ocp) Test.@test CTModels.OCP.__is_state_set(ocp) - Test.@test CTModels.OCP.__is_control_set(ocp) - Test.@test CTModels.OCP.__is_variable_set(ocp) + Test.@test !CTModels.OCP.__is_control_empty(ocp) + Test.@test !CTModels.OCP.__is_variable_empty(ocp) Test.@test CTModels.OCP.__is_dynamics_set(ocp) Test.@test CTModels.OCP.__is_objective_set(ocp) Test.@test CTModels.OCP.__is_autonomous_set(ocp) - # definition is optional: model is complete without it + # definition is optional: model is consistent without it Test.@test CTModels.OCP.__is_consistent(ocp) - Test.@test CTModels.OCP.__is_complete(ocp) ocp.definition = CTModels.Definition(quote end) - Test.@test CTModels.OCP.__is_definition_set(ocp) - Test.@test CTModels.OCP.__is_complete(ocp) + Test.@test !CTModels.OCP.__is_definition_empty(ocp) + Test.@test CTModels.OCP.__is_consistent(ocp) Test.@test !CTModels.OCP.__is_empty(ocp) end @@ -129,7 +124,7 @@ function test_ocp_model_types() Test.@testset "fake PreModel buildability" begin function can_build(ocp_local) - return CTModels.OCP.__is_complete(ocp_local) + return CTModels.OCP.__is_consistent(ocp_local) end empty_ocp = CTModels.PreModel() diff --git a/test/suite/ocp/test_variable.jl b/test/suite/ocp/test_variable.jl index 0273190c..b3e56a80 100644 --- a/test/suite/ocp/test_variable.jl +++ b/test/suite/ocp/test_variable.jl @@ -27,9 +27,9 @@ function test_variable() # some checks ocp = CTModels.PreModel() Test.@test ocp.variable isa CTModels.EmptyVariableModel - Test.@test !CTModels.OCP.__is_variable_set(ocp) + Test.@test CTModels.OCP.__is_variable_empty(ocp) CTModels.variable!(ocp, 1) - Test.@test CTModels.OCP.__is_variable_set(ocp) + Test.@test !CTModels.OCP.__is_variable_empty(ocp) # variable! ocp = CTModels.PreModel() From 9e0622bc07be5beb856802ed1d096b489aa7b238 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Mon, 20 Apr 2026 13:11:46 +0200 Subject: [PATCH 2/6] Add user-facing Model predicates and restrict predicates to Model only - Add has_variable, has_control, has_abstract_definition, is_abstractly_defined, is_nonautonomous, is_nonvariable for Model - Remove is_autonomous, is_variable, is_control_free on PreModel - Update Display call sites to use internal predicates - Update tests accordingly --- src/Display/Display.jl | 1 + src/Display/pre_model.jl | 6 +- src/OCP/Building/model.jl | 110 +++++++++++++++++- src/OCP/Core/time_dependence.jl | 54 --------- src/OCP/OCP.jl | 3 + test/suite/ocp/test_ocp_model_types.jl | 59 ++++++++++ test/suite/ocp/test_time_dependence.jl | 6 +- .../suite/ocp/test_variable_control_checks.jl | 58 ++------- 8 files changed, 186 insertions(+), 111 deletions(-) diff --git a/src/Display/Display.jl b/src/Display/Display.jl index 8e99963b..4312e8da 100644 --- a/src/Display/Display.jl +++ b/src/Display/Display.jl @@ -40,6 +40,7 @@ import ..OCP: AbstractDefinition, Definition, EmptyDefinition # Import internal helpers from OCP for display 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 diff --git a/src/Display/pre_model.jl b/src/Display/pre_model.jl index abd916f3..c49c9952 100644 --- a/src/Display/pre_model.jl +++ b/src/Display/pre_model.jl @@ -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) diff --git a/src/OCP/Building/model.jl b/src/OCP/Building/model.jl index 8488f96b..f14680e5 100644 --- a/src/OCP/Building/model.jl +++ b/src/OCP/Building/model.jl @@ -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}( @@ -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) diff --git a/src/OCP/Core/time_dependence.jl b/src/OCP/Core/time_dependence.jl index 54d34930..25d35e91 100644 --- a/src/OCP/Core/time_dependence.jl +++ b/src/OCP/Core/time_dependence.jl @@ -33,57 +33,3 @@ function time_dependence!(ocp::PreModel; autonomous::Bool)::Nothing ocp.autonomous = autonomous return nothing end - -""" -$(TYPEDSIGNATURES) - -Check whether the system is autonomous. - -# Arguments -- `ocp::PreModel`: The optimal control problem. - -# Returns -- `Bool`: `true` if the system is autonomous (i.e., does not explicitly depend on time), `false` otherwise. - -# Example -```julia-repl -julia> is_autonomous(ocp) # returns true or false -``` -""" -is_autonomous(ocp::PreModel) = ocp.autonomous - -""" -$(TYPEDSIGNATURES) - -Check whether the problem has optimisation variables. - -# Arguments -- `ocp::PreModel`: The optimal control problem. - -# Returns -- `Bool`: `true` if the problem has optimisation variables (variable dimension > 0), `false` otherwise. - -# Example -```julia-repl -julia> is_variable(ocp) # returns true if variables are present -``` -""" -is_variable(ocp::PreModel) = dimension(ocp.variable) > 0 - -""" -$(TYPEDSIGNATURES) - -Check whether the problem is control-free (no control input). - -# Arguments -- `ocp::PreModel`: The optimal control problem. - -# Returns -- `Bool`: `true` if the problem has no control input (control dimension == 0), `false` otherwise. - -# Example -```julia-repl -julia> is_control_free(ocp) # returns true if no control input -``` -""" -is_control_free(ocp::PreModel) = dimension(ocp.control) == 0 diff --git a/src/OCP/OCP.jl b/src/OCP/OCP.jl index 25b43d00..359cb6e1 100644 --- a/src/OCP/OCP.jl +++ b/src/OCP/OCP.jl @@ -111,6 +111,9 @@ export has_fixed_initial_time, has_free_initial_time export has_fixed_final_time, has_free_final_time export is_autonomous export is_variable, is_control_free +export has_variable, has_control +export has_abstract_definition, is_abstractly_defined +export is_nonautonomous, is_nonvariable export is_initial_time_fixed, is_initial_time_free export is_final_time_fixed, is_final_time_free export state_dimension, control_dimension, variable_dimension diff --git a/test/suite/ocp/test_ocp_model_types.jl b/test/suite/ocp/test_ocp_model_types.jl index 61009a8e..81512d8a 100644 --- a/test/suite/ocp/test_ocp_model_types.jl +++ b/test/suite/ocp/test_ocp_model_types.jl @@ -151,6 +151,65 @@ function test_ocp_model_types() Test.@test can_build(ocp) end + + # ======================================================================== + # User-Facing Predicates + # ======================================================================== + + Test.@testset "User-Facing Predicates" begin + dyn!(r, t, x, u, v) = r .= 0 + mayer_fn(x0, xf, v) = 0.0 + + function build_model(; with_variable=false, with_control=true, + autonomous=true, with_definition=false) + ocp = CTModels.PreModel() + CTModels.time!(ocp; t0=0.0, tf=1.0) + CTModels.state!(ocp, 2) + with_control && CTModels.control!(ocp, 1) + with_variable && CTModels.variable!(ocp, 1) + CTModels.dynamics!(ocp, dyn!) + CTModels.objective!(ocp, :min; mayer=mayer_fn) + with_definition && CTModels.definition!(ocp, :(ẋ = Ax + Bu)) + CTModels.time_dependence!(ocp; autonomous=autonomous) + return CTModels.build(ocp) + end + + Test.@testset "has_variable / is_nonvariable" begin + model = build_model(with_variable=false) + Test.@test !CTModels.has_variable(model) + Test.@test CTModels.is_nonvariable(model) + + model2 = build_model(with_variable=true) + Test.@test CTModels.has_variable(model2) + Test.@test !CTModels.is_nonvariable(model2) + end + + Test.@testset "has_control" begin + model = build_model(with_control=false) + Test.@test !CTModels.has_control(model) + + model2 = build_model(with_control=true) + Test.@test CTModels.has_control(model2) + end + + Test.@testset "is_nonautonomous" begin + model = build_model(autonomous=true) + Test.@test !CTModels.is_nonautonomous(model) + + model2 = build_model(autonomous=false) + Test.@test CTModels.is_nonautonomous(model2) + end + + Test.@testset "has_abstract_definition / is_abstractly_defined" begin + model = build_model(with_definition=false) + Test.@test !CTModels.has_abstract_definition(model) + Test.@test !CTModels.is_abstractly_defined(model) + + model2 = build_model(with_definition=true) + Test.@test CTModels.has_abstract_definition(model2) + Test.@test CTModels.is_abstractly_defined(model2) + end + end end end diff --git a/test/suite/ocp/test_time_dependence.jl b/test/suite/ocp/test_time_dependence.jl index 0f9569e7..b41fef44 100644 --- a/test/suite/ocp/test_time_dependence.jl +++ b/test/suite/ocp/test_time_dependence.jl @@ -31,7 +31,7 @@ function test_time_dependence() # Set once CTModels.time_dependence!(ocp; autonomous=true) Test.@test CTModels.OCP.__is_autonomous_set(ocp) - Test.@test CTModels.is_autonomous(ocp) === true + Test.@test ocp.autonomous === true # Second call must fail Test.@test_throws Exceptions.PreconditionError CTModels.time_dependence!( @@ -66,8 +66,8 @@ function test_time_dependence() pre_autonomous = build_premodel_with_time_dependence(true) pre_nonautonomous = build_premodel_with_time_dependence(false) - Test.@test CTModels.is_autonomous(pre_autonomous) === true - Test.@test CTModels.is_autonomous(pre_nonautonomous) === false + Test.@test pre_autonomous.autonomous === true + Test.@test pre_nonautonomous.autonomous === false end end end diff --git a/test/suite/ocp/test_variable_control_checks.jl b/test/suite/ocp/test_variable_control_checks.jl index 68f34b05..bc8b133d 100644 --- a/test/suite/ocp/test_variable_control_checks.jl +++ b/test/suite/ocp/test_variable_control_checks.jl @@ -9,48 +9,6 @@ const SHOWTIMING = isdefined(Main, :TestData) ? Main.TestData.SHOWTIMING : true function test_variable_control_checks() Test.@testset "Variable and Control Checks Tests" verbose=VERBOSE showtiming=SHOWTIMING begin - # ==================================================================== - # UNIT TESTS - PreModel Methods - # ==================================================================== - - Test.@testset "is_variable - PreModel" begin - Test.@testset "Default PreModel (no variable)" begin - ocp = CTModels.PreModel() - Test.@test CTModels.is_variable(ocp) === false - end - - Test.@testset "PreModel with variable" begin - ocp = CTModels.PreModel() - CTModels.variable!(ocp, 2) - Test.@test CTModels.is_variable(ocp) === true - end - - Test.@testset "PreModel with zero dimension variable" begin - ocp = CTModels.PreModel() - CTModels.variable!(ocp, 0) - Test.@test CTModels.is_variable(ocp) === false - end - end - - Test.@testset "is_control_free - PreModel" begin - Test.@testset "Default PreModel (no control)" begin - ocp = CTModels.PreModel() - Test.@test CTModels.is_control_free(ocp) === true - end - - Test.@testset "PreModel with control" begin - ocp = CTModels.PreModel() - CTModels.control!(ocp, 1) - Test.@test CTModels.is_control_free(ocp) === false - end - - Test.@testset "PreModel with multiple control inputs" begin - ocp = CTModels.PreModel() - CTModels.control!(ocp, 3) - Test.@test CTModels.is_control_free(ocp) === false - end - end - # ==================================================================== # UNIT TESTS - Model Methods # ==================================================================== @@ -154,8 +112,8 @@ function test_variable_control_checks() CTModels.control!(ocp, 1) CTModels.variable!(ocp, 1) - Test.@test CTModels.is_variable(ocp) === true - Test.@test CTModels.is_control_free(ocp) === false + Test.@test !CTModels.OCP.__is_variable_empty(ocp) + Test.@test !CTModels.OCP.__is_control_empty(ocp) end Test.@testset "PreModel with variable only (control-free)" begin @@ -164,8 +122,8 @@ function test_variable_control_checks() CTModels.state!(ocp, 2) CTModels.variable!(ocp, 1) - Test.@test CTModels.is_variable(ocp) === true - Test.@test CTModels.is_control_free(ocp) === true + Test.@test !CTModels.OCP.__is_variable_empty(ocp) + Test.@test CTModels.OCP.__is_control_empty(ocp) end Test.@testset "PreModel with control only (no variable)" begin @@ -174,8 +132,8 @@ function test_variable_control_checks() CTModels.state!(ocp, 2) CTModels.control!(ocp, 1) - Test.@test CTModels.is_variable(ocp) === false - Test.@test CTModels.is_control_free(ocp) === false + Test.@test CTModels.OCP.__is_variable_empty(ocp) + Test.@test !CTModels.OCP.__is_control_empty(ocp) end Test.@testset "PreModel with neither variable nor control" begin @@ -183,8 +141,8 @@ function test_variable_control_checks() CTModels.time!(ocp; t0=0.0, tf=1.0) CTModels.state!(ocp, 2) - Test.@test CTModels.is_variable(ocp) === false - Test.@test CTModels.is_control_free(ocp) === true + Test.@test CTModels.OCP.__is_variable_empty(ocp) + Test.@test CTModels.OCP.__is_control_empty(ocp) end end From c06702a425007c44cec08874e1690419384357e3 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Mon, 20 Apr 2026 13:14:13 +0200 Subject: [PATCH 3/6] Update CHANGELOG and BREAKING for 0.9.15 predicate changes - Document removal of PreModel predicates (breaking change) - Add new user-facing Model predicates documentation - Update internal changes section --- BREAKING.md | 53 ++++++++++++++++++++++++++++------------------------ CHANGELOG.md | 40 ++++++++++++++++++++++----------------- 2 files changed, 52 insertions(+), 41 deletions(-) diff --git a/BREAKING.md b/BREAKING.md index af4b6488..1e12921e 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 538243d3..9b3294d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 From 65d1922e1aed83a58fa808cbff4b3e5fa76cef5f Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Mon, 20 Apr 2026 13:55:27 +0200 Subject: [PATCH 4/6] up to beta version --- BREAKING.md | 2 +- CHANGELOG.md | 2 +- Project.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/BREAKING.md b/BREAKING.md index 1e12921e..836cffc6 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b3294d1..7ec080cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Project.toml b/Project.toml index 3d69df7c..871ec183 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "CTModels" uuid = "34c4fa32-2049-4079-8329-de33c2a22e2d" -version = "0.9.15" +version = "0.9.15-beta" authors = ["Olivier Cots "] [deps] From 172626593f3ad0168cda54dbb23f3ff9449dab1c Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Mon, 20 Apr 2026 14:02:51 +0200 Subject: [PATCH 5/6] foo --- src/CTModels.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CTModels.jl b/src/CTModels.jl index a9edd7e9..4ceb6316 100644 --- a/src/CTModels.jl +++ b/src/CTModels.jl @@ -91,4 +91,4 @@ using .Serialization include(joinpath(@__DIR__, "Init", "Init.jl")) using .Init -end +end \ No newline at end of file From fa7553e3ed3e6af12ce2a7fa7237b1b6686b9701 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Mon, 20 Apr 2026 16:55:06 +0200 Subject: [PATCH 6/6] Bump version to 0.10.0 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 871ec183..601afe94 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "CTModels" uuid = "34c4fa32-2049-4079-8329-de33c2a22e2d" -version = "0.9.15-beta" +version = "0.10.0" authors = ["Olivier Cots "] [deps]