Skip to content

Commit d975adb

Browse files
authored
Extend LinearStateSpace class (#569)
* extend `LinearStateSpace` class * Fix PEP 8 issues * Update lss.py Add conditions for finding the constant term in the transition matrix. * Update docstring
1 parent 6ffbe57 commit d975adb

File tree

2 files changed

+130
-45
lines changed

2 files changed

+130
-45
lines changed

quantecon/lss.py

+97-35
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import numpy as np
1313
from numpy.random import multivariate_normal
1414
from scipy.linalg import solve
15+
from .matrix_eqn import solve_discrete_lyapunov
1516
from numba import jit
1617
from .util import check_random_state
1718

@@ -281,59 +282,61 @@ def moment_sequence(self):
281282
mu_x = A.dot(mu_x)
282283
Sigma_x = A.dot(Sigma_x).dot(A.T) + C.dot(C.T)
283284

284-
def stationary_distributions(self, max_iter=200, tol=1e-5):
285+
def stationary_distributions(self):
285286
r"""
286287
Compute the moments of the stationary distributions of :math:`x_t` and
287-
:math:`y_t` if possible. Computation is by iteration, starting from
288-
the initial conditions self.mu_0 and self.Sigma_0
289-
290-
Parameters
291-
----------
292-
max_iter : scalar(int), optional(default=200)
293-
The maximum number of iterations allowed
294-
tol : scalar(float), optional(default=1e-5)
295-
The tolerance level that one wishes to achieve
288+
:math:`y_t` if possible. Computation is by solving the discrete
289+
Lyapunov equation.
296290
297291
Returns
298292
-------
299-
mu_x_star : array_like(float)
293+
mu_x : array_like(float)
300294
An n x 1 array representing the stationary mean of :math:`x_t`
301-
mu_y_star : array_like(float)
295+
mu_y : array_like(float)
302296
An k x 1 array representing the stationary mean of :math:`y_t`
303-
Sigma_x_star : array_like(float)
297+
Sigma_x : array_like(float)
304298
An n x n array representing the stationary var-cov matrix
305299
of :math:`x_t`
306-
Sigma_y_star : array_like(float)
300+
Sigma_y : array_like(float)
307301
An k x k array representing the stationary var-cov matrix
308302
of :math:`y_t`
303+
Sigma_yx : array_like(float)
304+
An k x n array representing the stationary cov matrix
305+
between :math:`y_t` and :math:`x_t`.
309306
310307
"""
311-
# == Initialize iteration == #
312-
m = self.moment_sequence()
313-
mu_x, mu_y, Sigma_x, Sigma_y = next(m)
314-
i = 0
315-
error = tol + 1
308+
self.__partition()
309+
num_const, sorted_idx = self.num_const, self.sorted_idx
310+
A21, A22 = self.A21, self.A22
311+
CC2 = self.C2 @ self.C2.T
312+
n = self.n
313+
314+
if num_const > 0:
315+
μ = solve(np.eye(n-num_const) - A22, A21)
316+
else:
317+
μ = solve(np.eye(n-num_const) - A22, np.zeros((n, 1)))
318+
Σ = solve_discrete_lyapunov(A22, CC2, method='bartels-stewart')
316319

317-
# == Loop until convergence or failure == #
318-
while error > tol:
320+
mu_x = np.empty((n, 1))
321+
mu_x[:num_const] = self.mu_0[sorted_idx[:num_const]]
322+
mu_x[num_const:] = μ
319323

320-
if i > max_iter:
321-
fail_message = 'Convergence failed after {} iterations'
322-
raise ValueError(fail_message.format(max_iter))
324+
Sigma_x = np.zeros((n, n))
325+
Sigma_x[num_const:, num_const:] = Σ
323326

324-
else:
325-
i += 1
326-
mu_x1, mu_y1, Sigma_x1, Sigma_y1 = next(m)
327-
error_mu = np.max(np.abs(mu_x1 - mu_x))
328-
error_Sigma = np.max(np.abs(Sigma_x1 - Sigma_x))
329-
error = max(error_mu, error_Sigma)
330-
mu_x, Sigma_x = mu_x1, Sigma_x1
327+
mu_x = self.P.T @ mu_x
328+
Sigma_x = self.P.T @ Sigma_x @ self.P
331329

332-
# == Prepare return values == #
333-
mu_x_star, Sigma_x_star = mu_x, Sigma_x
334-
mu_y_star, Sigma_y_star = mu_y1, Sigma_y1
330+
mu_y = self.G @ mu_x
331+
Sigma_y = self.G @ Sigma_x @ self.G.T
332+
if self.H is not None:
333+
Sigma_y += self.H @ self.H.T
334+
Sigma_yx = self.G @ Sigma_x
335+
336+
self.mu_x, self.mu_y = mu_x, mu_y
337+
self.Sigma_x, self.Sigma_y, self.Sigma_yx = Sigma_x, Sigma_y, Sigma_yx
335338

336-
return mu_x_star, mu_y_star, Sigma_x_star, Sigma_y_star
339+
return mu_x, mu_y, Sigma_x, Sigma_y, Sigma_yx
337340

338341
def geometric_sums(self, beta, x_t):
339342
r"""
@@ -406,3 +409,62 @@ def impulse_response(self, j=5):
406409
Apower = np.dot(Apower, A)
407410

408411
return xcoef, ycoef
412+
413+
def __partition(self):
414+
r"""
415+
Reorder the states by shifting the constant terms to the
416+
top of the state vector. Then partition the linear state
417+
space model following Appendix C in RMT Ch2 such that the
418+
A22 matrix associated with non-constant states have eigenvalues
419+
all strictly smaller than 1.
420+
421+
.. math::
422+
\left[\begin{array}{c}
423+
const\\
424+
x_{2,t+1}
425+
\end{array}\right]=\left[\begin{array}{cc}
426+
I & 0\\
427+
A_{21} & A_{22}
428+
\end{array}\right]\left[\begin{array}{c}
429+
1\\
430+
x_{2,t}
431+
\end{array}\right]+\left[\begin{array}{c}
432+
0\\
433+
C_{2}
434+
\end{array}\right]w_{t+1}
435+
436+
Returns
437+
-------
438+
A22 : array_like(float)
439+
Lower right part of the reordered matrix A.
440+
A21 : array_like(float)
441+
Lower left part of the reordered matrix A.
442+
"""
443+
A, C = self.A, self.C
444+
n = self.n
445+
446+
sorted_idx = []
447+
A_diag = np.diag(A)
448+
num_const = 0
449+
for idx in range(n):
450+
if (A_diag[idx] == 1) and (C[idx, :] == 0).all() \
451+
and np.linalg.norm(A[idx, :]) == 1:
452+
sorted_idx.insert(0, idx)
453+
num_const += 1
454+
else:
455+
sorted_idx.append(idx)
456+
self.num_const = num_const
457+
self.sorted_idx = sorted_idx
458+
459+
P = np.zeros((n, n))
460+
P[range(n), sorted_idx] = 1
461+
462+
sorted_A = P @ A @ P.T
463+
sorted_C = P @ C
464+
A21 = sorted_A[num_const:, :num_const]
465+
A22 = sorted_A[num_const:, num_const:]
466+
C2 = sorted_C[num_const:, :]
467+
468+
self.P, self.A21, self.A22, self.C2 = P, A21, A22, C2
469+
470+
return A21, A22

quantecon/tests/test_lss.py

+33-10
Original file line numberDiff line numberDiff line change
@@ -13,35 +13,58 @@
1313
class TestLinearStateSpace(unittest.TestCase):
1414

1515
def setUp(self):
16-
# Initial Values
16+
# Example 1
1717
A = .95
1818
C = .05
1919
G = 1.
2020
mu_0 = .75
2121

22-
self.ss = LinearStateSpace(A, C, G, mu_0=mu_0)
22+
self.ss1 = LinearStateSpace(A, C, G, mu_0=mu_0)
23+
24+
# Example 2
25+
ρ1 = 0.5
26+
ρ2 = 0.3
27+
α = 0.5
28+
29+
A = np.array([[ρ1, ρ2, α], [1, 0, 0], [0, 0, 1]])
30+
C = np.array([[1], [0], [0]])
31+
G = np.array([[1, 0, 0]])
32+
mu_0 = [0.5, 0.5, 1]
33+
34+
self.ss2 = LinearStateSpace(A, C, G, mu_0=mu_0)
2335

2436
def tearDown(self):
25-
del self.ss
37+
del self.ss1
38+
del self.ss2
2639

2740
def test_stationarity(self):
28-
vals = self.ss.stationary_distributions(max_iter=1000, tol=1e-9)
29-
ssmux, ssmuy, sssigx, sssigy = vals
41+
vals = self.ss1.stationary_distributions()
42+
ssmux, ssmuy, sssigx, sssigy, sssigyx = vals
3043

3144
self.assertTrue(abs(ssmux - ssmuy) < 2e-8)
3245
self.assertTrue(abs(sssigx - sssigy) < 2e-8)
3346
self.assertTrue(abs(ssmux) < 2e-8)
34-
self.assertTrue(abs(sssigx - self.ss.C**2/(1 - self.ss.A**2)) < 2e-8)
47+
self.assertTrue(abs(sssigx - self.ss1.C**2/(1 - self.ss1.A**2)) < 2e-8)
48+
self.assertTrue(abs(sssigyx - self.ss1.G @ sssigx) < 2e-8)
49+
50+
vals = self.ss2.stationary_distributions()
51+
ssmux, ssmuy, sssigx, sssigy, sssigyx = vals
52+
53+
assert_allclose(ssmux.flatten(), np.array([2.5, 2.5, 1]))
54+
assert_allclose(ssmuy.flatten(), np.array([2.5]))
55+
assert_allclose(sssigx, self.ss2.A @ sssigx @ self.ss2.A.T + self.ss2.C @ self.ss2.C.T)
56+
assert_allclose(sssigy, self.ss2.G @ sssigx @ self.ss2.G.T)
57+
assert_allclose(sssigyx, self.ss2.G @ sssigx)
3558

3659
def test_simulate(self):
37-
ss = self.ss
60+
ss = self.ss1
3861

3962
sim = ss.simulate(ts_length=250)
4063
for arr in sim:
4164
self.assertTrue(len(arr[0])==250)
4265

4366
def test_simulate_with_seed(self):
44-
ss = self.ss
67+
ss = self.ss1
4568

4669
xval, yval = ss.simulate(ts_length=5, random_state=5)
4770
expected_output = np.array([0.75 , 0.73456137, 0.6812898, 0.76876387,
@@ -51,14 +74,14 @@ def test_simulate_with_seed(self):
5174
assert_allclose(yval[0], expected_output)
5275

5376
def test_replicate(self):
54-
xval, yval = self.ss.replicate(T=100, num_reps=5000)
77+
xval, yval = self.ss1.replicate(T=100, num_reps=5000)
5578

5679
assert_allclose(xval, yval)
5780
self.assertEqual(xval.size, 5000)
5881
self.assertLessEqual(abs(np.mean(xval)), .05)
5982

6083
def test_replicate_with_seed(self):
61-
xval, yval = self.ss.replicate(T=100, num_reps=5, random_state=5)
84+
xval, yval = self.ss1.replicate(T=100, num_reps=5, random_state=5)
6285
expected_output = np.array([0.06871204, 0.06937119, -0.1478022,
6386
0.23841252, -0.06823762])
6487

0 commit comments

Comments
 (0)