Skip to content

Commit 505b7d0

Browse files
committed
Add test addition of two Cycles with different observed names
1 parent 08818ad commit 505b7d0

File tree

1 file changed

+75
-0
lines changed

1 file changed

+75
-0
lines changed

tests/statespace/models/structural/components/test_cycle.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,78 @@ def test_cycle_multivariate_with_innovations_and_cycle_length(rng):
211211
for i in range(3):
212212
expected_Q[2 * i : 2 * i + 2, 2 * i : 2 * i + 2] = np.eye(2) * sigmas[i] ** 2
213213
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

Comments
 (0)