Skip to content
Open
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
10 changes: 6 additions & 4 deletions src/onepass.jl
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,8 @@ function p_control!(
p, p_ocp, u, m; components_names=nothing, log=false, backend=__default_parsing_backend()
)
log && println("control: $u, dim: $m")
(p.is_global_dyn || p.is_coord_dyn) && return __throw("control must be declared before dynamics", p.lnum, p.line)
!isnothing(p.criterion) && return __throw("control must be declared before cost criterion", p.lnum, p.line)
u isa Symbol || return __throw("forbidden control name: $u", p.lnum, p.line)
uu = QuoteNode(u)
if m == 1
Expand Down Expand Up @@ -848,7 +850,7 @@ function p_dynamics!(
log && println("dynamics: βˆ‚($x)($t) == $e")
isnothing(label) || return __throw("dynamics cannot be labelled", p.lnum, p.line)
isnothing(p.x) && return __throw("state not yet declared", p.lnum, p.line)
isnothing(p.u) && return __throw("control not yet declared", p.lnum, p.line)
# isnothing(p.u) && return __throw("control not yet declared", p.lnum, p.line)
isnothing(p.t) && return __throw("time not yet declared", p.lnum, p.line)
x β‰  p.x && return __throw("wrong state $x for dynamics", p.lnum, p.line)
t β‰  p.t && return __throw("wrong time $t for dynamics", p.lnum, p.line)
Expand Down Expand Up @@ -948,7 +950,7 @@ function p_dynamics_coord!(
log && println("dynamics: βˆ‚($x[$i])($t) == $e")
isnothing(label) || return __throw("dynamics cannot be labelled", p.lnum, p.line)
isnothing(p.x) && return __throw("state not yet declared", p.lnum, p.line)
isnothing(p.u) && return __throw("control not yet declared", p.lnum, p.line)
# isnothing(p.u) && return __throw("control not yet declared", p.lnum, p.line)
isnothing(p.t) && return __throw("time not yet declared", p.lnum, p.line)
x β‰  p.x && return __throw("wrong state $x for dynamics", p.lnum, p.line)
t β‰  p.t && return __throw("wrong time $t for dynamics", p.lnum, p.line)
Expand Down Expand Up @@ -1038,7 +1040,7 @@ end
function p_lagrange!(p, p_ocp, e, type; log=false, backend=__default_parsing_backend())
log && println("objective (Lagrange): ∫($e) β†’ $type")
isnothing(p.x) && return __throw("state not yet declared", p.lnum, p.line)
isnothing(p.u) && return __throw("control not yet declared", p.lnum, p.line)
# isnothing(p.u) && return __throw("control not yet declared", p.lnum, p.line)
isnothing(p.t) && return __throw("time not yet declared", p.lnum, p.line)
xut = __symgen(:xut)
ee = replace_call(e, [p.x, p.u], p.t, [xut, xut])
Expand Down Expand Up @@ -1159,7 +1161,7 @@ function p_bolza!(p, p_ocp, e1, e2, type; log=false, backend=__default_parsing_b
isnothing(p.x) && return __throw("state not yet declared", p.lnum, p.line)
isnothing(p.t0) && return __throw("time not yet declared", p.lnum, p.line)
isnothing(p.tf) && return __throw("time not yet declared", p.lnum, p.line)
isnothing(p.u) && return __throw("control not yet declared", p.lnum, p.line)
# isnothing(p.u) && return __throw("control not yet declared", p.lnum, p.line)
isnothing(p.t) && return __throw("time not yet declared", p.lnum, p.line)
xut = __symgen(:xut)
ee2 = replace_call(e2, [p.x, p.u], p.t, [xut, xut])
Expand Down
6 changes: 3 additions & 3 deletions src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -264,16 +264,16 @@ julia> e = :( ((x^2)(t0) + u[1])(t) ); replace_call(e, [ x, u ], t , [ :xx, :uu
:((xx ^ 2)(t0) + uu[1])
```
"""
function replace_call(e, x::Vector{Symbol}, t, y)
function replace_call(e, x::Vector{<:Union{Nothing, Symbol}}, t, y)
@assert length(x) == length(y)
foo(x, t, y) =
(h, args...) -> begin
ee = Expr(h, args...)
@match ee begin
:($eee($tt)) && if tt == t
end => let ch = false
for i in 1:length(x)
if has(eee, x[i])
for i in eachindex(x)
if !isnothing(x[i]) && has(eee, x[i]) # skip Nothing symbols
eee = subs(eee, x[i], y[i])
ch = true # todo: unnecessary (as subs can be idempotent)?
end
Expand Down
8 changes: 6 additions & 2 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,12 @@ using NLPModels

include("utils.jl")

const VERBOSE = true
const SHOWTIMING = true
# Controls nested testset output formatting (used by individual test files)
module TestData
const VERBOSE = true
const SHOWTIMING = true
end
using .TestData: VERBOSE, SHOWTIMING

# Run tests using the TestRunner extension
CTBase.run_tests(;
Expand Down
242 changes: 242 additions & 0 deletions test/test_control_zero.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
module TestControlZero

using Test: Test
import CTParser
import CTBase.Exceptions
import CTModels.OCP
import CTModels.Init

# for the @def and @init macros
import CTBase
import CTModels

const VERBOSE = isdefined(Main, :TestData) ? Main.TestData.VERBOSE : true
const SHOWTIMING = isdefined(Main, :TestData) ? Main.TestData.SHOWTIMING : true

function test_control_zero()
Test.@testset "Control Zero Dimension Tests" verbose=VERBOSE showtiming=SHOWTIMING begin

# Build a Model without control
function get_model(; variable=false)
if variable
return CTParser.@def begin
v ∈ R, variable
t ∈ [0, 1], time
x ∈ R², state
αΊ‹(t) == [xβ‚‚(t), -x₁(t)]
x₁(1)^2 + v β†’ min
end
else
return CTParser.@def begin
t ∈ [0, 1], time
x ∈ R², state
αΊ‹(t) == [xβ‚‚(t), -x₁(t)]
x₁(1)^2 β†’ min
end
end
end

# ====================================================================
# UNIT TESTS - Building without control
# ====================================================================

Test.@testset "build() - Model without control" begin
o = get_model()
Test.@test o isa OCP.Model
Test.@test OCP.control_dimension(o) == 0
Test.@test OCP.control_name(o) == ""
Test.@test OCP.control_components(o) == String[]
end

Test.@testset "build() - Model without control but with variable" begin
# build a model with a variable
ov = get_model(variable=true)
Test.@test OCP.control_dimension(ov) == 0
Test.@test OCP.variable_dimension(ov) == 1
Test.@test OCP.state_dimension(ov) == 2
end

# ====================================================================
# UNIT TESTS - Declaration Order Validation
# ====================================================================

Test.@testset "Control declaration order validation" begin
# Control after dynamics should fail
Test.@test_throws Exceptions.ParsingError begin
CTParser.@def begin
t ∈ [0, 1], time
x ∈ R², state
αΊ‹(t) == [xβ‚‚(t), -x₁(t)]
u ∈ R, control # ❌ After dynamics
x₁(1)^2 β†’ min
end
end

# Control after cost should fail
Test.@test_throws Exceptions.ParsingError begin
CTParser.@def begin
t ∈ [0, 1], time
x ∈ R², state
x₁(1)^2 β†’ min
u ∈ R, control # ❌ After cost
end
end
end

# ====================================================================
# UNIT TESTS - Coordinate Dynamics Without Control
# ====================================================================

Test.@testset "Coordinate dynamics without control" begin
o = CTParser.@def begin
t ∈ [0, 1], time
x ∈ R², state
βˆ‚(x₁)(t) == xβ‚‚(t)
βˆ‚(xβ‚‚)(t) == -x₁(t)
x₁(1)^2 β†’ min
end
Test.@test OCP.control_dimension(o) == 0
Test.@test OCP.state_dimension(o) == 2
end

# ====================================================================
# UNIT TESTS - Advanced Cost Criteria Without Control
# ====================================================================

Test.@testset "Advanced cost criteria without control" begin
# Lagrange cost
o1 = CTParser.@def begin
t ∈ [0, 1], time
x ∈ R², state
αΊ‹(t) == [xβ‚‚(t), -x₁(t)]
∫(x₁(t)^2 + xβ‚‚(t)^2) β†’ min
end
Test.@test OCP.control_dimension(o1) == 0

# Bolza cost
o2 = CTParser.@def begin
t ∈ [0, 1], time
x ∈ R², state
αΊ‹(t) == [xβ‚‚(t), -x₁(t)]
x₁(0)^2 + ∫(xβ‚‚(t)^2) β†’ min
end
Test.@test OCP.control_dimension(o2) == 0
end

# ====================================================================
# UNIT TESTS - Constraints Without Control
# ====================================================================

Test.@testset "Constraints without control" begin
o = CTParser.@def begin
t ∈ [0, 1], time
x ∈ R², state
αΊ‹(t) == [xβ‚‚(t), -x₁(t)]
x₁(0) == 1
xβ‚‚(0) == 0
x₁(1) + xβ‚‚(1) ≀ 1
x₁(1)^2 β†’ min
end
Test.@test OCP.control_dimension(o) == 0
Test.@test OCP.state_dimension(o) == 2
end

# ====================================================================
# UNIT TESTS - Initialization without control
# ====================================================================

Test.@testset "Init - initial_control with scalar throws error" begin
o = get_model()
Test.@test_throws Exceptions.IncorrectArgument begin
ig = CTParser.@init o begin
u(t) := 0.5
end
end
end

Test.@testset "Init - initial_control with non-empty vector throws error" begin
o = get_model()
Test.@test_throws Exceptions.IncorrectArgument begin
ig = CTParser.@init o begin
u(t) := [0.5]
end
end
end

Test.@testset "Init - initial_guess without control" begin
o = get_model()
ig = CTParser.@init o begin end
u_init = OCP.control(ig)
Test.@test ig isa Init.InitialGuess
Test.@test u_init isa Function
Test.@test u_init(0.5) == Float64[]
end

Test.@testset "Advanced initialization without control" begin
# Test with state initialization only
o = get_model()
ig = CTParser.@init o begin
x(t) := [sin(t), cos(t)]
end
Test.@test ig isa Init.InitialGuess

# Test with variable initialization
o = get_model(; variable=true)
ig2 = CTParser.@init o begin
x(t) := [sin(t), cos(t)]
v := 1.0
end
Test.@test ig2 isa Init.InitialGuess
Test.@test OCP.variable(ig2) == 1.0
end

# ====================================================================
# INTEGRATION TESTS - Serialization without control
# ====================================================================

Test.@testset "Serialization - Solution building without control" begin
o = get_model()
# Create a solution without control
T = collect(range(0, 1, length=10))
x_data = hcat(sin.(T), cos.(T)) # (10, 2) matrix
u_data = Matrix{Float64}(undef, 10, 0) # Empty control matrix (10Γ—0)
p_data = hcat(cos.(T), -sin.(T)) # (10, 2) matrix
v_data = Float64[]

sol = OCP.build_solution(
o,
T,
T,
T,
T,
x_data,
u_data,
v_data,
p_data;
objective=1.0,
iterations=10,
constraints_violation=0.0,
message="Test solution",
status=:success,
successful=true,
)

# Test that control_dimension is 0
Test.@test OCP.control_dimension(sol) == 0

# Test that control function returns empty vector
u_func = OCP.control(sol)
Test.@test u_func(0.5) == Float64[]

# Test that solution properties are correct
Test.@test OCP.state_dimension(sol) == 2
Test.@test OCP.objective(sol) == 1.0
end

end
end

end # module

# CRITICAL: Redefine in outer scope for TestRunner
test_control_zero() = TestControlZero.test_control_zero()
Loading