@@ -211,3 +211,78 @@ def test_cycle_multivariate_with_innovations_and_cycle_length(rng):
211
211
for i in range (3 ):
212
212
expected_Q [2 * i : 2 * i + 2 , 2 * i : 2 * i + 2 ] = np .eye (2 ) * sigmas [i ] ** 2
213
213
np .testing .assert_allclose (Q , expected_Q )
214
+
215
+
216
+ def test_add_multivariate_cycle_components_with_different_observed ():
217
+ """
218
+ Test adding two multivariate CycleComponents with different observed_state_names.
219
+ Ensures that combining two multivariate CycleComponents with different observed state names
220
+ results in the correct block-diagonal state space matrices and state naming.
221
+ """
222
+ cycle1 = st .CycleComponent (
223
+ name = "cycle1" ,
224
+ cycle_length = 12 ,
225
+ estimate_cycle_length = False ,
226
+ innovations = False ,
227
+ observed_state_names = ["a1" , "a2" ],
228
+ )
229
+ cycle2 = st .CycleComponent (
230
+ name = "cycle2" ,
231
+ cycle_length = 6 ,
232
+ estimate_cycle_length = False ,
233
+ innovations = False ,
234
+ observed_state_names = ["b1" , "b2" ],
235
+ )
236
+ mod = (cycle1 + cycle2 ).build (verbose = False )
237
+
238
+ # check dimensions
239
+ assert mod .k_endog == 4
240
+ assert mod .k_states == 8
241
+ assert mod .k_posdef == 2 * mod .k_endog # 2 innovations per variable
242
+
243
+ # check state names and coords
244
+ expected_state_names = [
245
+ "cycle1_Cos[a1]" ,
246
+ "cycle1_Sin[a1]" ,
247
+ "cycle1_Cos[a2]" ,
248
+ "cycle1_Sin[a2]" ,
249
+ "cycle2_Cos[b1]" ,
250
+ "cycle2_Sin[b1]" ,
251
+ "cycle2_Cos[b2]" ,
252
+ "cycle2_Sin[b2]" ,
253
+ ]
254
+ assert mod .state_names == expected_state_names
255
+
256
+ assert mod .coords ["cycle1_state" ] == ["cycle1_Cos" , "cycle1_Sin" ]
257
+ assert mod .coords ["cycle2_state" ] == ["cycle2_Cos" , "cycle2_Sin" ]
258
+ assert mod .coords ["cycle1_endog" ] == ["a1" , "a2" ]
259
+ assert mod .coords ["cycle2_endog" ] == ["b1" , "b2" ]
260
+
261
+ # evaluate design, transition, selection matrices
262
+ Z , T , R = pytensor .function (
263
+ [], [mod .ssm ["design" ], mod .ssm ["transition" ], mod .ssm ["selection" ]], mode = "FAST_COMPILE"
264
+ )()
265
+
266
+ # design: each row selects first state of its block
267
+ expected_Z = np .zeros ((4 , 8 ))
268
+ expected_Z [0 , 0 ] = 1.0 # "a1" -> cycle1_Cos[a1]
269
+ expected_Z [1 , 2 ] = 1.0 # "a2" -> cycle1_Cos[a2]
270
+ expected_Z [2 , 4 ] = 1.0 # "b1" -> cycle2_Cos[b1]
271
+ expected_Z [3 , 6 ] = 1.0 # "b2" -> cycle2_Cos[b2]
272
+ assert_allclose (Z , expected_Z )
273
+
274
+ # transition: block diagonal, each block is 2x2 frequency transition matrix
275
+ block1 = _frequency_transition_block (12 , 1 ).eval ()
276
+ block2 = _frequency_transition_block (6 , 1 ).eval ()
277
+ expected_T = np .zeros ((8 , 8 ))
278
+ for i in range (2 ):
279
+ expected_T [2 * i : 2 * i + 2 , 2 * i : 2 * i + 2 ] = block1
280
+ for i in range (2 ):
281
+ expected_T [4 + 2 * i : 4 + 2 * i + 2 , 4 + 2 * i : 4 + 2 * i + 2 ] = block2
282
+ assert_allclose (T , expected_T )
283
+
284
+ # selection: block diagonal, each block is 2x2 identity
285
+ expected_R = np .zeros ((8 , 8 ))
286
+ for i in range (4 ):
287
+ expected_R [2 * i : 2 * i + 2 , 2 * i : 2 * i + 2 ] = np .eye (2 )
288
+ assert_allclose (R , expected_R )
0 commit comments