1+ module TestControlZero
2+
3+ using Test: Test
4+ import CTParser
5+ import CTBase. Exceptions
6+ import CTModels. OCP
7+ import CTModels. Init
8+
9+ # for the @def and @init macros
10+ import CTBase
11+ import CTModels
12+ import ExaModels
13+ import MadNLP
14+
15+ include (joinpath (@__DIR__ , " utils.jl" ))
16+
17+ const VERBOSE = isdefined (Main, :TestData ) ? Main. TestData. VERBOSE : true
18+ const SHOWTIMING = isdefined (Main, :TestData ) ? Main. TestData. SHOWTIMING : true
19+
20+ function test_control_zero ()
21+ Test. @testset " Control Zero Dimension Tests" verbose= VERBOSE showtiming= SHOWTIMING begin
22+
23+ # Build a Model without control
24+ function get_model (; variable= false )
25+ if variable
26+ return CTParser. @def begin
27+ v ∈ R, variable
28+ t ∈ [0 , 1 ], time
29+ x ∈ R², state
30+ ẋ (t) == [x₂ (t), - x₁ (t)]
31+ x₁ (1 )^ 2 + v → min
32+ end
33+ else
34+ return CTParser. @def begin
35+ t ∈ [0 , 1 ], time
36+ x ∈ R², state
37+ ẋ (t) == [x₂ (t), - x₁ (t)]
38+ x₁ (1 )^ 2 → min
39+ end
40+ end
41+ end
42+
43+ # ====================================================================
44+ # UNIT TESTS - Building without control
45+ # ====================================================================
46+
47+ Test. @testset " build() - Model without control" begin
48+ o = get_model ()
49+ Test. @test o isa OCP. Model
50+ Test. @test OCP. control_dimension (o) == 0
51+ Test. @test OCP. control_name (o) == " "
52+ Test. @test OCP. control_components (o) == String[]
53+ end
54+
55+ Test. @testset " build() - Model without control but with variable" begin
56+ # build a model with a variable
57+ ov = get_model (variable= true )
58+ Test. @test OCP. control_dimension (ov) == 0
59+ Test. @test OCP. variable_dimension (ov) == 1
60+ Test. @test OCP. state_dimension (ov) == 2
61+ end
62+
63+ # ====================================================================
64+ # UNIT TESTS - Declaration Order Validation
65+ # ====================================================================
66+
67+ Test. @testset " Control declaration order validation" begin
68+ # Control after dynamics should fail
69+ Test. @test_throws Exceptions. ParsingError begin
70+ CTParser. @def begin
71+ t ∈ [0 , 1 ], time
72+ x ∈ R², state
73+ ẋ (t) == [x₂ (t), - x₁ (t)]
74+ u ∈ R, control # ❌ After dynamics
75+ x₁ (1 )^ 2 → min
76+ end
77+ end
78+
79+ # Control after cost should fail
80+ Test. @test_throws Exceptions. ParsingError begin
81+ CTParser. @def begin
82+ t ∈ [0 , 1 ], time
83+ x ∈ R², state
84+ x₁ (1 )^ 2 → min
85+ u ∈ R, control # ❌ After cost
86+ end
87+ end
88+ end
89+
90+ # ====================================================================
91+ # UNIT TESTS - Coordinate Dynamics Without Control
92+ # ====================================================================
93+
94+ Test. @testset " Coordinate dynamics without control" begin
95+ o = CTParser. @def begin
96+ t ∈ [0 , 1 ], time
97+ x ∈ R², state
98+ ∂ (x₁)(t) == x₂ (t)
99+ ∂ (x₂)(t) == - x₁ (t)
100+ x₁ (1 )^ 2 → min
101+ end
102+ Test. @test OCP. control_dimension (o) == 0
103+ Test. @test OCP. state_dimension (o) == 2
104+ end
105+
106+ # ====================================================================
107+ # UNIT TESTS - Advanced Cost Criteria Without Control
108+ # ====================================================================
109+
110+ Test. @testset " Advanced cost criteria without control" begin
111+ # Lagrange cost
112+ o1 = CTParser. @def begin
113+ t ∈ [0 , 1 ], time
114+ x ∈ R², state
115+ ẋ (t) == [x₂ (t), - x₁ (t)]
116+ ∫ (x₁ (t)^ 2 + x₂ (t)^ 2 ) → min
117+ end
118+ Test. @test OCP. control_dimension (o1) == 0
119+
120+ # Bolza cost
121+ o2 = CTParser. @def begin
122+ t ∈ [0 , 1 ], time
123+ x ∈ R², state
124+ ẋ (t) == [x₂ (t), - x₁ (t)]
125+ x₁ (0 )^ 2 + ∫ (x₂ (t)^ 2 ) → min
126+ end
127+ Test. @test OCP. control_dimension (o2) == 0
128+ end
129+
130+ # ====================================================================
131+ # UNIT TESTS - Constraints Without Control
132+ # ====================================================================
133+
134+ Test. @testset " Constraints without control" begin
135+ o = CTParser. @def begin
136+ t ∈ [0 , 1 ], time
137+ x ∈ R², state
138+ ẋ (t) == [x₂ (t), - x₁ (t)]
139+ x₁ (0 ) == 1
140+ x₂ (0 ) == 0
141+ x₁ (1 ) + x₂ (1 ) ≤ 1
142+ x₁ (1 )^ 2 → min
143+ end
144+ Test. @test OCP. control_dimension (o) == 0
145+ Test. @test OCP. state_dimension (o) == 2
146+ end
147+
148+ # ====================================================================
149+ # UNIT TESTS - Initialization without control
150+ # ====================================================================
151+
152+ Test. @testset " Init - initial_control with scalar throws error" begin
153+ o = get_model ()
154+ Test. @test_throws Exceptions. IncorrectArgument begin
155+ ig = CTParser. @init o begin
156+ u (t) := 0.5
157+ end
158+ end
159+ end
160+
161+ Test. @testset " Init - initial_control with non-empty vector throws error" begin
162+ o = get_model ()
163+ Test. @test_throws Exceptions. IncorrectArgument begin
164+ ig = CTParser. @init o begin
165+ u (t) := [0.5 ]
166+ end
167+ end
168+ end
169+
170+ Test. @testset " Init - initial_guess without control" begin
171+ o = get_model ()
172+ ig = CTParser. @init o begin end
173+ u_init = OCP. control (ig)
174+ Test. @test ig isa Init. InitialGuess
175+ Test. @test u_init isa Function
176+ Test. @test u_init (0.5 ) == Float64[]
177+ end
178+
179+ Test. @testset " Advanced initialization without control" begin
180+ # Test with state initialization only
181+ o = get_model ()
182+ ig = CTParser. @init o begin
183+ x (t) := [sin (t), cos (t)]
184+ end
185+ Test. @test ig isa Init. InitialGuess
186+
187+ # Test with variable initialization
188+ o = get_model (; variable= true )
189+ ig2 = CTParser. @init o begin
190+ x (t) := [sin (t), cos (t)]
191+ v := 1.0
192+ end
193+ Test. @test ig2 isa Init. InitialGuess
194+ Test. @test OCP. variable (ig2) == 1.0
195+ end
196+
197+ # ====================================================================
198+ # INTEGRATION TESTS - Getter Function Without Control
199+ # ====================================================================
200+
201+ Test. @testset " Getter function without control" begin
202+ o = get_model ()
203+
204+ # Define grid size explicitly
205+ N = 10
206+
207+ # Build NLP and getter using discretise_exa_full
208+ m, exa_getter = discretise_exa_full (o; grid_size= N)
209+ Test. @test m isa ExaModels. ExaModel
210+
211+ # Solve the NLP to get a solution
212+ sol = MadNLP. madnlp (m; tol= 1e-6 , print_level= MadNLP. ERROR)
213+ Test. @test sol. status == MadNLP. SOLVE_SUCCEEDED
214+
215+ # Test getter function - this should work without throwing errors
216+ # Test state retrieval (should work)
217+ state_result = exa_getter (sol, val= :state )
218+ Test. @test state_result isa Matrix
219+ Test. @test size (state_result) == (2 , N+ 1 ) # N grid points
220+
221+ # Test control retrieval (should return empty array, not throw error)
222+ control_result = exa_getter (sol, val= :control )
223+ Test. @test control_result isa Vector
224+ Test. @test length (control_result) == 0
225+
226+ # Test control multipliers (should return empty array, not throw error)
227+ control_l_result = exa_getter (sol, val= :control_l )
228+ Test. @test control_l_result isa Vector
229+ Test. @test length (control_l_result) == 0
230+
231+ control_u_result = exa_getter (sol, val= :control_u )
232+ Test. @test control_u_result isa Vector
233+ Test. @test length (control_u_result) == 0
234+ end
235+
236+ # ====================================================================
237+ # INTEGRATION TESTS - Serialization without control
238+ # ====================================================================
239+
240+ Test. @testset " Serialization - Solution building without control" begin
241+ o = get_model ()
242+ # Create a solution without control
243+ T = collect (range (0 , 1 , length= 10 ))
244+ x_data = hcat (sin .(T), cos .(T)) # (10, 2) matrix
245+ u_data = Matrix {Float64} (undef, 10 , 0 ) # Empty control matrix (10×0)
246+ p_data = hcat (cos .(T), - sin .(T)) # (10, 2) matrix
247+ v_data = Float64[]
248+
249+ sol = OCP. build_solution (
250+ o,
251+ T,
252+ T,
253+ T,
254+ T,
255+ x_data,
256+ u_data,
257+ v_data,
258+ p_data;
259+ objective= 1.0 ,
260+ iterations= 10 ,
261+ constraints_violation= 0.0 ,
262+ message= " Test solution" ,
263+ status= :success ,
264+ successful= true ,
265+ )
266+
267+ # Test that control_dimension is 0
268+ Test. @test OCP. control_dimension (sol) == 0
269+
270+ # Test that control function returns empty vector
271+ u_func = OCP. control (sol)
272+ Test. @test u_func (0.5 ) == Float64[]
273+
274+ # Test that solution properties are correct
275+ Test. @test OCP. state_dimension (sol) == 2
276+ Test. @test OCP. objective (sol) == 1.0
277+ end
278+
279+ end
280+ end
281+
282+ end # module
283+
284+ # CRITICAL: Redefine in outer scope for TestRunner
285+ test_control_zero () = TestControlZero. test_control_zero ()
0 commit comments