11import numpy as np
2+ import pytensor
23
34from numpy .testing import assert_allclose
45from pytensor import config
56
67from pymc_extras .statespace .models import structural as st
8+ from pymc_extras .statespace .models .structural .utils import _frequency_transition_block
79from tests .statespace .models .structural .conftest import _assert_basic_coords_correct
810from tests .statespace .test_utilities import assert_pattern_repeats , simulate_from_numpy_model
911
@@ -31,7 +33,7 @@ def test_cycle_component_with_dampening(rng):
3133 params = {"cycle" : np .array ([10.0 , 10.0 ], dtype = config .floatX ), "cycle_dampening_factor" : 0.75 }
3234 x , y = simulate_from_numpy_model (cycle , rng , params , steps = 100 )
3335
34- # Check that the cycle dampens to zero over time
36+ # check that cycle dampens to zero over time
3537 assert_allclose (y [- 1 ], 0.0 , atol = ATOL , rtol = RTOL )
3638
3739
@@ -75,6 +77,33 @@ def test_cycle_multivariate_deterministic(rng):
7577 assert np .std (y [:, 1 ]) > np .std (y [:, 0 ])
7678 assert np .std (y [:, 2 ]) > np .std (y [:, 0 ])
7779
80+ # check design, transition, selection matrices
81+ Z , T , R = pytensor .function (
82+ [],
83+ [cycle .ssm ["design" ], cycle .ssm ["transition" ], cycle .ssm ["selection" ]],
84+ mode = "FAST_COMPILE" ,
85+ )()
86+
87+ # each block is [1, 0] for design
88+ expected_Z = np .zeros ((3 , 6 ))
89+ expected_Z [0 , 0 ] = 1.0
90+ expected_Z [1 , 2 ] = 1.0
91+ expected_Z [2 , 4 ] = 1.0
92+ np .testing .assert_allclose (Z , expected_Z )
93+
94+ # each block is 2x2 frequency transition matrix for given cycle length (12 here)
95+ block = _frequency_transition_block (12 , 1 ).eval ()
96+ expected_T = np .zeros ((6 , 6 ))
97+ for i in range (3 ):
98+ expected_T [2 * i : 2 * i + 2 , 2 * i : 2 * i + 2 ] = block
99+ np .testing .assert_allclose (T , expected_T )
100+
101+ # each block is 2x2 identity for selection
102+ expected_R = np .zeros ((6 , 6 ))
103+ for i in range (3 ):
104+ expected_R [2 * i : 2 * i + 2 , 2 * i : 2 * i + 2 ] = np .eye (2 )
105+ np .testing .assert_allclose (R , expected_R )
106+
78107
79108def test_cycle_multivariate_with_dampening (rng ):
80109 """Test multivariate cycle component with dampening."""
@@ -118,7 +147,7 @@ def test_cycle_multivariate_with_innovations_and_cycle_length(rng):
118147 "cycle" : np .array ([[1.0 , 1.0 ], [2.0 , 2.0 ], [3.0 , 3.0 ]], dtype = config .floatX ),
119148 "cycle_length" : 12.0 ,
120149 "cycle_dampening_factor" : 0.95 ,
121- "sigma_cycle" : np .array ([0.5 , 1.0 , 1.5 ]), # Different innovation variances per variable
150+ "sigma_cycle" : np .array ([0.5 , 1.0 , 1.5 ]), # different innov variances per var
122151 }
123152 x , y = simulate_from_numpy_model (cycle , rng , params )
124153
@@ -138,3 +167,47 @@ def test_cycle_multivariate_with_innovations_and_cycle_length(rng):
138167 # Check that each variable shows some variation (due to innovations)
139168 for i in range (3 ):
140169 assert np .std (y [:, i ]) > 0
170+
171+ # check design, transition, selection & state_cov matrices
172+ Z , T , R , Q = pytensor .function (
173+ [
174+ cycle ._name_to_variable ["cycle_length" ],
175+ cycle ._name_to_variable ["cycle_dampening_factor" ],
176+ cycle ._name_to_variable ["sigma_cycle" ],
177+ ],
178+ [
179+ cycle .ssm ["design" ],
180+ cycle .ssm ["transition" ],
181+ cycle .ssm ["selection" ],
182+ cycle .ssm ["state_cov" ],
183+ ],
184+ mode = "FAST_COMPILE" ,
185+ )(params ["cycle_length" ], params ["cycle_dampening_factor" ], params ["sigma_cycle" ])
186+
187+ # each block is [1, 0] for design
188+ expected_Z = np .zeros ((3 , 6 ))
189+ expected_Z [0 , 0 ] = 1.0
190+ expected_Z [1 , 2 ] = 1.0
191+ expected_Z [2 , 4 ] = 1.0
192+ np .testing .assert_allclose (Z , expected_Z )
193+
194+ # each block is 2x2 frequency transition matrix for given cycle length (12 here),
195+ # scaled by dampening factor
196+ block = _frequency_transition_block (12 , 1 ).eval () * params ["cycle_dampening_factor" ]
197+ expected_T = np .zeros ((6 , 6 ))
198+ for i in range (3 ):
199+ expected_T [2 * i : 2 * i + 2 , 2 * i : 2 * i + 2 ] = block
200+ np .testing .assert_allclose (T , expected_T )
201+
202+ # each block is 2x2 identity for selection
203+ expected_R = np .zeros ((6 , 6 ))
204+ for i in range (3 ):
205+ expected_R [2 * i : 2 * i + 2 , 2 * i : 2 * i + 2 ] = np .eye (2 )
206+ np .testing .assert_allclose (R , expected_R )
207+
208+ # each block is sigma^2 * I_2 for state_cov
209+ sigmas = params ["sigma_cycle" ]
210+ expected_Q = np .zeros ((6 , 6 ))
211+ for i in range (3 ):
212+ expected_Q [2 * i : 2 * i + 2 , 2 * i : 2 * i + 2 ] = np .eye (2 ) * sigmas [i ] ** 2
213+ np .testing .assert_allclose (Q , expected_Q )
0 commit comments