diff --git a/quantecon/__init__.py b/quantecon/__init__.py index 6726d501f..a8cb08ead 100644 --- a/quantecon/__init__.py +++ b/quantecon/__init__.py @@ -12,51 +12,43 @@ "Cannot import numba from current anaconda distribution. \ Please run `conda install numba` to install the latest version.") -# Modules +#-Modules-# from . import distributions from . import game_theory from . import quad from . import random from . import optimize -# Objects -from ._compute_fp import compute_fixed_point -from ._discrete_rv import DiscreteRV -from ._dle import DLE -from ._ecdf import ECDF -from ._estspec import smooth, periodogram, ar_periodogram -from ._graph_tools import DiGraph, random_tournament_graph -from ._gridtools import (cartesian, mlinspace, cartesian_nearest_index, - simplex_grid, simplex_index, num_compositions) -from ._inequality import (lorenz_curve, gini_coefficient, shorrocks_index, - rank_size) -from ._kalman import Kalman -from ._lae import LAE -from ._arma import ARMA -from ._lqcontrol import LQ, LQMarkov -from ._filter import hamilton_filter -from ._lqnash import nnash -from ._ivp import IVP -from ._lss import LinearStateSpace -from ._matrix_eqn import solve_discrete_lyapunov, solve_discrete_riccati -from ._quadsums import var_quadratic_sum, m_quadratic_sum -from ._rank_nullspace import rank_est, nullspace -from ._robustlq import RBLQ - -#-> Propose Delete From Top Level -# Promote to keep current examples working +#-Objects-# +from .compute_fp import compute_fixed_point +from .discrete_rv import DiscreteRV +from .dle import DLE +from .ecdf import ECDF +from .estspec import smooth, periodogram, ar_periodogram +# from .game_theory import #Place Holder if we wish to promote any general objects to the qe namespace. +from .graph_tools import DiGraph, random_tournament_graph +from .gridtools import ( + cartesian, mlinspace, cartesian_nearest_index, simplex_grid, simplex_index, + num_compositions +) +from .inequality import lorenz_curve, gini_coefficient, shorrocks_index, \ + rank_size +from .kalman import Kalman +from .lae import LAE +from .arma import ARMA +from .lqcontrol import LQ, LQMarkov +from .filter import hamilton_filter +from .lqnash import nnash +from .lss import LinearStateSpace +from .matrix_eqn import solve_discrete_lyapunov, solve_discrete_riccati +from .quadsums import var_quadratic_sum, m_quadratic_sum +#->Propose Delete From Top Level +#Promote to keep current examples working from .markov import MarkovChain, random_markov_chain, random_stochastic_matrix, \ - gth_solve, tauchen, rouwenhorst - - -from .util import searchsorted, fetch_nb_dependencies, tic, tac, toc -#<- - -# Imports that should be deprecated with markov package + gth_solve, tauchen, rouwenhorst +#Imports that Should be Deprecated with markov package from .markov import mc_compute_stationary, mc_sample_path - -# Imports that are deprecated and will be removed in further versions -from . import (ecdf, arma, compute_fp, discrete_rv, dle, estspec, filter, - graph_tools, gridtools, inequality, ivp, kalman, lae, - lqcontrol, lqnash, lss, matrix_eqn, quadsums, rank_nullspace, - robustlq) +#<- +from .rank_nullspace import rank_est, nullspace +from .robustlq import RBLQ +from .util import searchsorted, fetch_nb_dependencies, tic, tac, toc diff --git a/quantecon/_arma.py b/quantecon/_arma.py deleted file mode 100644 index be0f5699f..000000000 --- a/quantecon/_arma.py +++ /dev/null @@ -1,258 +0,0 @@ -""" -Provides functions for working with and visualizing scalar ARMA processes. - -TODO: 1. Fix warnings concerning casting complex variables back to floats - -""" -import numpy as np -from .util import check_random_state - - -class ARMA: - r""" - This class represents scalar ARMA(p, q) processes. - - If phi and theta are scalars, then the model is - understood to be - - .. math:: - - X_t = \phi X_{t-1} + \epsilon_t + \theta \epsilon_{t-1} - - where :math:`\epsilon_t` is a white noise process with standard - deviation :math:`\sigma`. If phi and theta are arrays or sequences, - then the interpretation is the ARMA(p, q) model - - .. math:: - - X_t = \phi_1 X_{t-1} + ... + \phi_p X_{t-p} + - - \epsilon_t + \theta_1 \epsilon_{t-1} + ... + - \theta_q \epsilon_{t-q} - - where - - * :math:`\phi = (\phi_1, \phi_2,..., \phi_p)` - * :math:`\theta = (\theta_1, \theta_2,..., \theta_q)` - * :math:`\sigma` is a scalar, the standard deviation of the - white noise - - Parameters - ---------- - phi : scalar or iterable or array_like(float) - Autocorrelation values for the autocorrelated variable. - See above for explanation. - theta : scalar or iterable or array_like(float) - Autocorrelation values for the white noise of the model. - See above for explanation - sigma : scalar(float) - The standard deviation of the white noise - - Attributes - ---------- - phi, theta, sigma : see Parmeters - ar_poly : array_like(float) - The polynomial form that is needed by scipy.signal to do the - processing we desire. Corresponds with the phi values - ma_poly : array_like(float) - The polynomial form that is needed by scipy.signal to do the - processing we desire. Corresponds with the theta values - - """ - def __init__(self, phi, theta=0, sigma=1): - self._phi, self._theta = phi, theta - self.sigma = sigma - self.set_params() - - def __repr__(self): - m = "ARMA(phi=%s, theta=%s, sigma=%s)" - return m % (self.phi, self.theta, self.sigma) - - def __str__(self): - m = "An ARMA({p}, {q}) process" - p = np.asarray(self.phi).size - q = np.asarray(self.theta).size - return m.format(p=p, q=q) - - # Special latex print method for working in notebook - def _repr_latex_(self): - m = r"$X_t = " - phi = np.atleast_1d(self.phi) - theta = np.atleast_1d(self.theta) - rhs = "" - for (tm, phi_p) in enumerate(phi): - # don't include terms if they are equal to zero - if abs(phi_p) > 1e-12: - rhs += r"%+g X_{t-%i}" % (phi_p, tm+1) - - if rhs[0] == "+": - rhs = rhs[1:] # remove initial `+` if phi_1 was positive - - rhs += r" + \epsilon_t" - - for (tm, th_q) in enumerate(theta): - # don't include terms if they are equal to zero - if abs(th_q) > 1e-12: - rhs += r"%+g \epsilon_{t-%i}" % (th_q, tm+1) - - return m + rhs + "$" - - @property - def phi(self): - return self._phi - - @phi.setter - def phi(self, new_value): - self._phi = new_value - self.set_params() - - @property - def theta(self): - return self._theta - - @theta.setter - def theta(self, new_value): - self._theta = new_value - self.set_params() - - def set_params(self): - r""" - Internally, scipy.signal works with systems of the form - - .. math:: - - ar_{poly}(L) X_t = ma_{poly}(L) \epsilon_t - - where L is the lag operator. To match this, we set - - .. math:: - - ar_{poly} = (1, -\phi_1, -\phi_2,..., -\phi_p) - - ma_{poly} = (1, \theta_1, \theta_2,..., \theta_q) - - In addition, ar_poly must be at least as long as ma_poly. - This can be achieved by padding it out with zeros when required. - - """ - # === set up ma_poly === # - ma_poly = np.asarray(self._theta) - self.ma_poly = np.insert(ma_poly, 0, 1) # The array (1, theta) - - # === set up ar_poly === # - if np.isscalar(self._phi): - ar_poly = np.array(-self._phi) - else: - ar_poly = -np.asarray(self._phi) - self.ar_poly = np.insert(ar_poly, 0, 1) # The array (1, -phi) - - # === pad ar_poly with zeros if required === # - if len(self.ar_poly) < len(self.ma_poly): - temp = np.zeros(len(self.ma_poly) - len(self.ar_poly)) - self.ar_poly = np.hstack((self.ar_poly, temp)) - - def impulse_response(self, impulse_length=30): - """ - Get the impulse response corresponding to our model. - - Returns - ------- - psi : array_like(float) - psi[j] is the response at lag j of the impulse response. - We take psi[0] as unity. - - """ - from scipy.signal import dimpulse - sys = self.ma_poly, self.ar_poly, 1 - times, psi = dimpulse(sys, n=impulse_length) - psi = psi[0].flatten() # Simplify return value into flat array - - return psi - - def spectral_density(self, two_pi=True, res=1200): - r""" - Compute the spectral density function. The spectral density is - the discrete time Fourier transform of the autocovariance - function. In particular, - - .. math:: - - f(w) = \sum_k \gamma(k) \exp(-ikw) - - where gamma is the autocovariance function and the sum is over - the set of all integers. - - Parameters - ---------- - two_pi : Boolean, optional - Compute the spectral density function over :math:`[0, \pi]` if - two_pi is False and :math:`[0, 2 \pi]` otherwise. Default value is - True - res : scalar or array_like(int), optional(default=1200) - If res is a scalar then the spectral density is computed at - `res` frequencies evenly spaced around the unit circle, but - if res is an array then the function computes the response - at the frequencies given by the array - - Returns - ------- - w : array_like(float) - The normalized frequencies at which h was computed, in - radians/sample - spect : array_like(float) - The frequency response - - """ - from scipy.signal import freqz - w, h = freqz(self.ma_poly, self.ar_poly, worN=res, whole=two_pi) - spect = h * np.conj(h) * self.sigma**2 - - return w, spect - - def autocovariance(self, num_autocov=16): - """ - Compute the autocovariance function from the ARMA parameters - over the integers range(num_autocov) using the spectral density - and the inverse Fourier transform. - - Parameters - ---------- - num_autocov : scalar(int), optional(default=16) - The number of autocovariances to calculate - - """ - spect = self.spectral_density()[1] - acov = np.fft.ifft(spect).real - - # num_autocov should be <= len(acov) / 2 - return acov[:num_autocov] - - def simulation(self, ts_length=90, random_state=None): - """ - Compute a simulated sample path assuming Gaussian shocks. - - Parameters - ---------- - ts_length : scalar(int), optional(default=90) - Number of periods to simulate for - - random_state : int or np.random.RandomState/Generator, optional - Random seed (integer) or np.random.RandomState or Generator - instance to set the initial state of the random number - generator for reproducibility. If None, a randomly - initialized RandomState is used. - - Returns - ------- - vals : array_like(float) - A simulation of the model that corresponds to this class - - """ - from scipy.signal import dlsim - random_state = check_random_state(random_state) - - sys = self.ma_poly, self.ar_poly, 1 - u = random_state.standard_normal((ts_length, 1)) * self.sigma - vals = dlsim(sys, u)[1] - - return vals.flatten() diff --git a/quantecon/_ce_util.py b/quantecon/_ce_util.py deleted file mode 100644 index 314e311b3..000000000 --- a/quantecon/_ce_util.py +++ /dev/null @@ -1,123 +0,0 @@ -""" -Utility functions used in CompEcon - -Based routines found in the CompEcon toolbox by Miranda and Fackler. - -References ----------- -Miranda, Mario J, and Paul L Fackler. Applied Computational Economics -and Finance, MIT Press, 2002. - -""" -from functools import reduce -import numpy as np - - -def ckron(*arrays): - """ - Repeatedly applies the np.kron function to an arbitrary number of - input arrays - - Parameters - ---------- - *arrays : tuple/list of np.ndarray - - Returns - ------- - out : np.ndarray - The result of repeated kronecker products. - - Notes - ----- - Based of original function `ckron` in CompEcon toolbox by Miranda - and Fackler. - - References - ---------- - Miranda, Mario J, and Paul L Fackler. Applied Computational - Economics and Finance, MIT Press, 2002. - - """ - return reduce(np.kron, arrays) - - -def gridmake(*arrays): - """ - Expands one or more vectors (or matrices) into a matrix where rows span the - cartesian product of combinations of the input arrays. Each column of the - input arrays will correspond to one column of the output matrix. - - Parameters - ---------- - *arrays : tuple/list of np.ndarray - Tuple/list of vectors to be expanded. - - Returns - ------- - out : np.ndarray - The cartesian product of combinations of the input arrays. - - Notes - ----- - Based of original function ``gridmake`` in CompEcon toolbox by - Miranda and Fackler - - References - ---------- - Miranda, Mario J, and Paul L Fackler. Applied Computational Economics - and Finance, MIT Press, 2002. - - """ - if all([i.ndim == 1 for i in arrays]): - d = len(arrays) - if d == 2: - out = _gridmake2(*arrays) - else: - out = _gridmake2(arrays[0], arrays[1]) - for arr in arrays[2:]: - out = _gridmake2(out, arr) - - return out - else: - raise NotImplementedError("Come back here") - - -def _gridmake2(x1, x2): - """ - Expands two vectors (or matrices) into a matrix where rows span the - cartesian product of combinations of the input arrays. Each column of the - input arrays will correspond to one column of the output matrix. - - Parameters - ---------- - x1 : np.ndarray - First vector to be expanded. - - x2 : np.ndarray - Second vector to be expanded. - - Returns - ------- - out : np.ndarray - The cartesian product of combinations of the input arrays. - - Notes - ----- - Based of original function ``gridmake2`` in CompEcon toolbox by - Miranda and Fackler. - - References - ---------- - Miranda, Mario J, and Paul L Fackler. Applied Computational Economics - and Finance, MIT Press, 2002. - - """ - if x1.ndim == 1 and x2.ndim == 1: - return np.column_stack([np.tile(x1, x2.shape[0]), - np.repeat(x2, x1.shape[0])]) - elif x1.ndim > 1 and x2.ndim == 1: - first = np.tile(x1, (x2.shape[0], 1)) - second = np.repeat(x2, x1.shape[0]) - return np.column_stack([first, second]) - else: - raise NotImplementedError("Come back here") diff --git a/quantecon/_compute_fp.py b/quantecon/_compute_fp.py deleted file mode 100644 index aec0727ec..000000000 --- a/quantecon/_compute_fp.py +++ /dev/null @@ -1,367 +0,0 @@ -""" -Compute an approximate fixed point of a given operator T, starting from -specified initial condition v. - -""" -import time -import warnings -import numpy as np -from numba import jit, generated_jit, types -from .game_theory.lemke_howson import _lemke_howson_tbl, _get_mixed_actions - - -def _print_after_skip(skip, it=None, dist=None, etime=None): - if it is None: - # print initial header - msg = "{i:<13}{d:<15}{t:<17}".format(i="Iteration", - d="Distance", - t="Elapsed (seconds)") - print(msg) - print("-" * len(msg)) - - return - - if it % skip == 0: - if etime is None: - print("After {it} iterations dist is {d}".format(it=it, d=dist)) - - else: - # leave 4 spaces between columns if we have %3.3e in d, t - msg = "{i:<13}{d:<15.3e}{t:<18.3e}" - print(msg.format(i=it, d=dist, t=etime)) - - return - - -_convergence_msg = 'Converged in {iterate} steps' -_non_convergence_msg = \ - 'max_iter attained before convergence in compute_fixed_point' - - -def _is_approx_fp(T, v, error_tol, *args, **kwargs): - error = np.max(np.abs(T(v, *args, **kwargs) - v)) - return error <= error_tol - - -def compute_fixed_point(T, v, error_tol=1e-3, max_iter=50, verbose=2, - print_skip=5, method='iteration', *args, **kwargs): - r""" - Computes and returns an approximate fixed point of the function `T`. - - The default method `'iteration'` simply iterates the function given - an initial condition `v` and returns :math:`T^k v` when the - condition :math:`\lVert T^k v - T^{k-1} v\rVert \leq - \mathrm{error\_tol}` is satisfied or the number of iterations - :math:`k` reaches `max_iter`. Provided that `T` is a contraction - mapping or similar, :math:`T^k v` will be an approximation to the - fixed point. - - The method `'imitation_game'` uses the "imitation game algorithm" - developed by McLennan and Tourky [1]_, which internally constructs - a sequence of two-player games called imitation games and utilizes - their Nash equilibria, computed by the Lemke-Howson algorithm - routine. It finds an approximate fixed point of `T`, a point - :math:`v^*` such that :math:`\lVert T(v) - v\rVert \leq - \mathrm{error\_tol}`, provided `T` is a function that satisfies the - assumptions of Brouwer's fixed point theorem, i.e., a continuous - function that maps a compact and convex set to itself. - - Parameters - ---------- - T : callable - A callable object (e.g., function) that acts on v - v : object - An object such that T(v) is defined; modified in place if - `method='iteration' and `v` is an array - error_tol : scalar(float), optional(default=1e-3) - Error tolerance - max_iter : scalar(int), optional(default=50) - Maximum number of iterations - verbose : scalar(int), optional(default=2) - Level of feedback (0 for no output, 1 for warnings only, 2 for - warning and residual error reports during iteration) - print_skip : scalar(int), optional(default=5) - How many iterations to apply between print messages (effective - only when `verbose=2`) - method : str, optional(default='iteration') - str in {'iteration', 'imitation_game'}. Method of computing - an approximate fixed point - args, kwargs : - Other arguments and keyword arguments that are passed directly - to the function T each time it is called - - Returns - ------- - v : object - The approximate fixed point - - References - ---------- - .. [1] A. McLennan and R. Tourky, "From Imitation Games to - Kakutani," 2006. - - """ - if max_iter < 1: - raise ValueError('max_iter must be a positive integer') - - if verbose not in (0, 1, 2): - raise ValueError('verbose should be 0, 1 or 2') - - if method not in ['iteration', 'imitation_game']: - raise ValueError('invalid method') - - if method == 'imitation_game': - is_approx_fp = \ - lambda v: _is_approx_fp(T, v, error_tol, *args, **kwargs) - v_star, converged, iterate = \ - _compute_fixed_point_ig(T, v, max_iter, verbose, print_skip, - is_approx_fp, *args, **kwargs) - return v_star - - # method == 'iteration' - iterate = 0 - - if verbose == 2: - start_time = time.time() - _print_after_skip(print_skip, it=None) - - while True: - new_v = T(v, *args, **kwargs) - iterate += 1 - error = np.max(np.abs(new_v - v)) - - try: - v[:] = new_v - except TypeError: - v = new_v - - if error <= error_tol or iterate >= max_iter: - break - - if verbose == 2: - etime = time.time() - start_time - _print_after_skip(print_skip, iterate, error, etime) - - if verbose == 2: - etime = time.time() - start_time - print_skip = 1 - _print_after_skip(print_skip, iterate, error, etime) - if verbose >= 1: - if error > error_tol: - warnings.warn(_non_convergence_msg, RuntimeWarning) - elif verbose == 2: - print(_convergence_msg.format(iterate=iterate)) - - return v - - -def _compute_fixed_point_ig(T, v, max_iter, verbose, print_skip, is_approx_fp, - *args, **kwargs): - """ - Implement the imitation game algorithm by McLennan and Tourky (2006) - for computing an approximate fixed point of `T`. - - Parameters - ---------- - is_approx_fp : callable - A callable with signature `is_approx_fp(v)` which determines - whether `v` is an approximate fixed point with a bool return - value (i.e., True or False) - - For the other parameters, see Parameters in compute_fixed_point. - - Returns - ------- - x_new : scalar(float) or ndarray(float) - Approximate fixed point. - - converged : bool - Whether the routine has converged. - - iterate : scalar(int) - Number of iterations. - - """ - if verbose == 2: - start_time = time.time() - _print_after_skip(print_skip, it=None) - - x_new = v - y_new = T(x_new, *args, **kwargs) - iterate = 1 - converged = is_approx_fp(x_new) - - if converged or iterate >= max_iter: - if verbose == 2: - error = np.max(np.abs(y_new - x_new)) - etime = time.time() - start_time - print_skip = 1 - _print_after_skip(print_skip, iterate, error, etime) - if verbose >= 1: - if not converged: - warnings.warn(_non_convergence_msg, RuntimeWarning) - elif verbose == 2: - print(_convergence_msg.format(iterate=iterate)) - return x_new, converged, iterate - - if verbose == 2: - error = np.max(np.abs(y_new - x_new)) - etime = time.time() - start_time - _print_after_skip(print_skip, iterate, error, etime) - - # Length of the arrays to store the computed sequences of x and y. - # If exceeded, reset to min(max_iter, buff_size*2). - buff_size = 2**8 - buff_size = min(max_iter, buff_size) - - shape = (buff_size,) + np.asarray(x_new).shape - X, Y = np.empty(shape), np.empty(shape) - X[0], Y[0] = x_new, y_new - x_new = Y[0] - - tableaux = tuple(np.empty((buff_size, buff_size*2+1)) for i in range(2)) - bases = tuple(np.empty(buff_size, dtype=int) for i in range(2)) - max_piv = 10**6 # Max number of pivoting steps in _lemke_howson_tbl - - while True: - y_new = T(x_new, *args, **kwargs) - iterate += 1 - converged = is_approx_fp(x_new) - - if converged or iterate >= max_iter: - break - - if verbose == 2: - error = np.max(np.abs(y_new - x_new)) - etime = time.time() - start_time - _print_after_skip(print_skip, iterate, error, etime) - - try: - X[iterate-1] = x_new - Y[iterate-1] = y_new - except IndexError: - buff_size = min(max_iter, buff_size*2) - shape = (buff_size,) + X.shape[1:] - X_tmp, Y_tmp = X, Y - X, Y = np.empty(shape), np.empty(shape) - X[:X_tmp.shape[0]], Y[:Y_tmp.shape[0]] = X_tmp, Y_tmp - X[iterate-1], Y[iterate-1] = x_new, y_new - - tableaux = tuple(np.empty((buff_size, buff_size*2+1)) - for i in range(2)) - bases = tuple(np.empty(buff_size, dtype=int) for i in range(2)) - - m = iterate - tableaux_curr = tuple(tableau[:m, :2*m+1] for tableau in tableaux) - bases_curr = tuple(basis[:m] for basis in bases) - _initialize_tableaux_ig(X[:m], Y[:m], tableaux_curr, bases_curr) - converged, num_iter = _lemke_howson_tbl( - tableaux_curr, bases_curr, init_pivot=m-1, max_iter=max_piv - ) - _, rho = _get_mixed_actions(tableaux_curr, bases_curr) - - if Y.ndim <= 2: - x_new = rho.dot(Y[:m]) - else: - shape_Y = Y.shape - Y_2d = Y.reshape(shape_Y[0], np.prod(shape_Y[1:])) - x_new = rho.dot(Y_2d[:m]).reshape(shape_Y[1:]) - - if verbose == 2: - error = np.max(np.abs(y_new - x_new)) - etime = time.time() - start_time - print_skip = 1 - _print_after_skip(print_skip, iterate, error, etime) - if verbose >= 1: - if not converged: - warnings.warn(_non_convergence_msg, RuntimeWarning) - elif verbose == 2: - print(_convergence_msg.format(iterate=iterate)) - - return x_new, converged, iterate - - -@jit(nopython=True) -def _initialize_tableaux_ig(X, Y, tableaux, bases): - """ - Given sequences `X` and `Y` of ndarrays, initialize the tableau and - basis arrays in place for the "geometric" imitation game as defined - in McLennan and Tourky (2006), to be passed to `_lemke_howson_tbl`. - - Parameters - ---------- - X, Y : ndarray(float) - Arrays of the same shape (m, n). - - tableaux : tuple(ndarray(float, ndim=2)) - Tuple of two arrays to be used to store the tableaux, of shape - (2m, 2m). Modified in place. - - bases : tuple(ndarray(int, ndim=1)) - Tuple of two arrays to be used to store the bases, of shape - (m,). Modified in place. - - Returns - ------- - tableaux : tuple(ndarray(float, ndim=2)) - View to `tableaux`. - - bases : tuple(ndarray(int, ndim=1)) - View to `bases`. - - """ - m = X.shape[0] - min_ = np.zeros(m) - - # Mover - for i in range(m): - for j in range(2*m): - if j == i or j == i + m: - tableaux[0][i, j] = 1 - else: - tableaux[0][i, j] = 0 - # Right hand side - tableaux[0][i, 2*m] = 1 - - # Imitator - for i in range(m): - # Slack variables - for j in range(m): - if j == i: - tableaux[1][i, j] = 1 - else: - tableaux[1][i, j] = 0 - # Payoff variables - for j in range(m): - d = X[i] - Y[j] - tableaux[1][i, m+j] = _square_sum(d) * (-1) - if tableaux[1][i, m+j] < min_[j]: - min_[j] = tableaux[1][i, m+j] - # Right hand side - tableaux[1][i, 2*m] = 1 - # Shift the payoff values - for i in range(m): - for j in range(m): - tableaux[1][i, m+j] -= min_[j] - tableaux[1][i, m+j] += 1 - - for pl, start in enumerate([m, 0]): - for i in range(m): - bases[pl][i] = start + i - - return tableaux, bases - - -@generated_jit(nopython=True, cache=True) -def _square_sum(a): - if isinstance(a, types.Number): - return lambda a: a**2 - elif isinstance(a, types.Array): - return _square_sum_array - - -def _square_sum_array(a): # pragma: no cover - sum_ = 0 - for x in a.flat: - sum_ += x**2 - return sum_ diff --git a/quantecon/_discrete_rv.py b/quantecon/_discrete_rv.py deleted file mode 100644 index 51dddf3dc..000000000 --- a/quantecon/_discrete_rv.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -Generates an array of draws from a discrete random variable with a -specified vector of probabilities. - -""" - -import numpy as np -from .util import check_random_state - - -class DiscreteRV: - """ - Generates an array of draws from a discrete random variable with - vector of probabilities given by q. - - Parameters - ---------- - q : array_like(float) - Nonnegative numbers that sum to 1. - - Attributes - ---------- - q : see Parameters. - Q : array_like(float) - The cumulative sum of q. - - """ - - def __init__(self, q): - self._q = np.asarray(q) - self.Q = np.cumsum(q) - - def __repr__(self): - return "DiscreteRV with {n} elements".format(n=self._q.size) - - def __str__(self): - return self.__repr__() - - @property - def q(self): - """ - Getter method for q. - - """ - return self._q - - @q.setter - def q(self, val): - """ - Setter method for q. - - """ - self._q = np.asarray(val) - self.Q = np.cumsum(val) - - def draw(self, k=1, random_state=None): - """ - Returns k draws from q. - - For each such draw, the value i is returned with probability - q[i]. - - Parameters - ---------- - k : scalar(int), optional - Number of draws to be returned - - random_state : int or np.random.RandomState/Generator, optional - Random seed (integer) or np.random.RandomState or Generator - instance to set the initial state of the random number - generator for reproducibility. If None, a randomly - initialized RandomState is used. - - Returns - ------- - array_like(int) - An array of k independent draws from q - - """ - random_state = check_random_state(random_state) - - return self.Q.searchsorted(random_state.uniform(0, 1, size=k), - side='right') diff --git a/quantecon/_dle.py b/quantecon/_dle.py deleted file mode 100644 index d16bfc61d..000000000 --- a/quantecon/_dle.py +++ /dev/null @@ -1,328 +0,0 @@ -""" -Provides a class called DLE to convert and solve dynamic linear economies -(as set out in Hansen & Sargent (2013)) as LQ problems. -""" - -import numpy as np -from ._lqcontrol import LQ -from ._matrix_eqn import solve_discrete_lyapunov -from ._rank_nullspace import nullspace - -class DLE(object): - r""" - This class is for analyzing dynamic linear economies, as set out in Hansen & Sargent (2013). - The planner's problem is to choose \{c_t, s_t, i_t, h_t, k_t, g_t\}_{t=0}^\infty to maximize - - \max -(1/2) \mathbb{E} \sum_{t=0}^{\infty} \beta^t [(s_t - b_t).(s_t-b_t) + g_t.g_t] - - subject to the linear constraints - - \Phi_c c_t + \Phi_g g_t + \Phi_i i_t = \Gamma k_{t-1} + d_t - k_t = \Delta_k k_{t-1} + \Theta_k i_t - h_t = \Delta_h h_{t-1} + \Theta_h c_t - s_t = \Lambda h_{t-1} + \Pi c_t - - and - - z_{t+1} = A_{22} z_t + C_2 w_{t+1} - b_t = U_b z_t - d_t = U_d z_t - - where h_{-1}, k_{-1}, and z_0 are given as initial conditions. - - Section 5.5 of HS2013 describes how to map these matrices into those of - a LQ problem. - - HS2013 sort the matrices defining the problem into three groups: - - Information: A_{22}, C_2, U_b , and U_d characterize the motion of information - sets and of taste and technology shocks - - Technology: \Phi_c, \Phi_g, \Phi_i, \Gamma, \Delta_k, and \Theta_k determine the - technology for producing consumption goods - - Preferences: \Delta_h, \Theta_h, \Lambda, and \Pi determine the technology for - producing consumption services from consumer goods. A scalar discount factor \beta - determines the preference ordering over consumption services. - - Parameters - ---------- - Information : tuple - Information is a tuple containing the matrices A_{22}, C_2, U_b, and U_d - Technology : tuple - Technology is a tuple containing the matrices \Phi_c, \Phi_g, \Phi_i, \Gamma, - \Delta_k, and \Theta_k - Preferences : tuple - Preferences is a tuple containing the scalar \beta and the - matrices \Lambda, \Pi, \Delta_h, and \Theta_h - - """ - - def __init__(self, information, technology, preferences): - - # === Unpack the tuples which define information, technology and preferences === # - self.a22, self.c2, self.ub, self.ud = information - self.phic, self.phig, self.phii, self.gamma, self.deltak, self.thetak = technology - self.beta, self.llambda, self.pih, self.deltah, self.thetah = preferences - - # === Computation of the dimension of the structural parameter matrices === # - self.nb, self.nh = self.llambda.shape - self.nd, self.nc = self.phic.shape - self.nz, self.nw = self.c2.shape - _, self.ng = self.phig.shape - self.nk, self.ni = self.thetak.shape - - # === Creation of various useful matrices === # - uc = np.hstack((np.eye(self.nc), np.zeros((self.nc, self.ng)))) - ug = np.hstack((np.zeros((self.ng, self.nc)), np.eye(self.ng))) - phiin = np.linalg.inv(np.hstack((self.phic, self.phig))) - phiinc = uc.dot(phiin) - b11 = - self.thetah.dot(phiinc).dot(self.phii) - a1 = self.thetah.dot(phiinc).dot(self.gamma) - a12 = np.vstack((self.thetah.dot(phiinc).dot( - self.ud), np.zeros((self.nk, self.nz)))) - - # === Creation of the A Matrix for the state transition of the LQ problem === # - - a11 = np.vstack((np.hstack((self.deltah, a1)), np.hstack( - (np.zeros((self.nk, self.nh)), self.deltak)))) - self.A = np.vstack((np.hstack((a11, a12)), np.hstack( - (np.zeros((self.nz, self.nk + self.nh)), self.a22)))) - - # === Creation of the B Matrix for the state transition of the LQ problem === # - - b1 = np.vstack((b11, self.thetak)) - self.B = np.vstack((b1, np.zeros((self.nz, self.ni)))) - - # === Creation of the C Matrix for the state transition of the LQ problem === # - - self.C = np.vstack((np.zeros((self.nk + self.nh, self.nw)), self.c2)) - - # === Define R,W and Q for the payoff function of the LQ problem === # - - self.H = np.hstack((self.llambda, self.pih.dot(uc).dot(phiin).dot(self.gamma), self.pih.dot( - uc).dot(phiin).dot(self.ud) - self.ub, -self.pih.dot(uc).dot(phiin).dot(self.phii))) - self.G = ug.dot(phiin).dot( - np.hstack((np.zeros((self.nd, self.nh)), self.gamma, self.ud, -self.phii))) - self.S = (self.G.T.dot(self.G) + self.H.T.dot(self.H)) / 2 - - self.nx = self.nh + self.nk + self.nz - self.n = self.ni + self.nh + self.nk + self.nz - - self.R = self.S[0:self.nx, 0:self.nx] - self.W = self.S[self.nx:self.n, 0:self.nx] - self.Q = self.S[self.nx:self.n, self.nx:self.n] - - # === Use quantecon's LQ code to solve our LQ problem === # - - lq = LQ(self.Q, self.R, self.A, self.B, - self.C, N=self.W, beta=self.beta) - - self.P, self.F, self.d = lq.stationary_values() - - # === Construct output matrices for our economy using the solution to the LQ problem === # - - self.A0 = self.A - self.B.dot(self.F) - - self.Sh = self.A0[0:self.nh, 0:self.nx] - self.Sk = self.A0[self.nh:self.nh + self.nk, 0:self.nx] - self.Sk1 = np.hstack((np.zeros((self.nk, self.nh)), np.eye( - self.nk), np.zeros((self.nk, self.nz)))) - self.Si = -self.F - self.Sd = np.hstack((np.zeros((self.nd, self.nh + self.nk)), self.ud)) - self.Sb = np.hstack((np.zeros((self.nb, self.nh + self.nk)), self.ub)) - self.Sc = uc.dot(phiin).dot(-self.phii.dot(self.Si) + - self.gamma.dot(self.Sk1) + self.Sd) - self.Sg = ug.dot(phiin).dot(-self.phii.dot(self.Si) + - self.gamma.dot(self.Sk1) + self.Sd) - self.Ss = self.llambda.dot(np.hstack((np.eye(self.nh), np.zeros( - (self.nh, self.nk + self.nz))))) + self.pih.dot(self.Sc) - - # === Calculate eigenvalues of A0 === # - self.A110 = self.A0[0:self.nh + self.nk, 0:self.nh + self.nk] - self.endo = np.linalg.eigvals(self.A110) - self.exo = np.linalg.eigvals(self.a22) - - # === Construct matrices for Lagrange Multipliers === # - - self.Mk = -2 * self.beta.item() * (np.hstack((np.zeros((self.nk, self.nh)), np.eye( - self.nk), np.zeros((self.nk, self.nz))))).dot(self.P).dot(self.A0) - self.Mh = -2 * self.beta.item() * (np.hstack((np.eye(self.nh), np.zeros( - (self.nh, self.nk)), np.zeros((self.nh, self.nz))))).dot(self.P).dot(self.A0) - self.Ms = -(self.Sb - self.Ss) - self.Md = -(np.linalg.inv(np.vstack((self.phic.T, self.phig.T))).dot( - np.vstack((self.thetah.T.dot(self.Mh) + self.pih.T.dot(self.Ms), -self.Sg)))) - self.Mc = -(self.thetah.T.dot(self.Mh) + self.pih.T.dot(self.Ms)) - self.Mi = -(self.thetak.T.dot(self.Mk)) - - def compute_steadystate(self, nnc=2): - """ - Computes the non-stochastic steady-state of the economy. - - Parameters - ---------- - nnc : array_like(float) - nnc is the location of the constant in the state vector x_t - - """ - zx = np.eye(self.A0.shape[0])-self.A0 - self.zz = nullspace(zx) - self.zz /= self.zz[nnc] - self.css = self.Sc.dot(self.zz) - self.sss = self.Ss.dot(self.zz) - self.iss = self.Si.dot(self.zz) - self.dss = self.Sd.dot(self.zz) - self.bss = self.Sb.dot(self.zz) - self.kss = self.Sk.dot(self.zz) - self.hss = self.Sh.dot(self.zz) - - def compute_sequence(self, x0, ts_length=None, Pay=None): - """ - Simulate quantities and prices for the economy - - Parameters - ---------- - x0 : array_like(float) - The initial state - - ts_length : scalar(int) - Length of the simulation - - Pay : array_like(float) - Vector to price an asset whose payout is Pay*xt - - """ - lq = LQ(self.Q, self.R, self.A, self.B, - self.C, N=self.W, beta=self.beta) - xp, up, wp = lq.compute_sequence(x0, ts_length) - self.h = self.Sh.dot(xp) - self.k = self.Sk.dot(xp) - self.i = self.Si.dot(xp) - self.b = self.Sb.dot(xp) - self.d = self.Sd.dot(xp) - self.c = self.Sc.dot(xp) - self.g = self.Sg.dot(xp) - self.s = self.Ss.dot(xp) - - # === Value of J-period risk-free bonds === # - # === See p.145: Equation (7.11.2) === # - e1 = np.zeros((1, self.nc)) - e1[0, 0] = 1 - self.R1_Price = np.empty((ts_length + 1, 1)) - self.R2_Price = np.empty((ts_length + 1, 1)) - self.R5_Price = np.empty((ts_length + 1, 1)) - for i in range(ts_length + 1): - self.R1_Price[i, 0] = self.beta * e1.dot(self.Mc).dot(np.linalg.matrix_power( - self.A0, 1)).dot(xp[:, i]) / e1.dot(self.Mc).dot(xp[:, i]) - self.R2_Price[i, 0] = self.beta**2 * e1.dot(self.Mc).dot( - np.linalg.matrix_power(self.A0, 2)).dot(xp[:, i]) / e1.dot(self.Mc).dot(xp[:, i]) - self.R5_Price[i, 0] = self.beta**5 * e1.dot(self.Mc).dot( - np.linalg.matrix_power(self.A0, 5)).dot(xp[:, i]) / e1.dot(self.Mc).dot(xp[:, i]) - - # === Gross rates of return on 1-period risk-free bonds === # - self.R1_Gross = 1 / self.R1_Price - - # === Net rates of return on J-period risk-free bonds === # - # === See p.148: log of gross rate of return, divided by j === # - self.R1_Net = np.log(1 / self.R1_Price) / 1 - self.R2_Net = np.log(1 / self.R2_Price) / 2 - self.R5_Net = np.log(1 / self.R5_Price) / 5 - - # === Value of asset whose payout vector is Pay*xt === # - # See p.145: Equation (7.11.1) - if isinstance(Pay, np.ndarray) == True: - self.Za = Pay.T.dot(self.Mc) - self.Q = solve_discrete_lyapunov( - self.A0.T * self.beta**0.5, self.Za) - self.q = self.beta / (1 - self.beta) * \ - np.trace(self.C.T.dot(self.Q).dot(self.C)) - self.Pay_Price = np.empty((ts_length + 1, 1)) - self.Pay_Gross = np.empty((ts_length + 1, 1)) - self.Pay_Gross[0, 0] = np.nan - for i in range(ts_length + 1): - self.Pay_Price[i, 0] = (xp[:, i].T.dot(self.Q).dot( - xp[:, i]) + self.q) / e1.dot(self.Mc).dot(xp[:, i]) - for i in range(ts_length): - self.Pay_Gross[i + 1, 0] = self.Pay_Price[i + 1, - 0] / (self.Pay_Price[i, 0] - Pay.dot(xp[:, i])) - return - - def irf(self, ts_length=100, shock=None): - """ - Create Impulse Response Functions - - Parameters - ---------- - - ts_length : scalar(int) - Number of periods to calculate IRF - - Shock : array_like(float) - Vector of shocks to calculate IRF to. Default is first element of w - - """ - - if type(shock) != np.ndarray: - # Default is to select first element of w - shock = np.vstack((np.ones((1, 1)), np.zeros((self.nw - 1, 1)))) - - self.c_irf = np.empty((ts_length, self.nc)) - self.s_irf = np.empty((ts_length, self.nb)) - self.i_irf = np.empty((ts_length, self.ni)) - self.k_irf = np.empty((ts_length, self.nk)) - self.h_irf = np.empty((ts_length, self.nh)) - self.g_irf = np.empty((ts_length, self.ng)) - self.d_irf = np.empty((ts_length, self.nd)) - self.b_irf = np.empty((ts_length, self.nb)) - - for i in range(ts_length): - self.c_irf[i, :] = self.Sc.dot( - np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T - self.s_irf[i, :] = self.Ss.dot( - np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T - self.i_irf[i, :] = self.Si.dot( - np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T - self.k_irf[i, :] = self.Sk.dot( - np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T - self.h_irf[i, :] = self.Sh.dot( - np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T - self.g_irf[i, :] = self.Sg.dot( - np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T - self.d_irf[i, :] = self.Sd.dot( - np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T - self.b_irf[i, :] = self.Sb.dot( - np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T - - return - - def canonical(self): - """ - Compute canonical preference representation - Uses auxiliary problem of 9.4.2, with the preference shock process reintroduced - Calculates pihat, llambdahat and ubhat for the equivalent canonical household technology - """ - Ac1 = np.hstack((self.deltah, np.zeros((self.nh, self.nz)))) - Ac2 = np.hstack((np.zeros((self.nz, self.nh)), self.a22)) - Ac = np.vstack((Ac1, Ac2)) - Bc = np.vstack((self.thetah, np.zeros((self.nz, self.nc)))) - Rc1 = np.hstack((self.llambda.T.dot(self.llambda), - - self.llambda.T.dot(self.ub))) - Rc2 = np.hstack((-self.ub.T.dot(self.llambda), self.ub.T.dot(self.ub))) - Rc = np.vstack((Rc1, Rc2)) - Qc = self.pih.T.dot(self.pih) - Nc = np.hstack( - (self.pih.T.dot(self.llambda), -self.pih.T.dot(self.ub))) - - lq_aux = LQ(Qc, Rc, Ac, Bc, N=Nc, beta=self.beta) - - P1, F1, d1 = lq_aux.stationary_values() - - self.F_b = F1[:, 0:self.nh] - self.F_f = F1[:, self.nh:] - - self.pihat = np.linalg.cholesky(self.pih.T.dot( - self.pih) + self.beta.dot(self.thetah.T).dot(P1[0:self.nh, 0:self.nh]).dot(self.thetah)).T - self.llambdahat = self.pihat.dot(self.F_b) - self.ubhat = - self.pihat.dot(self.F_f) - - return diff --git a/quantecon/_ecdf.py b/quantecon/_ecdf.py deleted file mode 100644 index a42604694..000000000 --- a/quantecon/_ecdf.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -Implements the empirical cumulative distribution function given an array -of observations. - -""" - -import numpy as np - - -class ECDF: - """ - One-dimensional empirical distribution function given a vector of - observations. - - Parameters - ---------- - observations : array_like - An array of observations - - Attributes - ---------- - observations : see Parameters - - """ - - def __init__(self, observations): - self.observations = np.asarray(observations) - - def __repr__(self): - return self.__str__() - - def __str__(self): - m = "Empirical CDF:\n - number of observations: {n}" - return m.format(n=self.observations.size) - - def __call__(self, x): - """ - Evaluates the ecdf at x - - Parameters - ---------- - x : scalar(float) - The x at which the ecdf is evaluated - - Returns - ------- - scalar(float) - Fraction of the sample less than x - - """ - def f(a): - return np.mean(self.observations <= a) - vf = np.frompyfunc(f, 1, 1) - return vf(x).astype(float) diff --git a/quantecon/_estspec.py b/quantecon/_estspec.py deleted file mode 100644 index 662dfab8a..000000000 --- a/quantecon/_estspec.py +++ /dev/null @@ -1,152 +0,0 @@ -""" -Functions for working with periodograms of scalar data. - -""" -import numpy as np -from numpy.fft import fft - - -def smooth(x, window_len=7, window='hanning'): - """ - Smooth the data in x using convolution with a window of requested - size and type. - - Parameters - ---------- - x : array_like(float) - A flat NumPy array containing the data to smooth - window_len : scalar(int), optional - An odd integer giving the length of the window. Defaults to 7. - window : string - A string giving the window type. Possible values are 'flat', - 'hanning', 'hamming', 'bartlett' or 'blackman' - - Returns - ------- - array_like(float) - The smoothed values - - Notes - ----- - Application of the smoothing window at the top and bottom of x is - done by reflecting x around these points to extend it sufficiently - in each direction. - - """ - if len(x) < window_len: - raise ValueError("Input vector length must be >= window length.") - - if window_len < 3: - raise ValueError("Window length must be at least 3.") - - if not window_len % 2: # window_len is even - window_len += 1 - print("Window length reset to {}".format(window_len)) - - windows = {'hanning': np.hanning, - 'hamming': np.hamming, - 'bartlett': np.bartlett, - 'blackman': np.blackman, - 'flat': np.ones # moving average - } - - # === Reflect x around x[0] and x[-1] prior to convolution === # - k = int(window_len / 2) - xb = x[:k] # First k elements - xt = x[-k:] # Last k elements - s = np.concatenate((xb[::-1], x, xt[::-1])) - - # === Select window values === # - if window in windows.keys(): - w = windows[window](window_len) - else: - msg = "Unrecognized window type '{}'".format(window) - print(msg + " Defaulting to hanning") - w = windows['hanning'](window_len) - - return np.convolve(w / w.sum(), s, mode='valid') - - -def periodogram(x, window=None, window_len=7): - r""" - Computes the periodogram - - .. math:: - - I(w) = \frac{1}{n} \Big[ \sum_{t=0}^{n-1} x_t e^{itw} \Big] ^2 - - at the Fourier frequencies :math:`w_j := \frac{2 \pi j}{n}`, - :math:`j = 0, \dots, n - 1`, using the fast Fourier transform. Only the - frequencies :math:`w_j` in :math:`[0, \pi]` and corresponding values - :math:`I(w_j)` are returned. If a window type is given then smoothing - is performed. - - Parameters - ---------- - x : array_like(float) - A flat NumPy array containing the data to smooth - window_len : scalar(int), optional(default=7) - An odd integer giving the length of the window. Defaults to 7. - window : string - A string giving the window type. Possible values are 'flat', - 'hanning', 'hamming', 'bartlett' or 'blackman' - - Returns - ------- - w : array_like(float) - Fourier frequencies at which periodogram is evaluated - I_w : array_like(float) - Values of periodogram at the Fourier frequencies - - """ - n = len(x) - I_w = np.abs(fft(x))**2 / n - w = 2 * np.pi * np.arange(n) / n # Fourier frequencies - w, I_w = w[:int(n/2)+1], I_w[:int(n/2)+1] # Take only values on [0, pi] - if window: - I_w = smooth(I_w, window_len=window_len, window=window) - return w, I_w - - -def ar_periodogram(x, window='hanning', window_len=7): - """ - Compute periodogram from data x, using prewhitening, smoothing and - recoloring. The data is fitted to an AR(1) model for prewhitening, - and the residuals are used to compute a first-pass periodogram with - smoothing. The fitted coefficients are then used for recoloring. - - Parameters - ---------- - x : array_like(float) - A flat NumPy array containing the data to smooth - window_len : scalar(int), optional - An odd integer giving the length of the window. Defaults to 7. - window : string - A string giving the window type. Possible values are 'flat', - 'hanning', 'hamming', 'bartlett' or 'blackman' - - Returns - ------- - w : array_like(float) - Fourier frequencies at which periodogram is evaluated - I_w : array_like(float) - Values of periodogram at the Fourier frequencies - - """ - # === run regression === # - x_lag = x[:-1] # lagged x - X = np.array([np.ones(len(x_lag)), x_lag]).T # add constant - - y = np.array(x[1:]) # current x - - beta_hat = np.linalg.solve(X.T @ X, X.T @ y) # solve for beta hat - e_hat = y - X @ beta_hat # compute residuals - phi = beta_hat[1] # pull out phi parameter - - # === compute periodogram on residuals === # - w, I_w = periodogram(e_hat, window=window, window_len=window_len) - - # === recolor and return === # - I_w = I_w / np.abs(1 - phi * np.exp(1j * w))**2 - - return w, I_w diff --git a/quantecon/_filter.py b/quantecon/_filter.py deleted file mode 100644 index 44d45f9a9..000000000 --- a/quantecon/_filter.py +++ /dev/null @@ -1,58 +0,0 @@ -""" - -function for filtering - -""" -import numpy as np - - -def hamilton_filter(data, h, p=None): - r""" - This function applies "Hamilton filter" to the data - - http://econweb.ucsd.edu/~jhamilto/hp.pdf - - Parameters - ---------- - data : array or dataframe - h : integer - Time horizon that we are likely to predict incorrectly. - Original paper recommends 2 for annual data, 8 for quarterly data, - 24 for monthly data. - p : integer (optional) - If supplied, it is p in the paper. Number of lags in regression. - If not supplied, random walk process is assumed. - - Returns - ------- - cycle : array of cyclical component - trend : trend component - - Notes - ----- - For seasonal data, it's desirable for p and h to be integer multiples of - the number of obsevations in a year. E.g. for quarterly data, h = 8 and p = - 4 are recommended. - - """ - # transform data to array - y = np.asarray(data, float) - # sample size - T = len(y) - - if p is not None: # if p is supplied - # construct X matrix of lags - X = np.ones((T-p-h+1, p+1)) - for j in range(1, p+1): - X[:, j] = y[p-j:T-h-j+1:1] - - # do OLS regression - b = np.linalg.solve(X.transpose()@X, X.transpose()@y[p+h-1:T]) - # trend component (`nan` for the first p+h-1 period) - trend = np.append(np.zeros(p+h-1)+np.nan, X@b) - # cyclical component - cycle = y - trend - else: # if p is not supplied (random walk) - cycle = np.append(np.zeros(h)+np.nan, y[h:T] - y[0:T-h]) - trend = y - cycle - return cycle, trend diff --git a/quantecon/_graph_tools.py b/quantecon/_graph_tools.py deleted file mode 100644 index 3f4dca799..000000000 --- a/quantecon/_graph_tools.py +++ /dev/null @@ -1,439 +0,0 @@ -""" -Tools for dealing with a directed graph. - -""" -import numpy as np -from scipy import sparse -from scipy.sparse import csgraph -from math import gcd -from numba import jit - -from .util import check_random_state - - -# Decorator for *_components properties -def annotate_nodes(func): - def new_func(self): - list_of_components = func(self) - if self.node_labels is not None: - return [self.node_labels[c] for c in list_of_components] - return list_of_components - return new_func - - -class DiGraph: - r""" - Class for a directed graph. It stores useful information about the - graph structure such as strong connectivity [1]_ and periodicity - [2]_. - - Parameters - ---------- - adj_matrix : array_like(ndim=2) - Adjacency matrix representing a directed graph. Must be of shape - n x n. - - weighted : bool, optional(default=False) - Whether to treat `adj_matrix` as a weighted adjacency matrix. - - node_labels : array_like(default=None) - Array_like of length n containing the labels associated with the - nodes, which must be homogeneous in type. If None, the labels - default to integers 0 through n-1. - - Attributes - ---------- - csgraph : scipy.sparse.csr_matrix - Compressed sparse representation of the digraph. - - is_strongly_connected : bool - Indicate whether the digraph is strongly connected. - - num_strongly_connected_components : int - The number of the strongly connected components. - - strongly_connected_components_indices : list(ndarray(int)) - List of numpy arrays containing the indices of the strongly - connected components. - - strongly_connected_components : list(ndarray) - List of numpy arrays containing the strongly connected - components, where the nodes are annotated with their labels (if - `node_labels` is not None). - - num_sink_strongly_connected_components : int - The number of the sink strongly connected components. - - sink_strongly_connected_components_indices : list(ndarray(int)) - List of numpy arrays containing the indices of the sink strongly - connected components. - - sink_strongly_connected_components : list(ndarray) - List of numpy arrays containing the sink strongly connected - components, where the nodes are annotated with their labels (if - `node_labels` is not None). - - is_aperiodic : bool - Indicate whether the digraph is aperiodic. - - period : int - The period of the digraph. Defined only for a strongly connected - digraph. - - cyclic_components_indices : list(ndarray(int)) - List of numpy arrays containing the indices of the cyclic - components. - - cyclic_components : list(ndarray) - List of numpy arrays containing the cyclic components, where the - nodes are annotated with their labels (if `node_labels` is not - None). - - References - ---------- - .. [1] `Strongly connected component - `_, - Wikipedia. - - .. [2] `Aperiodic graph - `_, Wikipedia. - - """ - - def __init__(self, adj_matrix, weighted=False, node_labels=None): - if weighted: - dtype = None - else: - dtype = bool - self.csgraph = sparse.csr_matrix(adj_matrix, dtype=dtype) - - m, n = self.csgraph.shape - if n != m: - raise ValueError('input matrix must be square') - - self.n = n # Number of nodes - - # Call the setter method - self.node_labels = node_labels - - self._num_scc = None - self._scc_proj = None - self._sink_scc_labels = None - - self._period = None - - def __repr__(self): - return self.__str__() - - def __str__(self): - return "Directed Graph:\n - n(number of nodes): {n}".format(n=self.n) - - @property - def node_labels(self): - return self._node_labels - - @node_labels.setter - def node_labels(self, values): - if values is None: - self._node_labels = None - else: - values = np.asarray(values) - if (values.ndim < 1) or (values.shape[0] != self.n): - raise ValueError( - 'node_labels must be an array_like of length n' - ) - if np.issubdtype(values.dtype, np.object_): - raise ValueError( - 'data in node_labels must be homogeneous in type' - ) - self._node_labels = values - - def _find_scc(self): - """ - Set ``self._num_scc`` and ``self._scc_proj`` - by calling ``scipy.sparse.csgraph.connected_components``: - * docs.scipy.org/doc/scipy/reference/sparse.csgraph.html - * github.com/scipy/scipy/blob/master/scipy/sparse/csgraph/_traversal.pyx - - ``self._scc_proj`` is a list of length `n` that assigns to each node - the label of the strongly connected component to which it belongs. - - """ - # Find the strongly connected components - self._num_scc, self._scc_proj = \ - csgraph.connected_components(self.csgraph, connection='strong') - - @property - def num_strongly_connected_components(self): - if self._num_scc is None: - self._find_scc() - return self._num_scc - - @property - def scc_proj(self): - if self._scc_proj is None: - self._find_scc() - return self._scc_proj - - @property - def is_strongly_connected(self): - return (self.num_strongly_connected_components == 1) - - def _condensation_lil(self): - """ - Return the sparse matrix representation of the condensation digraph - in lil format. - - """ - condensation_lil = sparse.lil_matrix( - (self.num_strongly_connected_components, - self.num_strongly_connected_components), dtype=bool - ) - - scc_proj = self.scc_proj - for node_from, node_to in _csr_matrix_indices(self.csgraph): - scc_from, scc_to = scc_proj[node_from], scc_proj[node_to] - if scc_from != scc_to: - condensation_lil[scc_from, scc_to] = True - - return condensation_lil - - def _find_sink_scc(self): - """ - Set self._sink_scc_labels, which is a list containing the labels of - the strongly connected components. - - """ - condensation_lil = self._condensation_lil() - - # A sink SCC is a SCC such that none of its members is strongly - # connected to nodes in other SCCs - # Those k's such that graph_condensed_lil.rows[k] == [] - self._sink_scc_labels = \ - np.where(np.logical_not(condensation_lil.rows))[0] - - @property - def sink_scc_labels(self): - if self._sink_scc_labels is None: - self._find_sink_scc() - return self._sink_scc_labels - - @property - def num_sink_strongly_connected_components(self): - return len(self.sink_scc_labels) - - @property - def strongly_connected_components_indices(self): - if self.is_strongly_connected: - return [np.arange(self.n)] - else: - return [np.where(self.scc_proj == k)[0] - for k in range(self.num_strongly_connected_components)] - - @property - @annotate_nodes - def strongly_connected_components(self): - return self.strongly_connected_components_indices - - @property - def sink_strongly_connected_components_indices(self): - if self.is_strongly_connected: - return [np.arange(self.n)] - else: - return [np.where(self.scc_proj == k)[0] - for k in self.sink_scc_labels.tolist()] - - @property - @annotate_nodes - def sink_strongly_connected_components(self): - return self.sink_strongly_connected_components_indices - - def _compute_period(self): - """ - Set ``self._period`` and ``self._cyclic_components_proj``. - - Use the algorithm described in: - J. P. Jarvis and D. R. Shier, - "Graph-Theoretic Analysis of Finite Markov Chains," 1996. - - """ - # Degenerate graph with a single node (which is strongly connected) - # csgraph.reconstruct_path would raise an exception - # github.com/scipy/scipy/issues/4018 - if self.n == 1: - if self.csgraph[0, 0] == 0: # No edge: "trivial graph" - self._period = 1 # Any universally accepted definition? - self._cyclic_components_proj = np.zeros(self.n, dtype=int) - return None - else: # Self loop - self._period = 1 - self._cyclic_components_proj = np.zeros(self.n, dtype=int) - return None - - if not self.is_strongly_connected: - raise NotImplementedError( - 'Not defined for a non strongly-connected digraph' - ) - - if np.any(self.csgraph.diagonal() > 0): - self._period = 1 - self._cyclic_components_proj = np.zeros(self.n, dtype=int) - return None - - # Construct a breadth-first search tree rooted at 0 - node_order, predecessors = \ - csgraph.breadth_first_order(self.csgraph, i_start=0) - bfs_tree_csr = \ - csgraph.reconstruct_path(self.csgraph, predecessors) - - # Edges not belonging to tree_csr - non_bfs_tree_csr = self.csgraph - bfs_tree_csr - non_bfs_tree_csr.eliminate_zeros() - - # Distance to 0 - level = np.zeros(self.n, dtype=int) - for i in range(1, self.n): - level[node_order[i]] = level[predecessors[node_order[i]]] + 1 - - # Determine the period - d = 0 - for node_from, node_to in _csr_matrix_indices(non_bfs_tree_csr): - value = level[node_from] - level[node_to] + 1 - d = gcd(d, value) - if d == 1: - self._period = 1 - self._cyclic_components_proj = np.zeros(self.n, dtype=int) - return None - - self._period = d - self._cyclic_components_proj = level % d - - @property - def period(self): - if self._period is None: - self._compute_period() - return self._period - - @property - def is_aperiodic(self): - return (self.period == 1) - - @property - def cyclic_components_indices(self): - if self.is_aperiodic: - return [np.arange(self.n)] - else: - return [np.where(self._cyclic_components_proj == k)[0] - for k in range(self.period)] - - @property - @annotate_nodes - def cyclic_components(self,): - return self.cyclic_components_indices - - def subgraph(self, nodes): - """ - Return the subgraph consisting of the given nodes and edges - between thses nodes. - - Parameters - ---------- - nodes : array_like(int, ndim=1) - Array of node indices. - - Returns - ------- - DiGraph - A DiGraph representing the subgraph. - - """ - adj_matrix = self.csgraph[np.ix_(nodes, nodes)] - - weighted = True # To copy the dtype - - if self.node_labels is not None: - node_labels = self.node_labels[nodes] - else: - node_labels = None - - return DiGraph(adj_matrix, weighted=weighted, node_labels=node_labels) - - -def _csr_matrix_indices(S): - """ - Generate the indices of nonzero entries of a csr_matrix S - - """ - m, n = S.shape - - for i in range(m): - for j in range(S.indptr[i], S.indptr[i+1]): - row_index, col_index = i, S.indices[j] - yield row_index, col_index - - -def random_tournament_graph(n, random_state=None): - """ - Return a random tournament graph [1]_ with n nodes. - - Parameters - ---------- - n : scalar(int) - Number of nodes. - - random_state : int or np.random.RandomState/Generator, optional - Random seed (integer) or np.random.RandomState or Generator - instance to set the initial state of the random number generator - for reproducibility. If None, a randomly initialized RandomState - is used. - - Returns - ------- - DiGraph - A DiGraph representing the tournament graph. - - References - ---------- - .. [1] `Tournament (graph theory) - `_, - Wikipedia. - - """ - random_state = check_random_state(random_state) - num_edges = n * (n-1) // 2 - r = random_state.random(num_edges) - row = np.empty(num_edges, dtype=int) - col = np.empty(num_edges, dtype=int) - _populate_random_tournament_row_col(n, r, row, col) - data = np.ones(num_edges, dtype=bool) - adj_matrix = sparse.coo_matrix((data, (row, col)), shape=(n, n)) - return DiGraph(adj_matrix) - - -@jit(nopython=True, cache=True) -def _populate_random_tournament_row_col(n, r, row, col): - """ - Populate ndarrays `row` and `col` with directed edge indices - determined by random numbers in `r` for a tournament graph with n - nodes, which has num_edges = n * (n-1) // 2 edges. - - Parameters - ---------- - n : scalar(int) - Number of nodes. - - r : ndarray(float, ndim=1) - ndarray of length num_edges containing random numbers in [0, 1). - - row, col : ndarray(int, ndim=1) - ndarrays of length num_edges to be modified in place. - - """ - k = 0 - for i in range(n): - for j in range(i+1, n): - if r[k] < 0.5: - row[k], col[k] = i, j - else: - row[k], col[k] = j, i - k += 1 diff --git a/quantecon/_gridtools.py b/quantecon/_gridtools.py deleted file mode 100644 index 8915c3451..000000000 --- a/quantecon/_gridtools.py +++ /dev/null @@ -1,435 +0,0 @@ -""" -Implements cartesian products and regular cartesian grids, and provides -a function that constructs a grid for a simplex as well as one that -determines the index of a point in the simplex. - -""" -import numpy as np -import scipy.special -from numba import jit, njit -from .util.numba import comb_jit - - -def cartesian(nodes, order='C'): - ''' - Cartesian product of a list of arrays - - Parameters - ---------- - nodes : list(array_like(ndim=1)) - - order : str, optional(default='C') - ('C' or 'F') order in which the product is enumerated - - Returns - ------- - out : ndarray(ndim=2) - each line corresponds to one point of the product space - ''' - - nodes = [np.asarray(e) for e in nodes] - shapes = [e.shape[0] for e in nodes] - - dtype = np.result_type(*nodes) - - n = len(nodes) - l = np.prod(shapes) - out = np.zeros((l, n), dtype=dtype) - - if order == 'C': - repetitions = np.cumprod([1] + shapes[:-1]) - else: - shapes.reverse() - sh = [1] + shapes[:-1] - repetitions = np.cumprod(sh) - repetitions = repetitions.tolist() - repetitions.reverse() - - for i in range(n): - _repeat_1d(nodes[i], repetitions[i], out[:, i]) - - return out - - -def mlinspace(a, b, nums, order='C'): - ''' - Constructs a regular cartesian grid - - Parameters - ---------- - a : array_like(ndim=1) - lower bounds in each dimension - - b : array_like(ndim=1) - upper bounds in each dimension - - nums : array_like(ndim=1) - number of nodes along each dimension - - order : str, optional(default='C') - ('C' or 'F') order in which the product is enumerated - - Returns - ------- - out : ndarray(ndim=2) - each line corresponds to one point of the product space - ''' - - a = np.asarray(a, dtype='float64') - b = np.asarray(b, dtype='float64') - nums = np.asarray(nums, dtype='int64') - nodes = [np.linspace(a[i], b[i], nums[i]) for i in range(len(nums))] - - return cartesian(nodes, order=order) - - -@njit -def _repeat_1d(x, K, out): - ''' - Repeats each element of a vector many times and repeats the whole - result many times - - Parameters - ---------- - x : ndarray(ndim=1) - vector to be repeated - - K : scalar(int) - number of times each element of x is repeated (inner iterations) - - out : ndarray(ndim=1) - placeholder for the result - - Returns - ------- - None - ''' - - N = x.shape[0] - L = out.shape[0] // (K*N) # number of outer iterations - # K # number of inner iterations - - # the result out should enumerate in C-order the elements - # of a 3-dimensional array T of dimensions (K,N,L) - # such that for all k,n,l, we have T[k,n,l] == x[n] - - for n in range(N): - val = x[n] - for k in range(K): - for l in range(L): - ind = k*N*L + n*L + l - out[ind] = val - - -def cartesian_nearest_index(x, nodes, order='C'): - """ - Return the index of the point closest to `x` within the cartesian - product generated by `nodes`. Each array in `nodes` must be sorted - in ascending order. - - Parameters - ---------- - x : array_like(ndim=1 or 2) - Point(s) to search the closest point(s) for. - - nodes : array_like(array_like(ndim=1)) - Array of sorted arrays. - - order : str, optional(default='C') - ('C' or 'F') order in which the product is enumerated. - - Returns - ------- - scalar(int) or ndarray(int, ndim=1) - Index (indices) of the closest point(s) to `x`. - - Examples - -------- - >>> nodes = (np.arange(3), np.arange(2)) - >>> prod = qe.cartesian(nodes) - >>> print(prod) - [[0 0] - [0 1] - [1 0] - [1 1] - [2 0] - [2 1]] - - Among the 6 points in the cartesian product `prod`, the closest to - the point (0.6, 0.4) is `prod[2]`: - - >>> x = (0.6, 0.4) - >>> qe.cartesian_nearest_index(x, nodes) # Pass `nodes`, not `prod` - 2 - - The closest to (-0.1, 1.2) and (2, 0) are `prod[1]` and `prod[4]`, - respectively: - - >>> x = [(-0.1, 1.2), (2, 0)] - >>> qe.cartesian_nearest_index(x, nodes) - array([1, 4]) - - Internally, the index in each dimension is searched by binary search - and then the index in the cartesian product is calculated (*not* by - constructing the cartesian product and then searching linearly over - it). - - """ - x = np.asarray(x) - is_1d = False - shape = x.shape - if len(shape) == 1: - is_1d = True - x = x[np.newaxis] - types = [type(e[0]) for e in nodes] - dtype = np.result_type(*types) - nodes = tuple(np.asarray(e, dtype=dtype) for e in nodes) - - n = shape[1-is_1d] - if len(nodes) != n: - msg = 'point `x`' if is_1d else 'points in `x`' - msg += ' must have same length as `nodes`' - raise ValueError(msg) - - out = _cartesian_nearest_indices(x, nodes, order=order) - if is_1d: - return out[0] - return out - - -@njit(cache=True) -def _cartesian_nearest_indices(X, nodes, order='C'): - """ - The main body of `cartesian_nearest_index`, jit-complied by Numba. - Note that `X` must be a 2-dim ndarray, and a Python list is not - accepted for `nodes`. - - Parameters - ---------- - X : ndarray(ndim=2) - Points to search the closest points for. - - nodes : tuple(ndarray(ndim=1)) - Tuple of sorted ndarrays of same dtype. - - order : str, optional(default='C') - ('C' or 'F') order in which the product is enumerated. - - Returns - ------- - ndarray(int, ndim=1) - Indices of the closest points to the points in `X`. - - """ - m, n = X.shape # m vectors of length n - nums_grids = np.empty(n, dtype=np.intp) - for i in range(n): - nums_grids[i] = len(nodes[i]) - - ind = np.empty(n, dtype=np.intp) - out = np.empty(m, dtype=np.intp) - - step = -1 if order == 'F' else 1 - slice_ = slice(None, None, step) - - for t in range(m): - for i in range(n): - if X[t, i] <= nodes[i][0]: - ind[i] = 0 - elif X[t, i] >= nodes[i][-1]: - ind[i] = nums_grids[i] - 1 - else: - k = np.searchsorted(nodes[i], X[t, i]) - ind[i] = ( - k if nodes[i][k] - X[t, i] < X[t, i] - nodes[i][k-1] - else k - 1 - ) - out[t] = _cartesian_index(ind[slice_], nums_grids[slice_]) - - return out - - -@njit(cache=True) -def _cartesian_index(indices, nums_grids): - n = len(indices) - idx = 0 - de_cumprod = 1 - for i in range(1,n+1): - idx += de_cumprod * indices[n-i] - de_cumprod *= nums_grids[n-i] - return idx - - -_msg_max_size_exceeded = 'Maximum allowed size exceeded' - - -@jit(nopython=True, cache=True) -def simplex_grid(m, n): - r""" - Construct an array consisting of the integer points in the - (m-1)-dimensional simplex :math:`\{x \mid x_0 + \cdots + x_{m-1} = n - \}`, or equivalently, the m-part compositions of n, which are listed - in lexicographic order. The total number of the points (hence the - length of the output array) is L = (n+m-1)!/(n!*(m-1)!) (i.e., - (n+m-1) choose (m-1)). - - Parameters - ---------- - m : scalar(int) - Dimension of each point. Must be a positive integer. - - n : scalar(int) - Number which the coordinates of each point sum to. Must be a - nonnegative integer. - - Returns - ------- - out : ndarray(int, ndim=2) - Array of shape (L, m) containing the integer points in the - simplex, aligned in lexicographic order. - - Notes - ----- - A grid of the (m-1)-dimensional *unit* simplex with n subdivisions - along each dimension can be obtained by `simplex_grid(m, n) / n`. - - Examples - -------- - >>> simplex_grid(3, 4) - array([[0, 0, 4], - [0, 1, 3], - [0, 2, 2], - [0, 3, 1], - [0, 4, 0], - [1, 0, 3], - [1, 1, 2], - [1, 2, 1], - [1, 3, 0], - [2, 0, 2], - [2, 1, 1], - [2, 2, 0], - [3, 0, 1], - [3, 1, 0], - [4, 0, 0]]) - - >>> simplex_grid(3, 4) / 4 - array([[ 0. , 0. , 1. ], - [ 0. , 0.25, 0.75], - [ 0. , 0.5 , 0.5 ], - [ 0. , 0.75, 0.25], - [ 0. , 1. , 0. ], - [ 0.25, 0. , 0.75], - [ 0.25, 0.25, 0.5 ], - [ 0.25, 0.5 , 0.25], - [ 0.25, 0.75, 0. ], - [ 0.5 , 0. , 0.5 ], - [ 0.5 , 0.25, 0.25], - [ 0.5 , 0.5 , 0. ], - [ 0.75, 0. , 0.25], - [ 0.75, 0.25, 0. ], - [ 1. , 0. , 0. ]]) - - References - ---------- - A. Nijenhuis and H. S. Wilf, Combinatorial Algorithms, Chapter 5, - Academic Press, 1978. - - """ - L = num_compositions_jit(m, n) - if L == 0: # Overflow occured - raise ValueError(_msg_max_size_exceeded) - out = np.empty((L, m), dtype=np.int_) - - x = np.zeros(m, dtype=np.int_) - x[m-1] = n - - for j in range(m): - out[0, j] = x[j] - - h = m - - for i in range(1, L): - h -= 1 - - val = x[h] - x[h] = 0 - x[m-1] = val - 1 - x[h-1] += 1 - - for j in range(m): - out[i, j] = x[j] - - if val != 1: - h = m - - return out - - -def simplex_index(x, m, n): - r""" - Return the index of the point x in the lexicographic order of the - integer points of the (m-1)-dimensional simplex :math:`\{x \mid x_0 - + \cdots + x_{m-1} = n\}`. - - Parameters - ---------- - x : array_like(int, ndim=1) - Integer point in the simplex, i.e., an array of m nonnegative - itegers that sum to n. - - m : scalar(int) - Dimension of each point. Must be a positive integer. - - n : scalar(int) - Number which the coordinates of each point sum to. Must be a - nonnegative integer. - - Returns - ------- - idx : scalar(int) - Index of x. - - """ - if m == 1: - return 0 - - decumsum = np.cumsum(x[-1:0:-1])[::-1] - idx = num_compositions(m, n) - 1 - for i in range(m-1): - if decumsum[i] == 0: - break - idx -= num_compositions(m-i, decumsum[i]-1) - return idx - - -def num_compositions(m, n): - """ - The total number of m-part compositions of n, which is equal to - (n+m-1) choose (m-1). - - Parameters - ---------- - m : scalar(int) - Number of parts of composition. - - n : scalar(int) - Integer to decompose. - - Returns - ------- - scalar(int) - Total number of m-part compositions of n. - - """ - # docs.scipy.org/doc/scipy/reference/generated/scipy.special.comb.html - return scipy.special.comb(n+m-1, m-1, exact=True) - - -@jit(nopython=True, cache=True) -def num_compositions_jit(m, n): - """ - Numba jit version of `num_compositions`. Return `0` if the outcome - exceeds the maximum value of `np.intp`. - - """ - return comb_jit(n+m-1, m-1) diff --git a/quantecon/_inequality.py b/quantecon/_inequality.py deleted file mode 100644 index 25ee252c3..000000000 --- a/quantecon/_inequality.py +++ /dev/null @@ -1,152 +0,0 @@ -""" -Implements inequality and segregation measures such as Gini, Lorenz Curve - -""" - -import numpy as np -from numba import njit, prange - - -@njit -def lorenz_curve(y): - """ - Calculates the Lorenz Curve, a graphical representation of - the distribution of income or wealth. - - It returns the cumulative share of people (x-axis) and - the cumulative share of income earned. - - Parameters - ---------- - y : array_like(float or int, ndim=1) - Array of income/wealth for each individual. - Unordered or ordered is fine. - - Returns - ------- - cum_people : array_like(float, ndim=1) - Cumulative share of people for each person index (i/n) - cum_income : array_like(float, ndim=1) - Cumulative share of income for each person index - - - References - ---------- - .. [1] https://en.wikipedia.org/wiki/Lorenz_curve - - Examples - -------- - >>> a_val, n = 3, 10_000 - >>> y = np.random.pareto(a_val, size=n) - >>> f_vals, l_vals = lorenz(y) - - """ - - n = len(y) - y = np.sort(y) - s = np.zeros(n + 1) - s[1:] = np.cumsum(y) - cum_people = np.zeros(n + 1) - cum_income = np.zeros(n + 1) - for i in range(1, n + 1): - cum_people[i] = i / n - cum_income[i] = s[i] / s[n] - return cum_people, cum_income - - -@njit(parallel=True) -def gini_coefficient(y): - r""" - Implements the Gini inequality index - - Parameters - ---------- - y : array_like(float) - Array of income/wealth for each individual. - Ordered or unordered is fine - - Returns - ------- - Gini index: float - The gini index describing the inequality of the array of income/wealth - - References - ---------- - - https://en.wikipedia.org/wiki/Gini_coefficient - """ - n = len(y) - i_sum = np.zeros(n) - for i in prange(n): - for j in range(n): - i_sum[i] += abs(y[i] - y[j]) - return np.sum(i_sum) / (2 * n * np.sum(y)) - - -def shorrocks_index(A): - r""" - Implements Shorrocks mobility index - - Parameters - ---------- - A : array_like(float) - Square matrix with transition probabilities (mobility matrix) of - dimension m - - Returns - ------- - Shorrocks index: float - The Shorrocks mobility index calculated as - - .. math:: - - s(A) = \frac{m - \sum_j a_{jj} }{m - 1} \in (0, 1) - - An index equal to 0 indicates complete immobility. - - References - ---------- - .. [1] Wealth distribution and social mobility in the US: - A quantitative approach (Benhabib, Bisin, Luo, 2017). - https://www.econ.nyu.edu/user/bisina/RevisionAugust.pdf - """ - - A = np.asarray(A) # Convert to array if not already - m, n = A.shape - - if m != n: - raise ValueError('A must be a square matrix') - - diag_sum = np.diag(A).sum() - - return (m - diag_sum) / (m - 1) - - -def rank_size(data, c=1.0): - """ - Generate rank-size data corresponding to distribution data. - - Examples - -------- - >>> y = np.exp(np.random.randn(1000)) # simulate data - >>> rank_data, size_data = rank_size(y, c=0.85) - - Parameters - ---------- - data : array_like - the set of observations - c : int or float - restrict plot to top (c x 100)% of the distribution - - Returns - ------- - rank_data : array_like(float, ndim=1) - Location in the population when sorted from smallest to largest - size_data : array_like(float, ndim=1) - Size data for top (c x 100)% of the observations - """ - w = - np.sort(- data) # Reverse sort - w = w[:int(len(w) * c)] # extract top (c * 100)% - rank_data = np.arange(len(w)) + 1 - size_data = w - return rank_data, size_data diff --git a/quantecon/_ivp.py b/quantecon/_ivp.py deleted file mode 100644 index 39cf08bd4..000000000 --- a/quantecon/_ivp.py +++ /dev/null @@ -1,238 +0,0 @@ -r""" -Base class for solving initial value problems (IVPs) of the form: - -.. math:: - - \frac{dy}{dt} = f(t,y),\ y(t_0) = y_0 - -using finite difference methods. The `quantecon.ivp` class uses various -integrators from the `scipy.integrate.ode` module to perform the -integration (i.e., solve the ODE) and parametric B-spline interpolation -from `scipy.interpolate` to approximate the value of the solution -between grid points. The `quantecon.ivp` module also provides a method -for computing the residual of the solution which can be used for -assessing the overall accuracy of the approximated solution. - -""" -import numpy as np -from scipy import integrate, interpolate - - -class IVP(integrate.ode): - - r""" - Creates an instance of the IVP class. - - Parameters - ---------- - f : callable ``f(t, y, *f_args)`` - Right hand side of the system of equations defining the ODE. - The independent variable, ``t``, is a ``scalar``; ``y`` is - an ``ndarray`` of dependent variables with ``y.shape == - (n,)``. The function `f` should return a ``scalar``, - ``ndarray`` or ``list`` (but not a ``tuple``). - jac : callable ``jac(t, y, *jac_args)``, optional(default=None) - Jacobian of the right hand side of the system of equations - defining the ODE. - - .. :math: - - \mathcal{J}_{i,j} = \bigg[\frac{\partial f_i}{\partial y_j}\bigg] - - """ - - def __init__(self, f, jac=None): - - super(IVP, self).__init__(f, jac) - - def _integrate_fixed_trajectory(self, h, T, step, relax): - """Generates a solution trajectory of fixed length.""" - # initialize the solution using initial condition - solution = np.hstack((self.t, self.y)) - - while self.successful(): - - self.integrate(self.t + h, step, relax) - current_step = np.hstack((self.t, self.y)) - solution = np.vstack((solution, current_step)) - - if (h > 0) and (self.t >= T): - break - elif (h < 0) and (self.t <= T): - break - else: - continue - - return solution - - def _integrate_variable_trajectory(self, h, g, tol, step, relax): - """Generates a solution trajectory of variable length.""" - # initialize the solution using initial condition - solution = np.hstack((self.t, self.y)) - - while self.successful(): - - self.integrate(self.t + h, step, relax) - current_step = np.hstack((self.t, self.y)) - solution = np.vstack((solution, current_step)) - - if g(self.t, self.y, *self.f_params) < tol: - break - else: - continue - - return solution - - def _initialize_integrator(self, t0, y0, integrator, **kwargs): - """Initializes the integrator prior to integration.""" - # set the initial condition - self.set_initial_value(y0, t0) - - # select the integrator - self.set_integrator(integrator, **kwargs) - - def compute_residual(self, traj, ti, k=3, ext=2): - r""" - The residual is the difference between the derivative of the B-spline - approximation of the solution trajectory and the right-hand side of the - original ODE evaluated along the approximated solution trajectory. - - Parameters - ---------- - traj : array_like (float) - Solution trajectory providing the data points for constructing the - B-spline representation. - ti : array_like (float) - Array of values for the independent variable at which to - interpolate the value of the B-spline. - k : int, optional(default=3) - Degree of the desired B-spline. Degree must satisfy - :math:`1 \le k \le 5`. - ext : int, optional(default=2) - Controls the value of returned elements for outside the - original knot sequence provided by traj. For extrapolation, set - `ext=0`; `ext=1` returns zero; `ext=2` raises a `ValueError`. - - Returns - ------- - residual : array (float) - Difference between the derivative of the B-spline approximation - of the solution trajectory and the right-hand side of the ODE - evaluated along the approximated solution trajectory. - - """ - # B-spline approximations of the solution and its derivative - soln = self.interpolate(traj, ti, k, 0, ext) - deriv = self.interpolate(traj, ti, k, 1, ext) - - # rhs of ode evaluated along approximate solution - T = ti.size - rhs_ode = np.vstack([self.f(ti[i], soln[i, 1:], *self.f_params) - for i in range(T)]) - rhs_ode = np.hstack((ti[:, np.newaxis], rhs_ode)) - - # should be roughly zero everywhere (if approximation is any good!) - residual = deriv - rhs_ode - - return residual - - def solve(self, t0, y0, h=1.0, T=None, g=None, tol=None, - integrator='dopri5', step=False, relax=False, **kwargs): - r""" - Solve the IVP by integrating the ODE given some initial condition. - - Parameters - ---------- - t0 : float - Initial condition for the independent variable. - y0 : array_like (float, shape=(n,)) - Initial condition for the dependent variables. - h : float, optional(default=1.0) - Step-size for computing the solution. Can be positive or negative - depending on the desired direction of integration. - T : int, optional(default=None) - Terminal value for the independent variable. One of either `T` - or `g` must be specified. - g : callable ``g(t, y, f_args)``, optional(default=None) - Provides a stopping condition for the integration. If specified - user must also specify a stopping tolerance, `tol`. - tol : float, optional (default=None) - Stopping tolerance for the integration. Only required if `g` is - also specifed. - integrator : str, optional(default='dopri5') - Must be one of 'vode', 'lsoda', 'dopri5', or 'dop853' - step : bool, optional(default=False) - Allows access to internal steps for those solvers that use adaptive - step size routines. Currently only 'vode', 'zvode', and 'lsoda' - support `step=True`. - relax : bool, optional(default=False) - Currently only 'vode', 'zvode', and 'lsoda' support `relax=True`. - **kwargs : dict, optional(default=None) - Dictionary of integrator specific keyword arguments. See the - Notes section of the docstring for `scipy.integrate.ode` for a - complete description of solver specific keyword arguments. - - Returns - ------- - solution: ndarray (float) - Simulated solution trajectory. - - """ - self._initialize_integrator(t0, y0, integrator, **kwargs) - - if (g is not None) and (tol is not None): - soln = self._integrate_variable_trajectory(h, g, tol, step, relax) - elif T is not None: - soln = self._integrate_fixed_trajectory(h, T, step, relax) - else: - mesg = "Either both 'g' and 'tol', or 'T' must be specified." - raise ValueError(mesg) - - return soln - - def interpolate(self, traj, ti, k=3, der=0, ext=2): - r""" - Parametric B-spline interpolation in N-dimensions. - - Parameters - ---------- - traj : array_like (float) - Solution trajectory providing the data points for constructing the - B-spline representation. - ti : array_like (float) - Array of values for the independent variable at which to - interpolate the value of the B-spline. - k : int, optional(default=3) - Degree of the desired B-spline. Degree must satisfy - :math:`1 \le k \le 5`. - der : int, optional(default=0) - The order of derivative of the spline to compute (must be less - than or equal to `k`). - ext : int, optional(default=2) Controls the value of returned elements - for outside the original knot sequence provided by traj. For - extrapolation, set `ext=0`; `ext=1` returns zero; `ext=2` raises a - `ValueError`. - - Returns - ------- - interp_traj: ndarray (float) - The interpolated trajectory. - - """ - # array of parameter values - u = traj[:, 0] - - # build list of input arrays - n = traj.shape[1] - x = [traj[:, i] for i in range(1, n)] - - # construct the B-spline representation (s=0 forces interpolation!) - tck, t = interpolate.splprep(x, u=u, k=k, s=0) - - # evaluate the B-spline (returns a list) - out = interpolate.splev(ti, tck, der, ext) - - # convert to a 2D array - interp_traj = np.hstack((ti[:, np.newaxis], np.array(out).T)) - - return interp_traj diff --git a/quantecon/_kalman.py b/quantecon/_kalman.py deleted file mode 100644 index c6a94c4b9..000000000 --- a/quantecon/_kalman.py +++ /dev/null @@ -1,321 +0,0 @@ -""" -Implements the Kalman filter for a linear Gaussian state space model. - -References ----------- - -https://lectures.quantecon.org/py/kalman.html - -""" -from textwrap import dedent -import numpy as np -from scipy.linalg import inv -from ._lss import LinearStateSpace -from ._matrix_eqn import solve_discrete_riccati - - -class Kalman: - r""" - Implements the Kalman filter for the Gaussian state space model - - .. math:: - - x_{t+1} = A x_t + C w_{t+1} \\ - y_t = G x_t + H v_t - - Here :math:`x_t` is the hidden state and :math:`y_t` is the measurement. - The shocks :math:`w_t` and :math:`v_t` are iid standard normals. Below - we use the notation - - .. math:: - - Q := CC' - R := HH' - - - Parameters - ---------- - ss : instance of LinearStateSpace - An instance of the quantecon.lss.LinearStateSpace class - x_hat : scalar(float) or array_like(float), optional(default=None) - An n x 1 array representing the mean x_hat of the - prior/predictive density. Set to zero if not supplied. - Sigma : scalar(float) or array_like(float), optional(default=None) - An n x n array representing the covariance matrix Sigma of - the prior/predictive density. Must be positive definite. - Set to the identity if not supplied. - - Attributes - ---------- - Sigma, x_hat : as above - Sigma_infinity : array_like or scalar(float) - The infinite limit of Sigma_t - K_infinity : array_like or scalar(float) - The stationary Kalman gain. - - - References - ---------- - - https://lectures.quantecon.org/py/kalman.html - - """ - - def __init__(self, ss, x_hat=None, Sigma=None): - self.ss = ss - self.set_state(x_hat, Sigma) - self._K_infinity = None - self._Sigma_infinity = None - - def set_state(self, x_hat, Sigma): - if Sigma is None: - self.Sigma = np.identity(self.ss.n) - else: - self.Sigma = np.atleast_2d(Sigma) - if x_hat is None: - self.x_hat = np.zeros((self.ss.n, 1)) - else: - self.x_hat = np.atleast_2d(x_hat) - self.x_hat.shape = self.ss.n, 1 - - def __repr__(self): - return self.__str__() - - def __str__(self): - m = """\ - Kalman filter: - - dimension of state space : {n} - - dimension of observation equation : {k} - """ - return dedent(m.format(n=self.ss.n, k=self.ss.k)) - - @property - def Sigma_infinity(self): - if self._Sigma_infinity is None: - self.stationary_values() - return self._Sigma_infinity - - @property - def K_infinity(self): - if self._K_infinity is None: - self.stationary_values() - return self._K_infinity - - def whitener_lss(self): - r""" - This function takes the linear state space system - that is an input to the Kalman class and it converts - that system to the time-invariant whitener represenation - given by - - .. math:: - - \tilde{x}_{t+1}^* = \tilde{A} \tilde{x} + \tilde{C} v - a = \tilde{G} \tilde{x} - - where - - .. math:: - - \tilde{x}_t = [x+{t}, \hat{x}_{t}, v_{t}] - - and - - .. math:: - - \tilde{A} = - \begin{bmatrix} - A & 0 & 0 \\ - KG & A-KG & KH \\ - 0 & 0 & 0 \\ - \end{bmatrix} - - .. math:: - - \tilde{C} = - \begin{bmatrix} - C & 0 \\ - 0 & 0 \\ - 0 & I \\ - \end{bmatrix} - - .. math:: - - \tilde{G} = - \begin{bmatrix} - G & -G & H \\ - \end{bmatrix} - - with :math:`A, C, G, H` coming from the linear state space system - that defines the Kalman instance - - Returns - ------- - whitened_lss : LinearStateSpace - This is the linear state space system that represents - the whitened system - """ - K = self.K_infinity - - # Get the matrix sizes - n, k, m, l = self.ss.n, self.ss.k, self.ss.m, self.ss.l - A, C, G, H = self.ss.A, self.ss.C, self.ss.G, self.ss.H - - Atil = np.vstack([np.hstack([A, np.zeros((n, n)), np.zeros((n, l))]), - np.hstack([np.dot(K, G), - A-np.dot(K, G), - np.dot(K, H)]), - np.zeros((l, 2*n + l))]) - - Ctil = np.vstack([np.hstack([C, np.zeros((n, l))]), - np.zeros((n, m+l)), - np.hstack([np.zeros((l, m)), np.eye(l)])]) - - Gtil = np.hstack([G, -G, H]) - - whitened_lss = LinearStateSpace(Atil, Ctil, Gtil) - self.whitened_lss = whitened_lss - - return whitened_lss - - def prior_to_filtered(self, y): - r""" - Updates the moments (x_hat, Sigma) of the time t prior to the - time t filtering distribution, using current measurement :math:`y_t`. - - The updates are according to - - .. math:: - - \hat{x}^F = \hat{x} + \Sigma G' (G \Sigma G' + R)^{-1} - (y - G \hat{x}) - \Sigma^F = \Sigma - \Sigma G' (G \Sigma G' + R)^{-1} G - \Sigma - - Parameters - ---------- - y : scalar or array_like(float) - The current measurement - - """ - # === simplify notation === # - G, H = self.ss.G, self.ss.H - R = np.dot(H, H.T) - - # === and then update === # - y = np.atleast_2d(y) - y.shape = self.ss.k, 1 - E = np.dot(self.Sigma, G.T) - F = np.dot(np.dot(G, self.Sigma), G.T) + R - M = np.dot(E, inv(F)) - self.x_hat = self.x_hat + np.dot(M, (y - np.dot(G, self.x_hat))) - self.Sigma = self.Sigma - np.dot(M, np.dot(G, self.Sigma)) - - def filtered_to_forecast(self): - """ - Updates the moments of the time t filtering distribution to the - moments of the predictive distribution, which becomes the time - t+1 prior - - """ - # === simplify notation === # - A, C = self.ss.A, self.ss.C - Q = np.dot(C, C.T) - - # === and then update === # - self.x_hat = np.dot(A, self.x_hat) - self.Sigma = np.dot(A, np.dot(self.Sigma, A.T)) + Q - - def update(self, y): - """ - Updates x_hat and Sigma given k x 1 ndarray y. The full - update, from one period to the next - - Parameters - ---------- - y : np.ndarray - A k x 1 ndarray y representing the current measurement - - """ - self.prior_to_filtered(y) - self.filtered_to_forecast() - - def stationary_values(self, method='doubling'): - r""" - Computes the limit of :math:`\Sigma_t` as t goes to infinity by - solving the associated Riccati equation. The outputs are stored in the - attributes `K_infinity` and `Sigma_infinity`. Computation is via the - doubling algorithm (default) or a QZ decomposition method (see the - documentation in `matrix_eqn.solve_discrete_riccati`). - - Parameters - ---------- - method : str, optional(default="doubling") - Solution method used in solving the associated Riccati - equation, str in {'doubling', 'qz'}. - - Returns - ------- - Sigma_infinity : array_like or scalar(float) - The infinite limit of :math:`\Sigma_t` - K_infinity : array_like or scalar(float) - The stationary Kalman gain. - - """ - - # === simplify notation === # - A, C, G, H = self.ss.A, self.ss.C, self.ss.G, self.ss.H - Q, R = np.dot(C, C.T), np.dot(H, H.T) - - # === solve Riccati equation, obtain Kalman gain === # - Sigma_infinity = solve_discrete_riccati(A.T, G.T, Q, R, method=method) - temp1 = np.dot(np.dot(A, Sigma_infinity), G.T) - temp2 = inv(np.dot(G, np.dot(Sigma_infinity, G.T)) + R) - K_infinity = np.dot(temp1, temp2) - - # == record as attributes and return == # - self._Sigma_infinity, self._K_infinity = Sigma_infinity, K_infinity - return Sigma_infinity, K_infinity - - def stationary_coefficients(self, j, coeff_type='ma'): - """ - Wold representation moving average or VAR coefficients for the - steady state Kalman filter. - - Parameters - ---------- - j : int - The lag length - coeff_type : string, either 'ma' or 'var' (default='ma') - The type of coefficent sequence to compute. Either 'ma' for - moving average or 'var' for VAR. - """ - # == simplify notation == # - A, G = self.ss.A, self.ss.G - K_infinity = self.K_infinity - # == compute and return coefficients == # - coeffs = [] - i = 1 - if coeff_type == 'ma': - coeffs.append(np.identity(self.ss.k)) - P_mat = A - P = np.identity(self.ss.n) # Create a copy - elif coeff_type == 'var': - coeffs.append(np.dot(G, K_infinity)) - P_mat = A - np.dot(K_infinity, G) - P = np.copy(P_mat) # Create a copy - else: - raise ValueError("Unknown coefficient type") - while i <= j: - coeffs.append(np.dot(np.dot(G, P), K_infinity)) - P = np.dot(P, P_mat) - i += 1 - return coeffs - - def stationary_innovation_covar(self): - # == simplify notation == # - H, G = self.ss.H, self.ss.G - R = np.dot(H, H.T) - Sigma_infinity = self.Sigma_infinity - - return np.dot(G, np.dot(Sigma_infinity, G.T)) + R diff --git a/quantecon/_lae.py b/quantecon/_lae.py deleted file mode 100644 index 83b59c87c..000000000 --- a/quantecon/_lae.py +++ /dev/null @@ -1,83 +0,0 @@ -r""" -Computes a sequence of marginal densities for a continuous state space -Markov chain :math:`X_t` where the transition probabilities can be represented -as densities. The estimate of the marginal density of :math:`X_t` is - -.. math:: - - \frac{1}{n} \sum_{i=0}^n p(X_{t-1}^i, y) - -This is a density in :math:`y`. - -References ----------- - -https://lectures.quantecon.org/py/stationary_densities.html - -""" -from textwrap import dedent -import numpy as np - - -class LAE: - """ - An instance is a representation of a look ahead estimator associated - with a given stochastic kernel p and a vector of observations X. - - Parameters - ---------- - p : function - The stochastic kernel. A function p(x, y) that is vectorized in - both x and y - X : array_like(float) - A vector containing observations - - Attributes - ---------- - p, X : see Parameters - - Examples - -------- - >>> psi = LAE(p, X) - >>> y = np.linspace(0, 1, 100) - >>> psi(y) # Evaluate look ahead estimate at grid of points y - - """ - - def __init__(self, p, X): - X = X.flatten() # So we know what we're dealing with - n = len(X) - self.p, self.X = p, X.reshape((n, 1)) - - def __repr__(self): - return self.__str__() - - def __str__(self): - m = """\ - Look ahead estimator - - number of observations : {n} - """ - return dedent(m.format(n=self.X.size)) - - def __call__(self, y): - """ - A vectorized function that returns the value of the look ahead - estimate at the values in the array y. - - Parameters - ---------- - y : array_like(float) - A vector of points at which we wish to evaluate the look- - ahead estimator - - Returns - ------- - psi_vals : array_like(float) - The values of the density estimate at the points in y - - """ - k = len(y) - v = self.p(self.X, y.reshape((1, k))) - psi_vals = np.mean(v, axis=0) # Take mean along each row - - return psi_vals.flatten() diff --git a/quantecon/_lqcontrol.py b/quantecon/_lqcontrol.py deleted file mode 100644 index 2b9fb3fc3..000000000 --- a/quantecon/_lqcontrol.py +++ /dev/null @@ -1,624 +0,0 @@ -""" -Provides a class called LQ for solving linear quadratic control -problems, and a class called LQMarkov for solving Markov jump -linear quadratic control problems. - -""" -from textwrap import dedent -import numpy as np -from scipy.linalg import solve -from ._matrix_eqn import solve_discrete_riccati, solve_discrete_riccati_system -from .util import check_random_state -from .markov import MarkovChain - - -class LQ: - r""" - This class is for analyzing linear quadratic optimal control - problems of either the infinite horizon form - - .. math:: - - \min \mathbb{E} - \Big[ \sum_{t=0}^{\infty} \beta^t r(x_t, u_t) \Big] - - with - - .. math:: - - r(x_t, u_t) := x_t' R x_t + u_t' Q u_t + 2 u_t' N x_t - - or the finite horizon form - - .. math:: - - \min \mathbb{E} - \Big[ - \sum_{t=0}^{T-1} \beta^t r(x_t, u_t) + \beta^T x_T' R_f x_T - \Big] - - Both are minimized subject to the law of motion - - .. math:: - - x_{t+1} = A x_t + B u_t + C w_{t+1} - - Here :math:`x` is n x 1, :math:`u` is k x 1, :math:`w` is j x 1 and the - matrices are conformable for these dimensions. The sequence :math:`{w_t}` - is assumed to be white noise, with zero mean and - :math:`\mathbb{E} [ w_t' w_t ] = I`, the j x j identity. - - If :math:`C` is not supplied as a parameter, the model is assumed to be - deterministic (and :math:`C` is set to a zero matrix of appropriate - dimension). - - For this model, the time t value (i.e., cost-to-go) function :math:`V_t` - takes the form - - .. math:: - - x' P_T x + d_T - - and the optimal policy is of the form :math:`u_T = -F_T x_T`. In the - infinite horizon case, :math:`V, P, d` and :math:`F` are all stationary. - - Parameters - ---------- - Q : array_like(float) - Q is the payoff (or cost) matrix that corresponds with the - control variable u and is k x k. Should be symmetric and - non-negative definite - R : array_like(float) - R is the payoff (or cost) matrix that corresponds with the - state variable x and is n x n. Should be symmetric and - non-negative definite - A : array_like(float) - A is part of the state transition as described above. It should - be n x n - B : array_like(float) - B is part of the state transition as described above. It should - be n x k - C : array_like(float), optional(default=None) - C is part of the state transition as described above and - corresponds to the random variable today. If the model is - deterministic then C should take default value of None - N : array_like(float), optional(default=None) - N is the cross product term in the payoff, as above. It should - be k x n. - beta : scalar(float), optional(default=1) - beta is the discount parameter - T : scalar(int), optional(default=None) - T is the number of periods in a finite horizon problem. - Rf : array_like(float), optional(default=None) - Rf is the final (in a finite horizon model) payoff(or cost) - matrix that corresponds with the control variable u and is n x - n. Should be symmetric and non-negative definite - - Attributes - ---------- - Q, R, N, A, B, C, beta, T, Rf : see Parameters - P : array_like(float) - P is part of the value function representation of - :math:`V(x) = x'Px + d` - d : array_like(float) - d is part of the value function representation of - :math:`V(x) = x'Px + d` - F : array_like(float) - F is the policy rule that determines the choice of control in - each period. - k, n, j : scalar(int) - The dimensions of the matrices as presented above - - """ - - def __init__(self, Q, R, A, B, C=None, N=None, beta=1, T=None, Rf=None): - # == Make sure all matrices can be treated as 2D arrays == # - converter = lambda X: np.atleast_2d(np.asarray(X, dtype='float')) - self.A, self.B, self.Q, self.R, self.N = list(map(converter, - (A, B, Q, R, N))) - # == Record dimensions == # - self.k, self.n = self.Q.shape[0], self.R.shape[0] - - self.beta = beta - - if C is None: - # == If C not given, then model is deterministic. Set C=0. == # - self.j = 1 - self.C = np.zeros((self.n, self.j)) - else: - self.C = converter(C) - self.j = self.C.shape[1] - - if N is None: - # == No cross product term in payoff. Set N=0. == # - self.N = np.zeros((self.k, self.n)) - - if T: - # == Model is finite horizon == # - self.T = T - self.Rf = np.asarray(Rf, dtype='float') - self.P = self.Rf - self.d = 0 - else: - self.P = None - self.d = None - self.T = None - - if (self.C != 0).any() and beta >= 1: - raise ValueError('beta must be strictly smaller than 1 if ' + - 'T = None and C != 0.') - - self.F = None - - def __repr__(self): - return self.__str__() - - def __str__(self): - m = """\ - Linear Quadratic control system - - beta (discount parameter) : {b} - - T (time horizon) : {t} - - n (number of state variables) : {n} - - k (number of control variables) : {k} - - j (number of shocks) : {j} - """ - t = "infinite" if self.T is None else self.T - return dedent(m.format(b=self.beta, n=self.n, k=self.k, j=self.j, - t=t)) - - def update_values(self): - """ - This method is for updating in the finite horizon case. It - shifts the current value function - - .. math:: - - V_t(x) = x' P_t x + d_t - - and the optimal policy :math:`F_t` one step *back* in time, - replacing the pair :math:`P_t` and :math:`d_t` with - :math:`P_{t-1}` and :math:`d_{t-1}`, and :math:`F_t` with - :math:`F_{t-1}` - - """ - # === Simplify notation === # - Q, R, A, B, N, C = self.Q, self.R, self.A, self.B, self.N, self.C - P, d = self.P, self.d - # == Some useful matrices == # - S1 = Q + self.beta * np.dot(B.T, np.dot(P, B)) - S2 = self.beta * np.dot(B.T, np.dot(P, A)) + N - S3 = self.beta * np.dot(A.T, np.dot(P, A)) - # == Compute F as (Q + B'PB)^{-1} (beta B'PA + N) == # - self.F = solve(S1, S2) - # === Shift P back in time one step == # - new_P = R - np.dot(S2.T, self.F) + S3 - # == Recalling that trace(AB) = trace(BA) == # - new_d = self.beta * (d + np.trace(np.dot(P, np.dot(C, C.T)))) - # == Set new state == # - self.P, self.d = new_P, new_d - - def stationary_values(self, method='doubling'): - """ - Computes the matrix :math:`P` and scalar :math:`d` that represent - the value function - - .. math:: - - V(x) = x' P x + d - - in the infinite horizon case. Also computes the control matrix - :math:`F` from :math:`u = - Fx`. Computation is via the solution - algorithm as specified by the `method` option (default to the - doubling algorithm) (see the documentation in - `matrix_eqn.solve_discrete_riccati`). - - Parameters - ---------- - method : str, optional(default='doubling') - Solution method used in solving the associated Riccati - equation, str in {'doubling', 'qz'}. - - Returns - ------- - P : array_like(float) - P is part of the value function representation of - :math:`V(x) = x'Px + d` - F : array_like(float) - F is the policy rule that determines the choice of control - in each period. - d : array_like(float) - d is part of the value function representation of - :math:`V(x) = x'Px + d` - - """ - # === simplify notation === # - Q, R, A, B, N, C = self.Q, self.R, self.A, self.B, self.N, self.C - - # === solve Riccati equation, obtain P === # - A0, B0 = np.sqrt(self.beta) * A, np.sqrt(self.beta) * B - P = solve_discrete_riccati(A0, B0, R, Q, N, method=method) - - # == Compute F == # - S1 = Q + self.beta * np.dot(B.T, np.dot(P, B)) - S2 = self.beta * np.dot(B.T, np.dot(P, A)) + N - F = solve(S1, S2) - - # == Compute d == # - if self.beta == 1: - d = 0 - else: - d = self.beta * np.trace(np.dot(P, np.dot(C, C.T))) / (1 - self.beta) - - # == Bind states and return values == # - self.P, self.F, self.d = P, F, d - - return P, F, d - - def compute_sequence(self, x0, ts_length=None, method='doubling', - random_state=None): - """ - Compute and return the optimal state and control sequences - :math:`x_0, ..., x_T` and :math:`u_0,..., u_T` under the - assumption that :math:`{w_t}` is iid and :math:`N(0, 1)`. - - Parameters - ---------- - x0 : array_like(float) - The initial state, a vector of length n - - ts_length : scalar(int) - Length of the simulation -- defaults to T in finite case - - method : str, optional(default='doubling') - Solution method used in solving the associated Riccati - equation, str in {'doubling', 'qz'}. Only relevant when the - `T` attribute is `None` (i.e., the horizon is infinite). - - random_state : int or np.random.RandomState/Generator, optional - Random seed (integer) or np.random.RandomState or Generator - instance to set the initial state of the random number - generator for reproducibility. If None, a randomly - initialized RandomState is used. - - Returns - ------- - x_path : array_like(float) - An n x T+1 matrix, where the t-th column represents :math:`x_t` - - u_path : array_like(float) - A k x T matrix, where the t-th column represents :math:`u_t` - - w_path : array_like(float) - A j x T+1 matrix, where the t-th column represent :math:`w_t` - - """ - - # === Simplify notation === # - A, B, C = self.A, self.B, self.C - - # == Preliminaries, finite horizon case == # - if self.T: - T = self.T if not ts_length else min(ts_length, self.T) - self.P, self.d = self.Rf, 0 - - # == Preliminaries, infinite horizon case == # - else: - T = ts_length if ts_length else 100 - if self.P is None: - self.stationary_values(method=method) - - # == Set up initial condition and arrays to store paths == # - random_state = check_random_state(random_state) - x0 = np.asarray(x0) - x0 = x0.reshape(self.n, 1) # Make sure x0 is a column vector - x_path = np.empty((self.n, T+1)) - u_path = np.empty((self.k, T)) - w_path = random_state.standard_normal((self.j, T+1)) - Cw_path = np.dot(C, w_path) - - # == Compute and record the sequence of policies == # - policies = [] - for t in range(T): - if self.T: # Finite horizon case - self.update_values() - policies.append(self.F) - - # == Use policy sequence to generate states and controls == # - F = policies.pop() - x_path[:, 0] = x0.flatten() - u_path[:, 0] = - np.dot(F, x0).flatten() - for t in range(1, T): - F = policies.pop() - Ax, Bu = np.dot(A, x_path[:, t-1]), np.dot(B, u_path[:, t-1]) - x_path[:, t] = Ax + Bu + Cw_path[:, t] - u_path[:, t] = - np.dot(F, x_path[:, t]) - Ax, Bu = np.dot(A, x_path[:, T-1]), np.dot(B, u_path[:, T-1]) - x_path[:, T] = Ax + Bu + Cw_path[:, T] - - return x_path, u_path, w_path - - -class LQMarkov: - r""" - This class is for analyzing Markov jump linear quadratic optimal - control problems of the infinite horizon form - - .. math:: - - \min \mathbb{E} - \Big[ \sum_{t=0}^{\infty} \beta^t r(x_t, s_t, u_t) \Big] - - with - - .. math:: - - r(x_t, s_t, u_t) := - (x_t' R(s_t) x_t + u_t' Q(s_t) u_t + 2 u_t' N(s_t) x_t) - - subject to the law of motion - - .. math:: - - x_{t+1} = A(s_t) x_t + B(s_t) u_t + C(s_t) w_{t+1} - - Here :math:`x` is n x 1, :math:`u` is k x 1, :math:`w` is j x 1 and the - matrices are conformable for these dimensions. The sequence :math:`{w_t}` - is assumed to be white noise, with zero mean and - :math:`\mathbb{E} [ w_t' w_t ] = I`, the j x j identity. - - If :math:`C` is not supplied as a parameter, the model is assumed to be - deterministic (and :math:`C` is set to a zero matrix of appropriate - dimension). - - The optimal value function :math:`V(x_t, s_t)` takes the form - - .. math:: - - x_t' P(s_t) x_t + d(s_t) - - and the optimal policy is of the form :math:`u_t = -F(s_t) x_t`. - - Parameters - ---------- - Π : array_like(float, ndim=2) - The Markov chain transition matrix with dimension m x m. - Qs : array_like(float) - Consists of m symmetric and non-negative definite payoff - matrices Q(s) with dimension k x k that corresponds with - the control variable u for each Markov state s - Rs : array_like(float) - Consists of m symmetric and non-negative definite payoff - matrices R(s) with dimension n x n that corresponds with - the state variable x for each Markov state s - As : array_like(float) - Consists of m state transition matrices A(s) with dimension - n x n for each Markov state s - Bs : array_like(float) - Consists of m state transition matrices B(s) with dimension - n x k for each Markov state s - Cs : array_like(float), optional(default=None) - Consists of m state transition matrices C(s) with dimension - n x j for each Markov state s. If the model is deterministic - then Cs should take default value of None - Ns : array_like(float), optional(default=None) - Consists of m cross product term matrices N(s) with dimension - k x n for each Markov state, - beta : scalar(float), optional(default=1) - beta is the discount parameter - - Attributes - ---------- - Π, Qs, Rs, Ns, As, Bs, Cs, beta : see Parameters - Ps : array_like(float) - Ps is part of the value function representation of - :math:`V(x, s) = x' P(s) x + d(s)` - ds : array_like(float) - ds is part of the value function representation of - :math:`V(x, s) = x' P(s) x + d(s)` - Fs : array_like(float) - Fs is the policy rule that determines the choice of control in - each period at each Markov state - m : scalar(int) - The number of Markov states - k, n, j : scalar(int) - The dimensions of the matrices as presented above - - """ - - def __init__(self, Π, Qs, Rs, As, Bs, Cs=None, Ns=None, beta=1): - - # == Make sure all matrices for each state are 2D arrays == # - def converter(Xs): - return np.array([np.atleast_2d(np.asarray(X, dtype='float')) - for X in Xs]) - self.As, self.Bs, self.Qs, self.Rs = list(map(converter, - (As, Bs, Qs, Rs))) - - # == Record number of states == # - self.m = self.Qs.shape[0] - # == Record dimensions == # - self.k, self.n = self.Qs.shape[1], self.Rs.shape[1] - - if Ns is None: - # == No cross product term in payoff. Set N=0. == # - Ns = [np.zeros((self.k, self.n)) for i in range(self.m)] - - self.Ns = converter(Ns) - - if Cs is None: - # == If C not given, then model is deterministic. Set C=0. == # - self.j = 1 - Cs = [np.zeros((self.n, self.j)) for i in range(self.m)] - - self.Cs = converter(Cs) - self.j = self.Cs.shape[2] - - self.beta = beta - - self.Π = np.asarray(Π, dtype='float') - - self.Ps = None - self.ds = None - self.Fs = None - - def __repr__(self): - return self.__str__() - - def __str__(self): - m = """\ - Markov Jump Linear Quadratic control system - - beta (discount parameter) : {b} - - T (time horizon) : {t} - - m (number of Markov states) : {m} - - n (number of state variables) : {n} - - k (number of control variables) : {k} - - j (number of shocks) : {j} - """ - t = "infinite" - return dedent(m.format(b=self.beta, m=self.m, n=self.n, k=self.k, - j=self.j, t=t)) - - def stationary_values(self, max_iter=1000): - """ - Computes the matrix :math:`P(s)` and scalar :math:`d(s)` that - represent the value function - - .. math:: - - V(x, s) = x' P(s) x + d(s) - - in the infinite horizon case. Also computes the control matrix - :math:`F` from :math:`u = - F(s) x`. - - Parameters - ---------- - max_iter : scalar(int), optional(default=1000) - The maximum number of iterations allowed - - Returns - ------- - Ps : array_like(float) - Ps is part of the value function representation of - :math:`V(x, s) = x' P(s) x + d(s)` - ds : array_like(float) - ds is part of the value function representation of - :math:`V(x, s) = x' P(s) x + d(s)` - Fs : array_like(float) - Fs is the policy rule that determines the choice of control in - each period at each Markov state - - """ - - # == Simplify notations == # - beta, Π = self.beta, self.Π - m, n, k = self.m, self.n, self.k - As, Bs, Cs = self.As, self.Bs, self.Cs - Qs, Rs, Ns = self.Qs, self.Rs, self.Ns - - # == Solve for P(s) by iterating discrete riccati system== # - Ps = solve_discrete_riccati_system(Π, As, Bs, Cs, Qs, Rs, Ns, beta, - max_iter=max_iter) - - # == calculate F and d == # - Fs = np.array([np.empty((k, n)) for i in range(m)]) - X = np.empty((m, m)) - sum1, sum2 = np.empty((k, k)), np.empty((k, n)) - for i in range(m): - # CCi = C_i C_i' - CCi = Cs[i] @ Cs[i].T - sum1[:, :] = 0. - sum2[:, :] = 0. - for j in range(m): - # for F - sum1 += beta * Π[i, j] * Bs[i].T @ Ps[j] @ Bs[i] - sum2 += beta * Π[i, j] * Bs[i].T @ Ps[j] @ As[i] - - # for d - X[j, i] = np.trace(Ps[j] @ CCi) - - Fs[i][:, :] = solve(Qs[i] + sum1, sum2 + Ns[i]) - - ds = solve(np.eye(m) - beta * Π, - np.diag(beta * Π @ X).reshape((m, 1))).flatten() - - self.Ps, self.ds, self.Fs = Ps, ds, Fs - - return Ps, ds, Fs - - def compute_sequence(self, x0, ts_length=None, random_state=None): - """ - Compute and return the optimal state and control sequences - :math:`x_0, ..., x_T` and :math:`u_0,..., u_T` under the - assumption that :math:`{w_t}` is iid and :math:`N(0, 1)`, - with Markov states sequence :math:`s_0, ..., s_T` - - Parameters - ---------- - x0 : array_like(float) - The initial state, a vector of length n - - ts_length : scalar(int), optional(default=None) - Length of the simulation. If None, T is set to be 100 - - random_state : int or np.random.RandomState/Generator, optional - Random seed (integer) or np.random.RandomState or Generator - instance to set the initial state of the random number - generator for reproducibility. If None, a randomly - initialized RandomState is used. - - Returns - ------- - x_path : array_like(float) - An n x T+1 matrix, where the t-th column represents :math:`x_t` - - u_path : array_like(float) - A k x T matrix, where the t-th column represents :math:`u_t` - - w_path : array_like(float) - A j x T+1 matrix, where the t-th column represent :math:`w_t` - - state : array_like(int) - Array containing the state values :math:`s_t` of the sample path - - """ - - # === solve for optimal policies === # - if self.Ps is None: - self.stationary_values() - - # === Simplify notation === # - As, Bs, Cs = self.As, self.Bs, self.Cs - Fs = self.Fs - - random_state = check_random_state(random_state) - x0 = np.asarray(x0) - x0 = x0.reshape(self.n, 1) - - T = ts_length if ts_length else 100 - - # == Simulate Markov states == # - chain = MarkovChain(self.Π) - state = chain.simulate_indices(ts_length=T+1, - random_state=random_state) - - # == Prepare storage arrays == # - x_path = np.empty((self.n, T+1)) - u_path = np.empty((self.k, T)) - w_path = random_state.standard_normal((self.j, T+1)) - Cw_path = np.empty((self.n, T+1)) - for i in range(T+1): - Cw_path[:, i] = Cs[state[i]] @ w_path[:, i] - - # == Use policy sequence to generate states and controls == # - x_path[:, 0] = x0.flatten() - u_path[:, 0] = - (Fs[state[0]] @ x0).flatten() - for t in range(1, T): - Ax = As[state[t]] @ x_path[:, t-1] - Bu = Bs[state[t]] @ u_path[:, t-1] - x_path[:, t] = Ax + Bu + Cw_path[:, t] - u_path[:, t] = - (Fs[state[t]] @ x_path[:, t]) - Ax = As[state[T]] @ x_path[:, T-1] - Bu = Bs[state[T]] @ u_path[:, T-1] - x_path[:, T] = Ax + Bu + Cw_path[:, T] - - return x_path, u_path, w_path, state diff --git a/quantecon/_lqnash.py b/quantecon/_lqnash.py deleted file mode 100644 index 2b2fbbbe0..000000000 --- a/quantecon/_lqnash.py +++ /dev/null @@ -1,153 +0,0 @@ -import numpy as np -from scipy.linalg import solve -from .util import check_random_state - - -def nnash(A, B1, B2, R1, R2, Q1, Q2, S1, S2, W1, W2, M1, M2, - beta=1.0, tol=1e-8, max_iter=1000, random_state=None): - r""" - Compute the limit of a Nash linear quadratic dynamic game. In this - problem, player i minimizes - - .. math:: - \sum_{t=0}^{\infty} - \left\{ - x_t' r_i x_t + 2 x_t' w_i - u_{it} +u_{it}' q_i u_{it} + u_{jt}' s_i u_{jt} + 2 u_{jt}' - m_i u_{it} - \right\} - - subject to the law of motion - - .. math:: - x_{t+1} = A x_t + b_1 u_{1t} + b_2 u_{2t} - - and a perceived control law :math:`u_j(t) = - f_j x_t` for the other - player. - - The solution computed in this routine is the :math:`f_i` and - :math:`p_i` of the associated double optimal linear regulator - problem. - - Parameters - ---------- - A : scalar(float) or array_like(float) - Corresponds to the above equation, should be of size (n, n) - B1 : scalar(float) or array_like(float) - As above, size (n, k_1) - B2 : scalar(float) or array_like(float) - As above, size (n, k_2) - R1 : scalar(float) or array_like(float) - As above, size (n, n) - R2 : scalar(float) or array_like(float) - As above, size (n, n) - Q1 : scalar(float) or array_like(float) - As above, size (k_1, k_1) - Q2 : scalar(float) or array_like(float) - As above, size (k_2, k_2) - S1 : scalar(float) or array_like(float) - As above, size (k_1, k_1) - S2 : scalar(float) or array_like(float) - As above, size (k_2, k_2) - W1 : scalar(float) or array_like(float) - As above, size (n, k_1) - W2 : scalar(float) or array_like(float) - As above, size (n, k_2) - M1 : scalar(float) or array_like(float) - As above, size (k_2, k_1) - M2 : scalar(float) or array_like(float) - As above, size (k_1, k_2) - beta : scalar(float), optional(default=1.0) - Discount rate - tol : scalar(float), optional(default=1e-8) - This is the tolerance level for convergence - max_iter : scalar(int), optional(default=1000) - This is the maximum number of iteratiosn allowed - random_state : int or np.random.RandomState/Generator, optional - Random seed (integer) or np.random.RandomState or Generator - instance to set the initial state of the random number generator - for reproducibility. If None, a randomly initialized RandomState - is used. - - Returns - ------- - F1 : array_like, dtype=float, shape=(k_1, n) - Feedback law for agent 1 - F2 : array_like, dtype=float, shape=(k_2, n) - Feedback law for agent 2 - P1 : array_like, dtype=float, shape=(n, n) - The steady-state solution to the associated discrete matrix - Riccati equation for agent 1 - P2 : array_like, dtype=float, shape=(n, n) - The steady-state solution to the associated discrete matrix - Riccati equation for agent 2 - - """ - # == Unload parameters and make sure everything is an array == # - params = A, B1, B2, R1, R2, Q1, Q2, S1, S2, W1, W2, M1, M2 - params = map(np.asarray, params) - A, B1, B2, R1, R2, Q1, Q2, S1, S2, W1, W2, M1, M2 = params - - # == Multiply A, B1, B2 by sqrt(beta) to enforce discounting == # - A, B1, B2 = [np.sqrt(beta) * x for x in (A, B1, B2)] - - n = A.shape[0] - - if B1.ndim == 1: - k_1 = 1 - B1 = np.reshape(B1, (n, 1)) - else: - k_1 = B1.shape[1] - - if B2.ndim == 1: - k_2 = 1 - B2 = np.reshape(B2, (n, 1)) - else: - k_2 = B2.shape[1] - - random_state = check_random_state(random_state) - v1 = np.eye(k_1) - v2 = np.eye(k_2) - P1 = np.zeros((n, n)) - P2 = np.zeros((n, n)) - F1 = random_state.standard_normal((k_1, n)) - F2 = random_state.standard_normal((k_2, n)) - - for it in range(max_iter): - # update - F10 = F1 - F20 = F2 - - G2 = solve(np.dot(B2.T, P2.dot(B2))+Q2, v2) - G1 = solve(np.dot(B1.T, P1.dot(B1))+Q1, v1) - H2 = np.dot(G2, B2.T.dot(P2)) - H1 = np.dot(G1, B1.T.dot(P1)) - - # break up the computation of F1, F2 - F1_left = v1 - np.dot(H1.dot(B2)+G1.dot(M1.T), - H2.dot(B1)+G2.dot(M2.T)) - F1_right = H1.dot(A)+G1.dot(W1.T) - np.dot(H1.dot(B2)+G1.dot(M1.T), - H2.dot(A)+G2.dot(W2.T)) - F1 = solve(F1_left, F1_right) - F2 = H2.dot(A)+G2.dot(W2.T) - np.dot(H2.dot(B1)+G2.dot(M2.T), F1) - - Lambda1 = A - B2.dot(F2) - Lambda2 = A - B1.dot(F1) - Pi1 = R1 + np.dot(F2.T, S1.dot(F2)) - Pi2 = R2 + np.dot(F1.T, S2.dot(F1)) - - P1 = np.dot(Lambda1.T, P1.dot(Lambda1)) + Pi1 - \ - np.dot(np.dot(Lambda1.T, P1.dot(B1)) + W1 - F2.T.dot(M1), F1) - P2 = np.dot(Lambda2.T, P2.dot(Lambda2)) + Pi2 - \ - np.dot(np.dot(Lambda2.T, P2.dot(B2)) + W2 - F1.T.dot(M2), F2) - - dd = np.max(np.abs(F10 - F1)) + np.max(np.abs(F20 - F2)) - - if dd < tol: # success! - break - - else: - msg = 'No convergence: Iteration limit of {0} reached in nnash' - raise ValueError(msg.format(max_iter)) - - return F1, F2, P1, P2 diff --git a/quantecon/_lss.py b/quantecon/_lss.py deleted file mode 100644 index e3c3794f4..000000000 --- a/quantecon/_lss.py +++ /dev/null @@ -1,470 +0,0 @@ -""" -Computes quantities associated with the Gaussian linear state space model. - -References ----------- - -https://lectures.quantecon.org/py/linear_models.html - -""" - -from textwrap import dedent -import numpy as np -from scipy.linalg import solve -from ._matrix_eqn import solve_discrete_lyapunov -from numba import jit -from .util import check_random_state - - -@jit -def simulate_linear_model(A, x0, v, ts_length): - r""" - This is a separate function for simulating a vector linear system of - the form - - .. math:: - - x_{t+1} = A x_t + v_t - - given :math:`x_0` = x0 - - Here :math:`x_t` and :math:`v_t` are both n x 1 and :math:`A` is n x n. - - The purpose of separating this functionality out is to target it for - optimization by Numba. For the same reason, matrix multiplication is - broken down into for loops. - - Parameters - ---------- - A : array_like or scalar(float) - Should be n x n - x0 : array_like - Should be n x 1. Initial condition - v : np.ndarray - Should be n x ts_length-1. Its t-th column is used as the time t - shock :math:`v_t` - ts_length : int - The length of the time series - - Returns - ------- - x : np.ndarray - Time series with ts_length columns, the t-th column being :math:`x_t` - """ - A = np.asarray(A) - n = A.shape[0] - x = np.empty((n, ts_length)) - x[:, 0] = x0 - for t in range(ts_length-1): - # x[:, t+1] = A.dot(x[:, t]) + v[:, t] - for i in range(n): - x[i, t+1] = v[i, t] # Shock - for j in range(n): - x[i, t+1] += A[i, j] * x[j, t] # Dot Product - return x - - -class LinearStateSpace: - r""" - A class that describes a Gaussian linear state space model of the - form: - - .. math:: - - x_{t+1} = A x_t + C w_{t+1} - - y_t = G x_t + H v_t - - where :math:`{w_t}` and :math:`{v_t}` are independent and standard normal - with dimensions k and l respectively. The initial conditions are - :math:`\mu_0` and :math:`\Sigma_0` for :math:`x_0 \sim N(\mu_0, \Sigma_0)`. - When :math:`\Sigma_0=0`, the draw of :math:`x_0` is exactly :math:`\mu_0`. - - Parameters - ---------- - A : array_like or scalar(float) - Part of the state transition equation. It should be `n x n` - C : array_like or scalar(float) - Part of the state transition equation. It should be `n x m` - G : array_like or scalar(float) - Part of the observation equation. It should be `k x n` - H : array_like or scalar(float), optional(default=None) - Part of the observation equation. It should be `k x l` - mu_0 : array_like or scalar(float), optional(default=None) - This is the mean of initial draw and is `n x 1` - Sigma_0 : array_like or scalar(float), optional(default=None) - This is the variance of the initial draw and is `n x n` and - also should be positive definite and symmetric - - Attributes - ---------- - A, C, G, H, mu_0, Sigma_0 : see Parameters - n, k, m, l : scalar(int) - The dimensions of x_t, y_t, w_t and v_t respectively - - """ - - def __init__(self, A, C, G, H=None, mu_0=None, Sigma_0=None): - self.A, self.G, self.C = list(map(self.convert, (A, G, C))) - # = Check Input Shapes = # - ni, nj = self.A.shape - if ni != nj: - raise ValueError( - "Matrix A (shape: %s) needs to be square" % (self.A.shape, )) - if ni != self.C.shape[0]: - raise ValueError( - "Matrix C (shape: %s) does not have compatible dimensions " - "with A. It should be shape: %s" % (self.C.shape, (ni, 1))) - self.m = self.C.shape[1] - self.k, self.n = self.G.shape - if self.n != ni: - raise ValueError("Matrix G (shape: %s) does not have compatible" - "dimensions with A (%s)" % (self.G.shape, - self.A.shape)) - if H is None: - self.H = None - self.l = None - else: - self.H = self.convert(H) - self.l = self.H.shape[1] - if mu_0 is None: - self.mu_0 = np.zeros((self.n, 1)) - else: - self.mu_0 = self.convert(mu_0) - self.mu_0.shape = self.n, 1 - if Sigma_0 is None: - self.Sigma_0 = np.zeros((self.n, self.n)) - else: - self.Sigma_0 = self.convert(Sigma_0) - - def __repr__(self): - return self.__str__() - - def __str__(self): - m = """\ - Linear Gaussian state space model: - - dimension of state space : {n} - - number of innovations : {m} - - dimension of observation equation : {k} - """ - return dedent(m.format(n=self.n, k=self.k, m=self.m)) - - def convert(self, x): - """ - Convert array_like objects (lists of lists, floats, etc.) into - well formed 2D NumPy arrays - - """ - return np.atleast_2d(np.asarray(x, dtype='float')) - - def simulate(self, ts_length=100, random_state=None): - r""" - Simulate a time series of length ts_length, first drawing - - .. math:: - - x_0 \sim N(\mu_0, \Sigma_0) - - Parameters - ---------- - ts_length : scalar(int), optional(default=100) - The length of the simulation - random_state : int or np.random.RandomState/Generator, optional - Random seed (integer) or np.random.RandomState or Generator - instance to set the initial state of the random number - generator for reproducibility. If None, a randomly - initialized RandomState is used. - - Returns - ------- - x : array_like(float) - An n x ts_length array, where the t-th column is :math:`x_t` - y : array_like(float) - A k x ts_length array, where the t-th column is :math:`y_t` - - """ - random_state = check_random_state(random_state) - - x0 = random_state.multivariate_normal(self.mu_0.flatten(), - self.Sigma_0) - w = random_state.standard_normal((self.m, ts_length-1)) - v = self.C.dot(w) # Multiply each w_t by C to get v_t = C w_t - # == simulate time series == # - x = simulate_linear_model(self.A, x0, v, ts_length) - - if self.H is not None: - v = random_state.standard_normal((self.l, ts_length)) - y = self.G.dot(x) + self.H.dot(v) - else: - y = self.G.dot(x) - - return x, y - - def replicate(self, T=10, num_reps=100, random_state=None): - r""" - Simulate num_reps observations of :math:`x_T` and :math:`y_T` given - :math:`x_0 \sim N(\mu_0, \Sigma_0)`. - - Parameters - ---------- - T : scalar(int), optional(default=10) - The period that we want to replicate values for - num_reps : scalar(int), optional(default=100) - The number of replications that we want - random_state : int or np.random.RandomState/Generator, optional - Random seed (integer) or np.random.RandomState or Generator - instance to set the initial state of the random number - generator for reproducibility. If None, a randomly - initialized RandomState is used. - - Returns - ------- - x : array_like(float) - An n x num_reps array, where the j-th column is the j_th - observation of :math:`x_T` - - y : array_like(float) - A k x num_reps array, where the j-th column is the j_th - observation of :math:`y_T` - - """ - random_state = check_random_state(random_state) - - x = np.empty((self.n, num_reps)) - for j in range(num_reps): - x_T, _ = self.simulate(ts_length=T+1, random_state=random_state) - x[:, j] = x_T[:, -1] - if self.H is not None: - v = random_state.standard_normal((self.l, num_reps)) - y = self.G.dot(x) + self.H.dot(v) - else: - y = self.G.dot(x) - - return x, y - - def moment_sequence(self): - r""" - Create a generator to calculate the population mean and - variance-covariance matrix for both :math:`x_t` and :math:`y_t` - starting at the initial condition (self.mu_0, self.Sigma_0). - Each iteration produces a 4-tuple of items (mu_x, mu_y, Sigma_x, - Sigma_y) for the next period. - - Yields - ------ - mu_x : array_like(float) - An n x 1 array representing the population mean of x_t - mu_y : array_like(float) - A k x 1 array representing the population mean of y_t - Sigma_x : array_like(float) - An n x n array representing the variance-covariance matrix - of x_t - Sigma_y : array_like(float) - A k x k array representing the variance-covariance matrix - of y_t - - """ - # == Simplify names == # - A, C, G, H = self.A, self.C, self.G, self.H - # == Initial moments == # - mu_x, Sigma_x = self.mu_0, self.Sigma_0 - - while 1: - mu_y = G.dot(mu_x) - if H is None: - Sigma_y = G.dot(Sigma_x).dot(G.T) - else: - Sigma_y = G.dot(Sigma_x).dot(G.T) + H.dot(H.T) - - yield mu_x, mu_y, Sigma_x, Sigma_y - - # == Update moments of x == # - mu_x = A.dot(mu_x) - Sigma_x = A.dot(Sigma_x).dot(A.T) + C.dot(C.T) - - def stationary_distributions(self): - r""" - Compute the moments of the stationary distributions of :math:`x_t` and - :math:`y_t` if possible. Computation is by solving the discrete - Lyapunov equation. - - Returns - ------- - mu_x : array_like(float) - An n x 1 array representing the stationary mean of :math:`x_t` - mu_y : array_like(float) - An k x 1 array representing the stationary mean of :math:`y_t` - Sigma_x : array_like(float) - An n x n array representing the stationary var-cov matrix - of :math:`x_t` - Sigma_y : array_like(float) - An k x k array representing the stationary var-cov matrix - of :math:`y_t` - Sigma_yx : array_like(float) - An k x n array representing the stationary cov matrix - between :math:`y_t` and :math:`x_t`. - - """ - self.__partition() - num_const, sorted_idx = self.num_const, self.sorted_idx - A21, A22 = self.A21, self.A22 - CC2 = self.C2 @ self.C2.T - n = self.n - - if num_const > 0: - μ = solve(np.eye(n-num_const) - A22, A21) - else: - μ = solve(np.eye(n-num_const) - A22, np.zeros((n, 1))) - Σ = solve_discrete_lyapunov(A22, CC2, method='bartels-stewart') - - mu_x = np.empty((n, 1)) - mu_x[:num_const] = self.mu_0[sorted_idx[:num_const]] - mu_x[num_const:] = μ - - Sigma_x = np.zeros((n, n)) - Sigma_x[num_const:, num_const:] = Σ - - mu_x = self.P.T @ mu_x - Sigma_x = self.P.T @ Sigma_x @ self.P - - mu_y = self.G @ mu_x - Sigma_y = self.G @ Sigma_x @ self.G.T - if self.H is not None: - Sigma_y += self.H @ self.H.T - Sigma_yx = self.G @ Sigma_x - - self.mu_x, self.mu_y = mu_x, mu_y - self.Sigma_x, self.Sigma_y, self.Sigma_yx = Sigma_x, Sigma_y, Sigma_yx - - return mu_x, mu_y, Sigma_x, Sigma_y, Sigma_yx - - def geometric_sums(self, beta, x_t): - r""" - Forecast the geometric sums - - .. math:: - - S_x := E \Big[ \sum_{j=0}^{\infty} \beta^j x_{t+j} | x_t \Big] - - S_y := E \Big[ \sum_{j=0}^{\infty} \beta^j y_{t+j} | x_t \Big] - - Parameters - ---------- - beta : scalar(float) - Discount factor, in [0, 1) - - beta : array_like(float) - The term x_t for conditioning - - Returns - ------- - S_x : array_like(float) - Geometric sum as defined above - - S_y : array_like(float) - Geometric sum as defined above - - """ - - I = np.identity(self.n) - S_x = solve(I - beta * self.A, x_t) - S_y = self.G.dot(S_x) - - return S_x, S_y - - def impulse_response(self, j=5): - r""" - Pulls off the imuplse response coefficients to a shock - in :math:`w_{t}` for :math:`x` and :math:`y` - - Important to note: We are uninterested in the shocks to - v for this method - - * :math:`x` coefficients are :math:`C, AC, A^2 C...` - * :math:`y` coefficients are :math:`GC, GAC, GA^2C...` - - Parameters - ---------- - j : Scalar(int) - Number of coefficients that we want - - Returns - ------- - xcoef : list(array_like(float, 2)) - The coefficients for x - ycoef : list(array_like(float, 2)) - The coefficients for y - """ - # Pull out matrices - A, C, G, H = self.A, self.C, self.G, self.H - Apower = np.copy(A) - - # Create room for coefficients - xcoef = [C] - ycoef = [np.dot(G, C)] - - for i in range(j): - xcoef.append(np.dot(Apower, C)) - ycoef.append(np.dot(G, np.dot(Apower, C))) - Apower = np.dot(Apower, A) - - return xcoef, ycoef - - def __partition(self): - r""" - Reorder the states by shifting the constant terms to the - top of the state vector. Then partition the linear state - space model following Appendix C in RMT Ch2 such that the - A22 matrix associated with non-constant states have eigenvalues - all strictly smaller than 1. - - .. math:: - \left[\begin{array}{c} - const\\ - x_{2,t+1} - \end{array}\right]=\left[\begin{array}{cc} - I & 0\\ - A_{21} & A_{22} - \end{array}\right]\left[\begin{array}{c} - 1\\ - x_{2,t} - \end{array}\right]+\left[\begin{array}{c} - 0\\ - C_{2} - \end{array}\right]w_{t+1} - - Returns - ------- - A22 : array_like(float) - Lower right part of the reordered matrix A. - A21 : array_like(float) - Lower left part of the reordered matrix A. - """ - A, C = self.A, self.C - n = self.n - - sorted_idx = [] - A_diag = np.diag(A) - num_const = 0 - for idx in range(n): - if (A_diag[idx] == 1) and (C[idx, :] == 0).all() and \ - np.linalg.norm(A[idx, :]) == 1: - sorted_idx.insert(0, idx) - num_const += 1 - else: - sorted_idx.append(idx) - self.num_const = num_const - self.sorted_idx = sorted_idx - - P = np.zeros((n, n)) - P[range(n), sorted_idx] = 1 - - sorted_A = P @ A @ P.T - sorted_C = P @ C - A21 = sorted_A[num_const:, :num_const] - A22 = sorted_A[num_const:, num_const:] - C2 = sorted_C[num_const:, :] - - self.P, self.A21, self.A22, self.C2 = P, A21, A22, C2 - - return A21, A22 diff --git a/quantecon/_matrix_eqn.py b/quantecon/_matrix_eqn.py deleted file mode 100644 index 09466e268..000000000 --- a/quantecon/_matrix_eqn.py +++ /dev/null @@ -1,312 +0,0 @@ -""" -This file holds several functions that are used to solve matrix -equations. Currently has functionality to solve: - -* Lyapunov Equations -* Riccati Equations - -TODO: 1. See issue 47 on github repository, should add support for - Sylvester equations - 2. Fix warnings from checking conditioning of matrices -""" -import numpy as np -from numpy.linalg import solve -from scipy.linalg import solve_discrete_lyapunov as sp_solve_discrete_lyapunov -from scipy.linalg import solve_discrete_are as sp_solve_discrete_are - - -EPS = np.finfo(float).eps - - -def solve_discrete_lyapunov(A, B, max_it=50, method="doubling"): - r""" - Computes the solution to the discrete lyapunov equation - - .. math:: - - AXA' - X + B = 0 - - :math:`X` is computed by using a doubling algorithm. In particular, we - iterate to convergence on :math:`X_j` with the following recursions for - :math:`j = 1, 2, \dots` starting from :math:`X_0 = B`, :math:`a_0 = A`: - - .. math:: - - a_j = a_{j-1} a_{j-1} - - .. math:: - - X_j = X_{j-1} + a_{j-1} X_{j-1} a_{j-1}' - - Parameters - ---------- - A : array_like(float, ndim=2) - An n x n matrix as described above. We assume in order for - convergence that the eigenvalues of A have moduli bounded by - unity - B : array_like(float, ndim=2) - An n x n matrix as described above. We assume in order for - convergence that the eigenvalues of A have moduli bounded by - unity - max_it : scalar(int), optional(default=50) - The maximum number of iterations - method : string, optional(default="doubling") - Describes the solution method to use. If it is "doubling" then - uses the doubling algorithm to solve, if it is "bartels-stewart" - then it uses scipy's implementation of the Bartels-Stewart - approach. - - Returns - ------- - gamma1: array_like(float, ndim=2) - Represents the value :math:`X` - - """ - if method == "doubling": - A, B = list(map(np.atleast_2d, [A, B])) - alpha0 = A - gamma0 = B - - diff = 5 - n_its = 1 - - while diff > 1e-15: - - alpha1 = alpha0.dot(alpha0) - gamma1 = gamma0 + np.dot(alpha0.dot(gamma0), alpha0.conjugate().T) - - diff = np.max(np.abs(gamma1 - gamma0)) - alpha0 = alpha1 - gamma0 = gamma1 - - n_its += 1 - - if n_its > max_it: - msg = "Exceeded maximum iterations {}, check input matrics" - raise ValueError(msg.format(n_its)) - - elif method == "bartels-stewart": - gamma1 = sp_solve_discrete_lyapunov(A, B) - - else: - msg = "Check your method input. Should be doubling or bartels-stewart" - raise ValueError(msg) - - return gamma1 - - -def solve_discrete_riccati(A, B, Q, R, N=None, tolerance=1e-10, max_iter=500, - method="doubling"): - """ - Solves the discrete-time algebraic Riccati equation - - .. math:: - - X = A'XA - (N + B'XA)'(B'XB + R)^{-1}(N + B'XA) + Q - - Computation is via a modified structured doubling algorithm, an - explanation of which can be found in the reference below, if - `method="doubling"` (default), and via a QZ decomposition method by - calling `scipy.linalg.solve_discrete_are` if `method="qz"`. - - Parameters - ---------- - A : array_like(float, ndim=2) - k x k array. - B : array_like(float, ndim=2) - k x n array - Q : array_like(float, ndim=2) - k x k, should be symmetric and non-negative definite - R : array_like(float, ndim=2) - n x n, should be symmetric and positive definite - N : array_like(float, ndim=2) - n x k array - tolerance : scalar(float), optional(default=1e-10) - The tolerance level for convergence - max_iter : scalar(int), optional(default=500) - The maximum number of iterations allowed - method : string, optional(default="doubling") - Describes the solution method to use. If it is "doubling" then - uses the doubling algorithm to solve, if it is "qz" then it uses - `scipy.linalg.solve_discrete_are` (in which case `tolerance` and - `max_iter` are irrelevant). - - Returns - ------- - X : array_like(float, ndim=2) - The fixed point of the Riccati equation; a k x k array - representing the approximate solution - - References - ---------- - Chiang, Chun-Yueh, Hung-Yuan Fan, and Wen-Wei Lin. "STRUCTURED DOUBLING - ALGORITHM FOR DISCRETE-TIME ALGEBRAIC RICCATI EQUATIONS WITH SINGULAR - CONTROL WEIGHTING MATRICES." Taiwanese Journal of Mathematics 14, no. 3A - (2010): pp-935. - - """ - methods = ['doubling', 'qz'] - if method not in methods: - msg = "Check your method input. Should be {} or {}".format(*methods) - raise ValueError(msg) - - # == Set up == # - error = tolerance + 1 - fail_msg = "Convergence failed after {} iterations." - - # == Make sure that all array_likes are np arrays, two-dimensional == # - A, B, Q, R = np.atleast_2d(A, B, Q, R) - n, k = R.shape[0], Q.shape[0] - I = np.identity(k) - if N is None: - N = np.zeros((n, k)) - else: - N = np.atleast_2d(N) - - if method == 'qz': - X = sp_solve_discrete_are(A, B, Q, R, s=N.T) - return X - - # if method == 'doubling' - # == Choose optimal value of gamma in R_hat = R + gamma B'B == # - current_min = np.inf - candidates = (0.01, 0.1, 0.25, 0.5, 1.0, 2.0, 10.0, 100.0, 10e5) - BB = np.dot(B.T, B) - BTA = np.dot(B.T, A) - for gamma in candidates: - Z = R + gamma * BB - cn = np.linalg.cond(Z) - if cn * EPS < 1: - Q_tilde = - Q + np.dot(N.T, solve(Z, N + gamma * BTA)) + gamma * I - G0 = np.dot(B, solve(Z, B.T)) - A0 = np.dot(I - gamma * G0, A) - np.dot(B, solve(Z, N)) - H0 = gamma * np.dot(A.T, A0) - Q_tilde - f1 = np.linalg.cond(Z, np.inf) - f2 = gamma * f1 - f3 = np.linalg.cond(I + np.dot(G0, H0)) - f_gamma = max(f1, f2, f3) - if f_gamma < current_min: - best_gamma = gamma - current_min = f_gamma - - # == If no candidate successful then fail == # - if current_min == np.inf: - msg = "Unable to initialize routine due to ill conditioned arguments" - raise ValueError(msg) - - gamma = best_gamma - R_hat = R + gamma * BB - - # == Initial conditions == # - Q_tilde = - Q + np.dot(N.T, solve(R_hat, N + gamma * BTA)) + gamma * I - G0 = np.dot(B, solve(R_hat, B.T)) - A0 = np.dot(I - gamma * G0, A) - np.dot(B, solve(R_hat, N)) - H0 = gamma * np.dot(A.T, A0) - Q_tilde - i = 1 - - # == Main loop == # - while error > tolerance: - - if i > max_iter: - raise ValueError(fail_msg.format(i)) - - else: - A1 = np.dot(A0, solve(I + np.dot(G0, H0), A0)) - G1 = G0 + np.dot(np.dot(A0, G0), solve(I + np.dot(H0, G0), A0.T)) - H1 = H0 + np.dot(A0.T, solve(I + np.dot(H0, G0), np.dot(H0, A0))) - - error = np.max(np.abs(H1 - H0)) - A0 = A1 - G0 = G1 - H0 = H1 - i += 1 - - return H1 + gamma * I # Return X - - -def solve_discrete_riccati_system(Π, As, Bs, Cs, Qs, Rs, Ns, beta, - tolerance=1e-10, max_iter=1000): - """ - Solves the stacked system of algebraic matrix Riccati equations - in the Markov Jump linear quadratic control problems, by iterating - Ps matrices until convergence. - - Parameters - ---------- - Π : array_like(float, ndim=2) - The Markov chain transition matrix with dimension m x m. - As : array_like(float) - Consists of m state transition matrices A(s) with dimension - n x n for each Markov state s - Bs : array_like(float) - Consists of m state transition matrices B(s) with dimension - n x k for each Markov state s - Cs : array_like(float), optional(default=None) - Consists of m state transition matrices C(s) with dimension - n x j for each Markov state s. If the model is deterministic - then Cs should take default value of None - Qs : array_like(float) - Consists of m symmetric and non-negative definite payoff - matrices Q(s) with dimension k x k that corresponds with - the control variable u for each Markov state s - Rs : array_like(float) - Consists of m symmetric and non-negative definite payoff - matrices R(s) with dimension n x n that corresponds with - the state variable x for each Markov state s - Ns : array_like(float), optional(default=None) - Consists of m cross product term matrices N(s) with dimension - k x n for each Markov state, - beta : scalar(float), optional(default=1) - beta is the discount parameter - tolerance : scalar(float), optional(default=1e-10) - The tolerance level for convergence - max_iter : scalar(int), optional(default=500) - The maximum number of iterations allowed - - Returns - ------- - Ps : array_like(float, ndim=2) - The fixed point of the stacked system of algebraic matrix - Riccati equations, consists of m n x n P(s) matrices - - """ - m = Qs.shape[0] - k, n = Qs.shape[1], Rs.shape[1] - # Create the Ps matrices, initialize as identity matrix - Ps = np.array([np.eye(n) for i in range(m)]) - Ps1 = np.copy(Ps) - - # == Set up for iteration on Riccati equations system == # - error = tolerance + 1 - fail_msg = "Convergence failed after {} iterations." - - # == Prepare array for iteration == # - sum1, sum2 = np.empty((n, n)), np.empty((n, n)) - - # == Main loop == # - iteration = 0 - while error > tolerance: - - if iteration > max_iter: - raise ValueError(fail_msg.format(max_iter)) - - else: - error = 0 - for i in range(m): - # Initialize arrays - sum1[:, :] = 0. - sum2[:, :] = 0. - for j in range(m): - sum1 += beta * Π[i, j] * As[i].T @ Ps[j] @ As[i] - sum2 += Π[i, j] * \ - (beta * As[i].T @ Ps[j] @ Bs[i] + Ns[i].T) @ \ - solve(Qs[i] + beta * Bs[i].T @ Ps[j] @ Bs[i], - beta * Bs[i].T @ Ps[j] @ As[i] + Ns[i]) - - Ps1[i][:, :] = Rs[i] + sum1 - sum2 - error += np.max(np.abs(Ps1[i] - Ps[i])) - - Ps[:, :, :] = Ps1[:, :, :] - iteration += 1 - - return Ps diff --git a/quantecon/_quadsums.py b/quantecon/_quadsums.py deleted file mode 100644 index d58fa739f..000000000 --- a/quantecon/_quadsums.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -This module provides functions to compute quadratic sums of the form described -in the docstrings. - -""" - - -import numpy as np -import scipy.linalg -from ._matrix_eqn import solve_discrete_lyapunov - - -def var_quadratic_sum(A, C, H, beta, x0): - r""" - Computes the expected discounted quadratic sum - - .. math:: - - q(x_0) = \mathbb{E} \Big[ \sum_{t=0}^{\infty} \beta^t x_t' H x_t \Big] - - Here :math:`{x_t}` is the VAR process :math:`x_{t+1} = A x_t + C w_t` - with :math:`{x_t}` standard normal and :math:`x_0` the initial condition. - - Parameters - ---------- - A : array_like(float, ndim=2) - The matrix described above in description. Should be n x n - C : array_like(float, ndim=2) - The matrix described above in description. Should be n x n - H : array_like(float, ndim=2) - The matrix described above in description. Should be n x n - beta: scalar(float) - Should take a value in (0, 1) - x_0: array_like(float, ndim=1) - The initial condtion. A conformable array (of length n, or with - n rows) - - Returns - ------- - q0: scalar(float) - Represents the value :math:`q(x_0)` - - Remarks: The formula for computing :math:`q(x_0)` is - :math:`q(x_0) = x_0' Q x_0 + v` - where - - * :math:`Q` is the solution to :math:`Q = H + \beta A' Q A`, and - * :math:`v = \frac{trace(C' Q C) \beta}{(1 - \beta)}` - - """ - # == Make sure that A, C, H and x0 are array_like == # - - A, C, H = list(map(np.atleast_2d, (A, C, H))) - x0 = np.atleast_1d(x0) - # == Start computations == # - Q = scipy.linalg.solve_discrete_lyapunov(np.sqrt(beta) * A.T, H) - cq = np.dot(np.dot(C.T, Q), C) - v = np.trace(cq) * beta / (1 - beta) - q0 = np.dot(np.dot(x0.T, Q), x0) + v - - return q0 - - -def m_quadratic_sum(A, B, max_it=50): - r""" - Computes the quadratic sum - - .. math:: - - V = \sum_{j=0}^{\infty} A^j B A^{j'} - - V is computed by solving the corresponding discrete lyapunov - equation using the doubling algorithm. See the documentation of - `util.solve_discrete_lyapunov` for more information. - - Parameters - ---------- - A : array_like(float, ndim=2) - An n x n matrix as described above. We assume in order for - convergence that the eigenvalues of :math:`A` have moduli bounded by - unity - B : array_like(float, ndim=2) - An n x n matrix as described above. We assume in order for - convergence that the eigenvalues of :math:`A` have moduli bounded by - unity - max_it : scalar(int), optional(default=50) - The maximum number of iterations - - Returns - ------- - gamma1: array_like(float, ndim=2) - Represents the value :math:`V` - - """ - - gamma1 = solve_discrete_lyapunov(A, B, max_it) - - return gamma1 diff --git a/quantecon/_rank_nullspace.py b/quantecon/_rank_nullspace.py deleted file mode 100644 index 6eb553c41..000000000 --- a/quantecon/_rank_nullspace.py +++ /dev/null @@ -1,95 +0,0 @@ -import numpy as np -from numpy.linalg import svd - - -def rank_est(A, atol=1e-13, rtol=0): - """ - Estimate the rank (i.e. the dimension of the nullspace) of a matrix. - - The algorithm used by this function is based on the singular value - decomposition of `A`. - - Parameters - ---------- - A : array_like(float, ndim=1 or 2) - A should be at most 2-D. A 1-D array with length n will be - treated as a 2-D with shape (1, n) - atol : scalar(float), optional(default=1e-13) - The absolute tolerance for a zero singular value. Singular - values smaller than `atol` are considered to be zero. - rtol : scalar(float), optional(default=0) - The relative tolerance. Singular values less than rtol*smax are - considered to be zero, where smax is the largest singular value. - - Returns - ------- - r : scalar(int) - The estimated rank of the matrix. - - Note: If both `atol` and `rtol` are positive, the combined tolerance - is the maximum of the two; that is: - - tol = max(atol, rtol * smax) - - Note: Singular values smaller than `tol` are considered to be zero. - - See also - -------- - numpy.linalg.matrix_rank - matrix_rank is basically the same as this function, but it does - not provide the option of the absolute tolerance. - - """ - - A = np.atleast_2d(A) - s = svd(A, compute_uv=False) - tol = max(atol, rtol * s[0]) - rank = int((s >= tol).sum()) - - return rank - - -def nullspace(A, atol=1e-13, rtol=0): - """ - Compute an approximate basis for the nullspace of A. - - The algorithm used by this function is based on the singular value - decomposition of `A`. - - Parameters - ---------- - A : array_like(float, ndim=1 or 2) - A should be at most 2-D. A 1-D array with length k will be - treated as a 2-D with shape (1, k) - atol : scalar(float), optional(default=1e-13) - The absolute tolerance for a zero singular value. Singular - values smaller than `atol` are considered to be zero. - rtol : scalar(float), optional(default=0) - The relative tolerance. Singular values less than rtol*smax are - considered to be zero, where smax is the largest singular value. - - Returns - ------- - ns : array_like(float, ndim=2) - If `A` is an array with shape (m, k), then `ns` will be an array - with shape (k, n), where n is the estimated dimension of the - nullspace of `A`. The columns of `ns` are a basis for the - nullspace; each element in numpy.dot(A, ns) will be - approximately zero. - - Note: If both `atol` and `rtol` are positive, the combined tolerance - is the maximum of the two; that is: - - tol = max(atol, rtol * smax) - - Note: Singular values smaller than `tol` are considered to be zero. - - """ - - A = np.atleast_2d(A) - u, s, vh = svd(A) - tol = max(atol, rtol * s[0]) - nnz = (s >= tol).sum() - ns = vh[nnz:].conj().T - - return ns diff --git a/quantecon/_robustlq.py b/quantecon/_robustlq.py deleted file mode 100644 index 86dd91fb5..000000000 --- a/quantecon/_robustlq.py +++ /dev/null @@ -1,400 +0,0 @@ -""" -Solves robust LQ control problems. - -""" -from textwrap import dedent -import numpy as np -from ._lqcontrol import LQ -from ._quadsums import var_quadratic_sum -from scipy.linalg import solve, inv, det -from ._matrix_eqn import solve_discrete_lyapunov - - -class RBLQ: - r""" - Provides methods for analysing infinite horizon robust LQ control - problems of the form - - .. math:: - - \min_{u_t} \sum_t \beta^t {x_t' R x_t + u_t' Q u_t } - - subject to - - .. math:: - - x_{t+1} = A x_t + B u_t + C w_{t+1} - - and with model misspecification parameter theta. - - Parameters - ---------- - Q : array_like(float, ndim=2) - The cost(payoff) matrix for the controls. See above for more. - Q should be k x k and symmetric and positive definite - R : array_like(float, ndim=2) - The cost(payoff) matrix for the state. See above for more. R - should be n x n and symmetric and non-negative definite - A : array_like(float, ndim=2) - The matrix that corresponds with the state in the state space - system. A should be n x n - B : array_like(float, ndim=2) - The matrix that corresponds with the control in the state space - system. B should be n x k - C : array_like(float, ndim=2) - The matrix that corresponds with the random process in the - state space system. C should be n x j - beta : scalar(float) - The discount factor in the robust control problem - theta : scalar(float) - The robustness factor in the robust control problem - - Attributes - ---------- - Q, R, A, B, C, beta, theta : see Parameters - k, n, j : scalar(int) - The dimensions of the matrices - - """ - - def __init__(self, Q, R, A, B, C, beta, theta): - - # == Make sure all matrices can be treated as 2D arrays == # - A, B, C, Q, R = list(map(np.atleast_2d, (A, B, C, Q, R))) - self.A, self.B, self.C, self.Q, self.R = A, B, C, Q, R - # == Record dimensions == # - self.k = self.Q.shape[0] - self.n = self.R.shape[0] - self.j = self.C.shape[1] - # == Remaining parameters == # - self.beta, self.theta = beta, theta - # == Check for case of no control (pure forecasting problem) == # - self.pure_forecasting = True if not Q.any() and not B.any() else False - - def __repr__(self): - return self.__str__() - - def __str__(self): - m = """\ - Robust linear quadratic control system - - beta (discount parameter) : {b} - - theta (robustness factor) : {th} - - n (number of state variables) : {n} - - k (number of control variables) : {k} - - j (number of shocks) : {j} - """ - return dedent(m.format(b=self.beta, n=self.n, k=self.k, j=self.j, - th=self.theta)) - - def d_operator(self, P): - r""" - The D operator, mapping P into - - .. math:: - - D(P) := P + PC(\theta I - C'PC)^{-1} C'P. - - Parameters - ---------- - P : array_like(float, ndim=2) - A matrix that should be n x n - - Returns - ------- - dP : array_like(float, ndim=2) - The matrix P after applying the D operator - - """ - C, theta = self.C, self.theta - I = np.identity(self.j) - S1 = np.dot(P, C) - S2 = np.dot(C.T, S1) - - dP = P + np.dot(S1, solve(theta * I - S2, S1.T)) - - return dP - - def b_operator(self, P): - r""" - The B operator, mapping P into - - .. math:: - - B(P) := R - \beta^2 A'PB(Q + \beta B'PB)^{-1}B'PA + \beta A'PA - - and also returning - - .. math:: - - F := (Q + \beta B'PB)^{-1} \beta B'PA - - Parameters - ---------- - P : array_like(float, ndim=2) - A matrix that should be n x n - - Returns - ------- - F : array_like(float, ndim=2) - The F matrix as defined above - new_p : array_like(float, ndim=2) - The matrix P after applying the B operator - - """ - A, B, Q, R, beta = self.A, self.B, self.Q, self.R, self.beta - S1 = Q + beta * np.dot(B.T, np.dot(P, B)) - S2 = beta * np.dot(B.T, np.dot(P, A)) - S3 = beta * np.dot(A.T, np.dot(P, A)) - F = solve(S1, S2) if not self.pure_forecasting else np.zeros( - (self.k, self.n)) - new_P = R - np.dot(S2.T, F) + S3 - - return F, new_P - - def robust_rule(self, method='doubling'): - """ - This method solves the robust control problem by tricking it - into a stacked LQ problem, as described in chapter 2 of Hansen- - Sargent's text "Robustness." The optimal control with observed - state is - - .. math:: - - u_t = - F x_t - - And the value function is :math:`-x'Px` - - Parameters - ---------- - method : str, optional(default='doubling') - Solution method used in solving the associated Riccati - equation, str in {'doubling', 'qz'}. - - Returns - ------- - F : array_like(float, ndim=2) - The optimal control matrix from above - P : array_like(float, ndim=2) - The positive semi-definite matrix defining the value - function - K : array_like(float, ndim=2) - the worst-case shock matrix K, where - :math:`w_{t+1} = K x_t` is the worst case shock - - """ - # == Simplify names == # - A, B, C, Q, R = self.A, self.B, self.C, self.Q, self.R - beta, theta = self.beta, self.theta - k, j = self.k, self.j - # == Set up LQ version == # - I = np.identity(j) - Z = np.zeros((k, j)) - - if self.pure_forecasting: - lq = LQ(-beta*I*theta, R, A, C, beta=beta) - - # == Solve and convert back to robust problem == # - P, f, d = lq.stationary_values(method=method) - F = np.zeros((self.k, self.n)) - K = -f[:k, :] - - else: - Ba = np.hstack([B, C]) - Qa = np.vstack([np.hstack([Q, Z]), np.hstack([Z.T, -beta*I*theta])]) - lq = LQ(Qa, R, A, Ba, beta=beta) - - # == Solve and convert back to robust problem == # - P, f, d = lq.stationary_values(method=method) - F = f[:k, :] - K = -f[k:f.shape[0], :] - - return F, K, P - - def robust_rule_simple(self, P_init=None, max_iter=80, tol=1e-8): - """ - A simple algorithm for computing the robust policy F and the - corresponding value function P, based around straightforward - iteration with the robust Bellman operator. This function is - easier to understand but one or two orders of magnitude slower - than self.robust_rule(). For more information see the docstring - of that method. - - Parameters - ---------- - P_init : array_like(float, ndim=2), optional(default=None) - The initial guess for the value function matrix. It will - be a matrix of zeros if no guess is given - max_iter : scalar(int), optional(default=80) - The maximum number of iterations that are allowed - tol : scalar(float), optional(default=1e-8) - The tolerance for convergence - - Returns - ------- - F : array_like(float, ndim=2) - The optimal control matrix from above - P : array_like(float, ndim=2) - The positive semi-definite matrix defining the value - function - K : array_like(float, ndim=2) - the worst-case shock matrix K, where - :math:`w_{t+1} = K x_t` is the worst case shock - - """ - # == Simplify names == # - A, B, C, Q, R = self.A, self.B, self.C, self.Q, self.R - beta, theta = self.beta, self.theta - # == Set up loop == # - P = np.zeros((self.n, self.n)) if P_init is None else P_init - iterate, e = 0, tol + 1 - while iterate < max_iter and e > tol: - F, new_P = self.b_operator(self.d_operator(P)) - e = np.sqrt(np.sum((new_P - P)**2)) - iterate += 1 - P = new_P - I = np.identity(self.j) - S1 = P.dot(C) - S2 = C.T.dot(S1) - K = inv(theta * I - S2).dot(S1.T).dot(A - B.dot(F)) - - return F, K, P - - def F_to_K(self, F, method='doubling'): - """ - Compute agent 2's best cost-minimizing response K, given F. - - Parameters - ---------- - F : array_like(float, ndim=2) - A k x n array - method : str, optional(default='doubling') - Solution method used in solving the associated Riccati - equation, str in {'doubling', 'qz'}. - - Returns - ------- - K : array_like(float, ndim=2) - Agent's best cost minimizing response for a given F - P : array_like(float, ndim=2) - The value function for a given F - - """ - Q2 = self.beta * self.theta - R2 = - self.R - np.dot(F.T, np.dot(self.Q, F)) - A2 = self.A - np.dot(self.B, F) - B2 = self.C - lq = LQ(Q2, R2, A2, B2, beta=self.beta) - neg_P, neg_K, d = lq.stationary_values(method=method) - - return -neg_K, -neg_P - - def K_to_F(self, K, method='doubling'): - """ - Compute agent 1's best value-maximizing response F, given K. - - Parameters - ---------- - K : array_like(float, ndim=2) - A j x n array - method : str, optional(default='doubling') - Solution method used in solving the associated Riccati - equation, str in {'doubling', 'qz'}. - - Returns - ------- - F : array_like(float, ndim=2) - The policy function for a given K - P : array_like(float, ndim=2) - The value function for a given K - - """ - A1 = self.A + np.dot(self.C, K) - B1 = self.B - Q1 = self.Q - R1 = self.R - self.beta * self.theta * np.dot(K.T, K) - lq = LQ(Q1, R1, A1, B1, beta=self.beta) - P, F, d = lq.stationary_values(method=method) - - return F, P - - def compute_deterministic_entropy(self, F, K, x0): - r""" - - Given K and F, compute the value of deterministic entropy, which - is - - .. math:: - - \sum_t \beta^t x_t' K'K x_t` - - with - - .. math:: - - x_{t+1} = (A - BF + CK) x_t - - Parameters - ---------- - F : array_like(float, ndim=2) - The policy function, a k x n array - K : array_like(float, ndim=2) - The worst case matrix, a j x n array - x0 : array_like(float, ndim=1) - The initial condition for state - - Returns - ------- - e : scalar(int) - The deterministic entropy - - """ - H0 = np.dot(K.T, K) - C0 = np.zeros((self.n, 1)) - A0 = self.A - np.dot(self.B, F) + np.dot(self.C, K) - e = var_quadratic_sum(A0, C0, H0, self.beta, x0) - - return e - - def evaluate_F(self, F): - """ - Given a fixed policy F, with the interpretation :math:`u = -F x`, this - function computes the matrix :math:`P_F` and constant :math:`d_F` - associated with discounted cost :math:`J_F(x) = x' P_F x + d_F` - - Parameters - ---------- - F : array_like(float, ndim=2) - The policy function, a k x n array - - Returns - ------- - P_F : array_like(float, ndim=2) - Matrix for discounted cost - d_F : scalar(float) - Constant for discounted cost - K_F : array_like(float, ndim=2) - Worst case policy - O_F : array_like(float, ndim=2) - Matrix for discounted entropy - o_F : scalar(float) - Constant for discounted entropy - - """ - # == Simplify names == # - Q, R, A, B, C = self.Q, self.R, self.A, self.B, self.C - beta, theta = self.beta, self.theta - - # == Solve for policies and costs using agent 2's problem == # - K_F, P_F = self.F_to_K(F) - I = np.identity(self.j) - H = inv(I - C.T.dot(P_F.dot(C)) / theta) - d_F = np.log(det(H)) - - # == Compute O_F and o_F == # - AO = np.sqrt(beta) * (A - np.dot(B, F) + np.dot(C, K_F)) - O_F = solve_discrete_lyapunov(AO.T, beta * np.dot(K_F.T, K_F)) - ho = (np.trace(H - 1) - d_F) / 2.0 - tr = np.trace(np.dot(O_F, C.dot(H.dot(C.T)))) - o_F = (ho + beta * tr) / (1 - beta) - - return K_F, P_F, d_F, O_F, o_F diff --git a/quantecon/arma.py b/quantecon/arma.py index 5065f3fd6..be0f5699f 100644 --- a/quantecon/arma.py +++ b/quantecon/arma.py @@ -1,27 +1,258 @@ -# This file is not meant for public use and will be removed v0.8.0. -# Use the `quantecon` namespace for importing the objects -# included below. +""" +Provides functions for working with and visualizing scalar ARMA processes. -import warnings -from . import _arma +TODO: 1. Fix warnings concerning casting complex variables back to floats +""" +import numpy as np +from .util import check_random_state -__all__ = ['ARMA'] +class ARMA: + r""" + This class represents scalar ARMA(p, q) processes. -def __dir__(): - return __all__ + If phi and theta are scalars, then the model is + understood to be + .. math:: -def __getattr__(name): - if name not in __all__: - raise AttributeError( - f"`quantecon.arma` is deprecated and has no attribute '{name}'." - ) + X_t = \phi X_{t-1} + \epsilon_t + \theta \epsilon_{t-1} - warnings.warn(f"Please use `{name}` from the `quantecon` namespace, " - "the `quantecon.arma` namespace is deprecated. You can use " - f"the following instead:\n `from quantecon import {name}`.", - category=DeprecationWarning, stacklevel=2) + where :math:`\epsilon_t` is a white noise process with standard + deviation :math:`\sigma`. If phi and theta are arrays or sequences, + then the interpretation is the ARMA(p, q) model - return getattr(_arma, name) + .. math:: + + X_t = \phi_1 X_{t-1} + ... + \phi_p X_{t-p} + + + \epsilon_t + \theta_1 \epsilon_{t-1} + ... + + \theta_q \epsilon_{t-q} + + where + + * :math:`\phi = (\phi_1, \phi_2,..., \phi_p)` + * :math:`\theta = (\theta_1, \theta_2,..., \theta_q)` + * :math:`\sigma` is a scalar, the standard deviation of the + white noise + + Parameters + ---------- + phi : scalar or iterable or array_like(float) + Autocorrelation values for the autocorrelated variable. + See above for explanation. + theta : scalar or iterable or array_like(float) + Autocorrelation values for the white noise of the model. + See above for explanation + sigma : scalar(float) + The standard deviation of the white noise + + Attributes + ---------- + phi, theta, sigma : see Parmeters + ar_poly : array_like(float) + The polynomial form that is needed by scipy.signal to do the + processing we desire. Corresponds with the phi values + ma_poly : array_like(float) + The polynomial form that is needed by scipy.signal to do the + processing we desire. Corresponds with the theta values + + """ + def __init__(self, phi, theta=0, sigma=1): + self._phi, self._theta = phi, theta + self.sigma = sigma + self.set_params() + + def __repr__(self): + m = "ARMA(phi=%s, theta=%s, sigma=%s)" + return m % (self.phi, self.theta, self.sigma) + + def __str__(self): + m = "An ARMA({p}, {q}) process" + p = np.asarray(self.phi).size + q = np.asarray(self.theta).size + return m.format(p=p, q=q) + + # Special latex print method for working in notebook + def _repr_latex_(self): + m = r"$X_t = " + phi = np.atleast_1d(self.phi) + theta = np.atleast_1d(self.theta) + rhs = "" + for (tm, phi_p) in enumerate(phi): + # don't include terms if they are equal to zero + if abs(phi_p) > 1e-12: + rhs += r"%+g X_{t-%i}" % (phi_p, tm+1) + + if rhs[0] == "+": + rhs = rhs[1:] # remove initial `+` if phi_1 was positive + + rhs += r" + \epsilon_t" + + for (tm, th_q) in enumerate(theta): + # don't include terms if they are equal to zero + if abs(th_q) > 1e-12: + rhs += r"%+g \epsilon_{t-%i}" % (th_q, tm+1) + + return m + rhs + "$" + + @property + def phi(self): + return self._phi + + @phi.setter + def phi(self, new_value): + self._phi = new_value + self.set_params() + + @property + def theta(self): + return self._theta + + @theta.setter + def theta(self, new_value): + self._theta = new_value + self.set_params() + + def set_params(self): + r""" + Internally, scipy.signal works with systems of the form + + .. math:: + + ar_{poly}(L) X_t = ma_{poly}(L) \epsilon_t + + where L is the lag operator. To match this, we set + + .. math:: + + ar_{poly} = (1, -\phi_1, -\phi_2,..., -\phi_p) + + ma_{poly} = (1, \theta_1, \theta_2,..., \theta_q) + + In addition, ar_poly must be at least as long as ma_poly. + This can be achieved by padding it out with zeros when required. + + """ + # === set up ma_poly === # + ma_poly = np.asarray(self._theta) + self.ma_poly = np.insert(ma_poly, 0, 1) # The array (1, theta) + + # === set up ar_poly === # + if np.isscalar(self._phi): + ar_poly = np.array(-self._phi) + else: + ar_poly = -np.asarray(self._phi) + self.ar_poly = np.insert(ar_poly, 0, 1) # The array (1, -phi) + + # === pad ar_poly with zeros if required === # + if len(self.ar_poly) < len(self.ma_poly): + temp = np.zeros(len(self.ma_poly) - len(self.ar_poly)) + self.ar_poly = np.hstack((self.ar_poly, temp)) + + def impulse_response(self, impulse_length=30): + """ + Get the impulse response corresponding to our model. + + Returns + ------- + psi : array_like(float) + psi[j] is the response at lag j of the impulse response. + We take psi[0] as unity. + + """ + from scipy.signal import dimpulse + sys = self.ma_poly, self.ar_poly, 1 + times, psi = dimpulse(sys, n=impulse_length) + psi = psi[0].flatten() # Simplify return value into flat array + + return psi + + def spectral_density(self, two_pi=True, res=1200): + r""" + Compute the spectral density function. The spectral density is + the discrete time Fourier transform of the autocovariance + function. In particular, + + .. math:: + + f(w) = \sum_k \gamma(k) \exp(-ikw) + + where gamma is the autocovariance function and the sum is over + the set of all integers. + + Parameters + ---------- + two_pi : Boolean, optional + Compute the spectral density function over :math:`[0, \pi]` if + two_pi is False and :math:`[0, 2 \pi]` otherwise. Default value is + True + res : scalar or array_like(int), optional(default=1200) + If res is a scalar then the spectral density is computed at + `res` frequencies evenly spaced around the unit circle, but + if res is an array then the function computes the response + at the frequencies given by the array + + Returns + ------- + w : array_like(float) + The normalized frequencies at which h was computed, in + radians/sample + spect : array_like(float) + The frequency response + + """ + from scipy.signal import freqz + w, h = freqz(self.ma_poly, self.ar_poly, worN=res, whole=two_pi) + spect = h * np.conj(h) * self.sigma**2 + + return w, spect + + def autocovariance(self, num_autocov=16): + """ + Compute the autocovariance function from the ARMA parameters + over the integers range(num_autocov) using the spectral density + and the inverse Fourier transform. + + Parameters + ---------- + num_autocov : scalar(int), optional(default=16) + The number of autocovariances to calculate + + """ + spect = self.spectral_density()[1] + acov = np.fft.ifft(spect).real + + # num_autocov should be <= len(acov) / 2 + return acov[:num_autocov] + + def simulation(self, ts_length=90, random_state=None): + """ + Compute a simulated sample path assuming Gaussian shocks. + + Parameters + ---------- + ts_length : scalar(int), optional(default=90) + Number of periods to simulate for + + random_state : int or np.random.RandomState/Generator, optional + Random seed (integer) or np.random.RandomState or Generator + instance to set the initial state of the random number + generator for reproducibility. If None, a randomly + initialized RandomState is used. + + Returns + ------- + vals : array_like(float) + A simulation of the model that corresponds to this class + + """ + from scipy.signal import dlsim + random_state = check_random_state(random_state) + + sys = self.ma_poly, self.ar_poly, 1 + u = random_state.standard_normal((ts_length, 1)) * self.sigma + vals = dlsim(sys, u)[1] + + return vals.flatten() diff --git a/quantecon/ce_util.py b/quantecon/ce_util.py index 0d5b03ad8..314e311b3 100644 --- a/quantecon/ce_util.py +++ b/quantecon/ce_util.py @@ -1,29 +1,123 @@ -# This file is not meant for public use and will be removed v0.8.0. -# Use the `quantecon` namespace for importing the objects -# included below. +""" +Utility functions used in CompEcon -import warnings -from . import _ce_util +Based routines found in the CompEcon toolbox by Miranda and Fackler. +References +---------- +Miranda, Mario J, and Paul L Fackler. Applied Computational Economics +and Finance, MIT Press, 2002. -__all__ = ['ckron', 'gridmake'] +""" +from functools import reduce +import numpy as np -def __dir__(): - return __all__ +def ckron(*arrays): + """ + Repeatedly applies the np.kron function to an arbitrary number of + input arrays + Parameters + ---------- + *arrays : tuple/list of np.ndarray -def __getattr__(name): - if name not in __all__: - raise AttributeError( - "`quantecon.ce_util` is deprecated and has no attribute " - f"'{name}'." - ) + Returns + ------- + out : np.ndarray + The result of repeated kronecker products. - warnings.warn(f"Please use `{name}` from the `quantecon` namespace, " - "the `quantecon.ce_util` namespace is deprecated. You can " - "use the following instead:\n " - f"`from quantecon import {name}`.", - category=DeprecationWarning, stacklevel=2) + Notes + ----- + Based of original function `ckron` in CompEcon toolbox by Miranda + and Fackler. - return getattr(_ce_util, name) + References + ---------- + Miranda, Mario J, and Paul L Fackler. Applied Computational + Economics and Finance, MIT Press, 2002. + + """ + return reduce(np.kron, arrays) + + +def gridmake(*arrays): + """ + Expands one or more vectors (or matrices) into a matrix where rows span the + cartesian product of combinations of the input arrays. Each column of the + input arrays will correspond to one column of the output matrix. + + Parameters + ---------- + *arrays : tuple/list of np.ndarray + Tuple/list of vectors to be expanded. + + Returns + ------- + out : np.ndarray + The cartesian product of combinations of the input arrays. + + Notes + ----- + Based of original function ``gridmake`` in CompEcon toolbox by + Miranda and Fackler + + References + ---------- + Miranda, Mario J, and Paul L Fackler. Applied Computational Economics + and Finance, MIT Press, 2002. + + """ + if all([i.ndim == 1 for i in arrays]): + d = len(arrays) + if d == 2: + out = _gridmake2(*arrays) + else: + out = _gridmake2(arrays[0], arrays[1]) + for arr in arrays[2:]: + out = _gridmake2(out, arr) + + return out + else: + raise NotImplementedError("Come back here") + + +def _gridmake2(x1, x2): + """ + Expands two vectors (or matrices) into a matrix where rows span the + cartesian product of combinations of the input arrays. Each column of the + input arrays will correspond to one column of the output matrix. + + Parameters + ---------- + x1 : np.ndarray + First vector to be expanded. + + x2 : np.ndarray + Second vector to be expanded. + + Returns + ------- + out : np.ndarray + The cartesian product of combinations of the input arrays. + + Notes + ----- + Based of original function ``gridmake2`` in CompEcon toolbox by + Miranda and Fackler. + + References + ---------- + Miranda, Mario J, and Paul L Fackler. Applied Computational Economics + and Finance, MIT Press, 2002. + + """ + if x1.ndim == 1 and x2.ndim == 1: + return np.column_stack([np.tile(x1, x2.shape[0]), + np.repeat(x2, x1.shape[0])]) + elif x1.ndim > 1 and x2.ndim == 1: + first = np.tile(x1, (x2.shape[0], 1)) + second = np.repeat(x2, x1.shape[0]) + return np.column_stack([first, second]) + else: + raise NotImplementedError("Come back here") diff --git a/quantecon/compute_fp.py b/quantecon/compute_fp.py index 99d62c410..aec0727ec 100644 --- a/quantecon/compute_fp.py +++ b/quantecon/compute_fp.py @@ -1,29 +1,367 @@ -# This file is not meant for public use and will be removed v0.8.0. -# Use the `quantecon` namespace for importing the objects -# included below. +""" +Compute an approximate fixed point of a given operator T, starting from +specified initial condition v. +""" +import time import warnings -from . import _compute_fp +import numpy as np +from numba import jit, generated_jit, types +from .game_theory.lemke_howson import _lemke_howson_tbl, _get_mixed_actions -__all__ = ['compute_fixed_point'] +def _print_after_skip(skip, it=None, dist=None, etime=None): + if it is None: + # print initial header + msg = "{i:<13}{d:<15}{t:<17}".format(i="Iteration", + d="Distance", + t="Elapsed (seconds)") + print(msg) + print("-" * len(msg)) + return -def __dir__(): - return __all__ + if it % skip == 0: + if etime is None: + print("After {it} iterations dist is {d}".format(it=it, d=dist)) + else: + # leave 4 spaces between columns if we have %3.3e in d, t + msg = "{i:<13}{d:<15.3e}{t:<18.3e}" + print(msg.format(i=it, d=dist, t=etime)) -def __getattr__(name): - if name not in __all__: - raise AttributeError( - "`quantecon.compute_fp` is deprecated and has no attribute " - f"'{name}'." - ) + return - warnings.warn(f"Please use `{name}` from the `quantecon` namespace, " - "the `quantecon.compute_fp` namespace is deprecated. You " - "can use the following instead:\n " - f"`from quantecon import {name}`.", - category=DeprecationWarning, stacklevel=2) - return getattr(_compute_fp, name) +_convergence_msg = 'Converged in {iterate} steps' +_non_convergence_msg = \ + 'max_iter attained before convergence in compute_fixed_point' + + +def _is_approx_fp(T, v, error_tol, *args, **kwargs): + error = np.max(np.abs(T(v, *args, **kwargs) - v)) + return error <= error_tol + + +def compute_fixed_point(T, v, error_tol=1e-3, max_iter=50, verbose=2, + print_skip=5, method='iteration', *args, **kwargs): + r""" + Computes and returns an approximate fixed point of the function `T`. + + The default method `'iteration'` simply iterates the function given + an initial condition `v` and returns :math:`T^k v` when the + condition :math:`\lVert T^k v - T^{k-1} v\rVert \leq + \mathrm{error\_tol}` is satisfied or the number of iterations + :math:`k` reaches `max_iter`. Provided that `T` is a contraction + mapping or similar, :math:`T^k v` will be an approximation to the + fixed point. + + The method `'imitation_game'` uses the "imitation game algorithm" + developed by McLennan and Tourky [1]_, which internally constructs + a sequence of two-player games called imitation games and utilizes + their Nash equilibria, computed by the Lemke-Howson algorithm + routine. It finds an approximate fixed point of `T`, a point + :math:`v^*` such that :math:`\lVert T(v) - v\rVert \leq + \mathrm{error\_tol}`, provided `T` is a function that satisfies the + assumptions of Brouwer's fixed point theorem, i.e., a continuous + function that maps a compact and convex set to itself. + + Parameters + ---------- + T : callable + A callable object (e.g., function) that acts on v + v : object + An object such that T(v) is defined; modified in place if + `method='iteration' and `v` is an array + error_tol : scalar(float), optional(default=1e-3) + Error tolerance + max_iter : scalar(int), optional(default=50) + Maximum number of iterations + verbose : scalar(int), optional(default=2) + Level of feedback (0 for no output, 1 for warnings only, 2 for + warning and residual error reports during iteration) + print_skip : scalar(int), optional(default=5) + How many iterations to apply between print messages (effective + only when `verbose=2`) + method : str, optional(default='iteration') + str in {'iteration', 'imitation_game'}. Method of computing + an approximate fixed point + args, kwargs : + Other arguments and keyword arguments that are passed directly + to the function T each time it is called + + Returns + ------- + v : object + The approximate fixed point + + References + ---------- + .. [1] A. McLennan and R. Tourky, "From Imitation Games to + Kakutani," 2006. + + """ + if max_iter < 1: + raise ValueError('max_iter must be a positive integer') + + if verbose not in (0, 1, 2): + raise ValueError('verbose should be 0, 1 or 2') + + if method not in ['iteration', 'imitation_game']: + raise ValueError('invalid method') + + if method == 'imitation_game': + is_approx_fp = \ + lambda v: _is_approx_fp(T, v, error_tol, *args, **kwargs) + v_star, converged, iterate = \ + _compute_fixed_point_ig(T, v, max_iter, verbose, print_skip, + is_approx_fp, *args, **kwargs) + return v_star + + # method == 'iteration' + iterate = 0 + + if verbose == 2: + start_time = time.time() + _print_after_skip(print_skip, it=None) + + while True: + new_v = T(v, *args, **kwargs) + iterate += 1 + error = np.max(np.abs(new_v - v)) + + try: + v[:] = new_v + except TypeError: + v = new_v + + if error <= error_tol or iterate >= max_iter: + break + + if verbose == 2: + etime = time.time() - start_time + _print_after_skip(print_skip, iterate, error, etime) + + if verbose == 2: + etime = time.time() - start_time + print_skip = 1 + _print_after_skip(print_skip, iterate, error, etime) + if verbose >= 1: + if error > error_tol: + warnings.warn(_non_convergence_msg, RuntimeWarning) + elif verbose == 2: + print(_convergence_msg.format(iterate=iterate)) + + return v + + +def _compute_fixed_point_ig(T, v, max_iter, verbose, print_skip, is_approx_fp, + *args, **kwargs): + """ + Implement the imitation game algorithm by McLennan and Tourky (2006) + for computing an approximate fixed point of `T`. + + Parameters + ---------- + is_approx_fp : callable + A callable with signature `is_approx_fp(v)` which determines + whether `v` is an approximate fixed point with a bool return + value (i.e., True or False) + + For the other parameters, see Parameters in compute_fixed_point. + + Returns + ------- + x_new : scalar(float) or ndarray(float) + Approximate fixed point. + + converged : bool + Whether the routine has converged. + + iterate : scalar(int) + Number of iterations. + + """ + if verbose == 2: + start_time = time.time() + _print_after_skip(print_skip, it=None) + + x_new = v + y_new = T(x_new, *args, **kwargs) + iterate = 1 + converged = is_approx_fp(x_new) + + if converged or iterate >= max_iter: + if verbose == 2: + error = np.max(np.abs(y_new - x_new)) + etime = time.time() - start_time + print_skip = 1 + _print_after_skip(print_skip, iterate, error, etime) + if verbose >= 1: + if not converged: + warnings.warn(_non_convergence_msg, RuntimeWarning) + elif verbose == 2: + print(_convergence_msg.format(iterate=iterate)) + return x_new, converged, iterate + + if verbose == 2: + error = np.max(np.abs(y_new - x_new)) + etime = time.time() - start_time + _print_after_skip(print_skip, iterate, error, etime) + + # Length of the arrays to store the computed sequences of x and y. + # If exceeded, reset to min(max_iter, buff_size*2). + buff_size = 2**8 + buff_size = min(max_iter, buff_size) + + shape = (buff_size,) + np.asarray(x_new).shape + X, Y = np.empty(shape), np.empty(shape) + X[0], Y[0] = x_new, y_new + x_new = Y[0] + + tableaux = tuple(np.empty((buff_size, buff_size*2+1)) for i in range(2)) + bases = tuple(np.empty(buff_size, dtype=int) for i in range(2)) + max_piv = 10**6 # Max number of pivoting steps in _lemke_howson_tbl + + while True: + y_new = T(x_new, *args, **kwargs) + iterate += 1 + converged = is_approx_fp(x_new) + + if converged or iterate >= max_iter: + break + + if verbose == 2: + error = np.max(np.abs(y_new - x_new)) + etime = time.time() - start_time + _print_after_skip(print_skip, iterate, error, etime) + + try: + X[iterate-1] = x_new + Y[iterate-1] = y_new + except IndexError: + buff_size = min(max_iter, buff_size*2) + shape = (buff_size,) + X.shape[1:] + X_tmp, Y_tmp = X, Y + X, Y = np.empty(shape), np.empty(shape) + X[:X_tmp.shape[0]], Y[:Y_tmp.shape[0]] = X_tmp, Y_tmp + X[iterate-1], Y[iterate-1] = x_new, y_new + + tableaux = tuple(np.empty((buff_size, buff_size*2+1)) + for i in range(2)) + bases = tuple(np.empty(buff_size, dtype=int) for i in range(2)) + + m = iterate + tableaux_curr = tuple(tableau[:m, :2*m+1] for tableau in tableaux) + bases_curr = tuple(basis[:m] for basis in bases) + _initialize_tableaux_ig(X[:m], Y[:m], tableaux_curr, bases_curr) + converged, num_iter = _lemke_howson_tbl( + tableaux_curr, bases_curr, init_pivot=m-1, max_iter=max_piv + ) + _, rho = _get_mixed_actions(tableaux_curr, bases_curr) + + if Y.ndim <= 2: + x_new = rho.dot(Y[:m]) + else: + shape_Y = Y.shape + Y_2d = Y.reshape(shape_Y[0], np.prod(shape_Y[1:])) + x_new = rho.dot(Y_2d[:m]).reshape(shape_Y[1:]) + + if verbose == 2: + error = np.max(np.abs(y_new - x_new)) + etime = time.time() - start_time + print_skip = 1 + _print_after_skip(print_skip, iterate, error, etime) + if verbose >= 1: + if not converged: + warnings.warn(_non_convergence_msg, RuntimeWarning) + elif verbose == 2: + print(_convergence_msg.format(iterate=iterate)) + + return x_new, converged, iterate + + +@jit(nopython=True) +def _initialize_tableaux_ig(X, Y, tableaux, bases): + """ + Given sequences `X` and `Y` of ndarrays, initialize the tableau and + basis arrays in place for the "geometric" imitation game as defined + in McLennan and Tourky (2006), to be passed to `_lemke_howson_tbl`. + + Parameters + ---------- + X, Y : ndarray(float) + Arrays of the same shape (m, n). + + tableaux : tuple(ndarray(float, ndim=2)) + Tuple of two arrays to be used to store the tableaux, of shape + (2m, 2m). Modified in place. + + bases : tuple(ndarray(int, ndim=1)) + Tuple of two arrays to be used to store the bases, of shape + (m,). Modified in place. + + Returns + ------- + tableaux : tuple(ndarray(float, ndim=2)) + View to `tableaux`. + + bases : tuple(ndarray(int, ndim=1)) + View to `bases`. + + """ + m = X.shape[0] + min_ = np.zeros(m) + + # Mover + for i in range(m): + for j in range(2*m): + if j == i or j == i + m: + tableaux[0][i, j] = 1 + else: + tableaux[0][i, j] = 0 + # Right hand side + tableaux[0][i, 2*m] = 1 + + # Imitator + for i in range(m): + # Slack variables + for j in range(m): + if j == i: + tableaux[1][i, j] = 1 + else: + tableaux[1][i, j] = 0 + # Payoff variables + for j in range(m): + d = X[i] - Y[j] + tableaux[1][i, m+j] = _square_sum(d) * (-1) + if tableaux[1][i, m+j] < min_[j]: + min_[j] = tableaux[1][i, m+j] + # Right hand side + tableaux[1][i, 2*m] = 1 + # Shift the payoff values + for i in range(m): + for j in range(m): + tableaux[1][i, m+j] -= min_[j] + tableaux[1][i, m+j] += 1 + + for pl, start in enumerate([m, 0]): + for i in range(m): + bases[pl][i] = start + i + + return tableaux, bases + + +@generated_jit(nopython=True, cache=True) +def _square_sum(a): + if isinstance(a, types.Number): + return lambda a: a**2 + elif isinstance(a, types.Array): + return _square_sum_array + + +def _square_sum_array(a): # pragma: no cover + sum_ = 0 + for x in a.flat: + sum_ += x**2 + return sum_ diff --git a/quantecon/discrete_rv.py b/quantecon/discrete_rv.py index 44552851c..51dddf3dc 100644 --- a/quantecon/discrete_rv.py +++ b/quantecon/discrete_rv.py @@ -1,29 +1,83 @@ -# This file is not meant for public use and will be removed v0.8.0. -# Use the `quantecon` namespace for importing the objects -# included below. +""" +Generates an array of draws from a discrete random variable with a +specified vector of probabilities. -import warnings -from . import _discrete_rv +""" +import numpy as np +from .util import check_random_state -__all__ = ['DiscreteRV'] +class DiscreteRV: + """ + Generates an array of draws from a discrete random variable with + vector of probabilities given by q. -def __dir__(): - return __all__ + Parameters + ---------- + q : array_like(float) + Nonnegative numbers that sum to 1. + Attributes + ---------- + q : see Parameters. + Q : array_like(float) + The cumulative sum of q. -def __getattr__(name): - if name not in __all__: - raise AttributeError( - "`quantecon.discrete_rv` is deprecated and has no attribute " - f"'{name}'." - ) + """ - warnings.warn(f"Please use `{name}` from the `quantecon` namespace, " - "the `quantecon.discrete_rv` namespace is deprecated. You " - "can use the following instead:\n " - f"`from quantecon import {name}`.", - category=DeprecationWarning, stacklevel=2) + def __init__(self, q): + self._q = np.asarray(q) + self.Q = np.cumsum(q) - return getattr(_discrete_rv, name) + def __repr__(self): + return "DiscreteRV with {n} elements".format(n=self._q.size) + + def __str__(self): + return self.__repr__() + + @property + def q(self): + """ + Getter method for q. + + """ + return self._q + + @q.setter + def q(self, val): + """ + Setter method for q. + + """ + self._q = np.asarray(val) + self.Q = np.cumsum(val) + + def draw(self, k=1, random_state=None): + """ + Returns k draws from q. + + For each such draw, the value i is returned with probability + q[i]. + + Parameters + ---------- + k : scalar(int), optional + Number of draws to be returned + + random_state : int or np.random.RandomState/Generator, optional + Random seed (integer) or np.random.RandomState or Generator + instance to set the initial state of the random number + generator for reproducibility. If None, a randomly + initialized RandomState is used. + + Returns + ------- + array_like(int) + An array of k independent draws from q + + """ + random_state = check_random_state(random_state) + + return self.Q.searchsorted(random_state.uniform(0, 1, size=k), + side='right') diff --git a/quantecon/dle.py b/quantecon/dle.py index 5173f9274..c987f97c4 100644 --- a/quantecon/dle.py +++ b/quantecon/dle.py @@ -1,28 +1,328 @@ -# This file is not meant for public use and will be removed v0.8.0. -# Use the `quantecon` namespace for importing the objects -# included below. +""" +Provides a class called DLE to convert and solve dynamic linear economies +(as set out in Hansen & Sargent (2013)) as LQ problems. +""" -import warnings -from . import _dle +import numpy as np +from .lqcontrol import LQ +from .matrix_eqn import solve_discrete_lyapunov +from .rank_nullspace import nullspace +class DLE(object): + r""" + This class is for analyzing dynamic linear economies, as set out in Hansen & Sargent (2013). + The planner's problem is to choose \{c_t, s_t, i_t, h_t, k_t, g_t\}_{t=0}^\infty to maximize -__all__ = ['DLE'] + \max -(1/2) \mathbb{E} \sum_{t=0}^{\infty} \beta^t [(s_t - b_t).(s_t-b_t) + g_t.g_t] + subject to the linear constraints -def __dir__(): - return __all__ + \Phi_c c_t + \Phi_g g_t + \Phi_i i_t = \Gamma k_{t-1} + d_t + k_t = \Delta_k k_{t-1} + \Theta_k i_t + h_t = \Delta_h h_{t-1} + \Theta_h c_t + s_t = \Lambda h_{t-1} + \Pi c_t + and -def __getattr__(name): - if name not in __all__: - raise AttributeError( - "`quantecon.dle` is deprecated and has no attribute " - f"'{name}'." - ) + z_{t+1} = A_{22} z_t + C_2 w_{t+1} + b_t = U_b z_t + d_t = U_d z_t - warnings.warn(f"Please use `{name}` from the `quantecon` namespace, " - "the `quantecon.dle` namespace is deprecated. You can use " - f"the following instead:\n `from quantecon import {name}`.", - category=DeprecationWarning, stacklevel=2) + where h_{-1}, k_{-1}, and z_0 are given as initial conditions. - return getattr(_dle, name) + Section 5.5 of HS2013 describes how to map these matrices into those of + a LQ problem. + + HS2013 sort the matrices defining the problem into three groups: + + Information: A_{22}, C_2, U_b , and U_d characterize the motion of information + sets and of taste and technology shocks + + Technology: \Phi_c, \Phi_g, \Phi_i, \Gamma, \Delta_k, and \Theta_k determine the + technology for producing consumption goods + + Preferences: \Delta_h, \Theta_h, \Lambda, and \Pi determine the technology for + producing consumption services from consumer goods. A scalar discount factor \beta + determines the preference ordering over consumption services. + + Parameters + ---------- + Information : tuple + Information is a tuple containing the matrices A_{22}, C_2, U_b, and U_d + Technology : tuple + Technology is a tuple containing the matrices \Phi_c, \Phi_g, \Phi_i, \Gamma, + \Delta_k, and \Theta_k + Preferences : tuple + Preferences is a tuple containing the scalar \beta and the + matrices \Lambda, \Pi, \Delta_h, and \Theta_h + + """ + + def __init__(self, information, technology, preferences): + + # === Unpack the tuples which define information, technology and preferences === # + self.a22, self.c2, self.ub, self.ud = information + self.phic, self.phig, self.phii, self.gamma, self.deltak, self.thetak = technology + self.beta, self.llambda, self.pih, self.deltah, self.thetah = preferences + + # === Computation of the dimension of the structural parameter matrices === # + self.nb, self.nh = self.llambda.shape + self.nd, self.nc = self.phic.shape + self.nz, self.nw = self.c2.shape + _, self.ng = self.phig.shape + self.nk, self.ni = self.thetak.shape + + # === Creation of various useful matrices === # + uc = np.hstack((np.eye(self.nc), np.zeros((self.nc, self.ng)))) + ug = np.hstack((np.zeros((self.ng, self.nc)), np.eye(self.ng))) + phiin = np.linalg.inv(np.hstack((self.phic, self.phig))) + phiinc = uc.dot(phiin) + b11 = - self.thetah.dot(phiinc).dot(self.phii) + a1 = self.thetah.dot(phiinc).dot(self.gamma) + a12 = np.vstack((self.thetah.dot(phiinc).dot( + self.ud), np.zeros((self.nk, self.nz)))) + + # === Creation of the A Matrix for the state transition of the LQ problem === # + + a11 = np.vstack((np.hstack((self.deltah, a1)), np.hstack( + (np.zeros((self.nk, self.nh)), self.deltak)))) + self.A = np.vstack((np.hstack((a11, a12)), np.hstack( + (np.zeros((self.nz, self.nk + self.nh)), self.a22)))) + + # === Creation of the B Matrix for the state transition of the LQ problem === # + + b1 = np.vstack((b11, self.thetak)) + self.B = np.vstack((b1, np.zeros((self.nz, self.ni)))) + + # === Creation of the C Matrix for the state transition of the LQ problem === # + + self.C = np.vstack((np.zeros((self.nk + self.nh, self.nw)), self.c2)) + + # === Define R,W and Q for the payoff function of the LQ problem === # + + self.H = np.hstack((self.llambda, self.pih.dot(uc).dot(phiin).dot(self.gamma), self.pih.dot( + uc).dot(phiin).dot(self.ud) - self.ub, -self.pih.dot(uc).dot(phiin).dot(self.phii))) + self.G = ug.dot(phiin).dot( + np.hstack((np.zeros((self.nd, self.nh)), self.gamma, self.ud, -self.phii))) + self.S = (self.G.T.dot(self.G) + self.H.T.dot(self.H)) / 2 + + self.nx = self.nh + self.nk + self.nz + self.n = self.ni + self.nh + self.nk + self.nz + + self.R = self.S[0:self.nx, 0:self.nx] + self.W = self.S[self.nx:self.n, 0:self.nx] + self.Q = self.S[self.nx:self.n, self.nx:self.n] + + # === Use quantecon's LQ code to solve our LQ problem === # + + lq = LQ(self.Q, self.R, self.A, self.B, + self.C, N=self.W, beta=self.beta) + + self.P, self.F, self.d = lq.stationary_values() + + # === Construct output matrices for our economy using the solution to the LQ problem === # + + self.A0 = self.A - self.B.dot(self.F) + + self.Sh = self.A0[0:self.nh, 0:self.nx] + self.Sk = self.A0[self.nh:self.nh + self.nk, 0:self.nx] + self.Sk1 = np.hstack((np.zeros((self.nk, self.nh)), np.eye( + self.nk), np.zeros((self.nk, self.nz)))) + self.Si = -self.F + self.Sd = np.hstack((np.zeros((self.nd, self.nh + self.nk)), self.ud)) + self.Sb = np.hstack((np.zeros((self.nb, self.nh + self.nk)), self.ub)) + self.Sc = uc.dot(phiin).dot(-self.phii.dot(self.Si) + + self.gamma.dot(self.Sk1) + self.Sd) + self.Sg = ug.dot(phiin).dot(-self.phii.dot(self.Si) + + self.gamma.dot(self.Sk1) + self.Sd) + self.Ss = self.llambda.dot(np.hstack((np.eye(self.nh), np.zeros( + (self.nh, self.nk + self.nz))))) + self.pih.dot(self.Sc) + + # === Calculate eigenvalues of A0 === # + self.A110 = self.A0[0:self.nh + self.nk, 0:self.nh + self.nk] + self.endo = np.linalg.eigvals(self.A110) + self.exo = np.linalg.eigvals(self.a22) + + # === Construct matrices for Lagrange Multipliers === # + + self.Mk = -2 * self.beta.item() * (np.hstack((np.zeros((self.nk, self.nh)), np.eye( + self.nk), np.zeros((self.nk, self.nz))))).dot(self.P).dot(self.A0) + self.Mh = -2 * self.beta.item() * (np.hstack((np.eye(self.nh), np.zeros( + (self.nh, self.nk)), np.zeros((self.nh, self.nz))))).dot(self.P).dot(self.A0) + self.Ms = -(self.Sb - self.Ss) + self.Md = -(np.linalg.inv(np.vstack((self.phic.T, self.phig.T))).dot( + np.vstack((self.thetah.T.dot(self.Mh) + self.pih.T.dot(self.Ms), -self.Sg)))) + self.Mc = -(self.thetah.T.dot(self.Mh) + self.pih.T.dot(self.Ms)) + self.Mi = -(self.thetak.T.dot(self.Mk)) + + def compute_steadystate(self, nnc=2): + """ + Computes the non-stochastic steady-state of the economy. + + Parameters + ---------- + nnc : array_like(float) + nnc is the location of the constant in the state vector x_t + + """ + zx = np.eye(self.A0.shape[0])-self.A0 + self.zz = nullspace(zx) + self.zz /= self.zz[nnc] + self.css = self.Sc.dot(self.zz) + self.sss = self.Ss.dot(self.zz) + self.iss = self.Si.dot(self.zz) + self.dss = self.Sd.dot(self.zz) + self.bss = self.Sb.dot(self.zz) + self.kss = self.Sk.dot(self.zz) + self.hss = self.Sh.dot(self.zz) + + def compute_sequence(self, x0, ts_length=None, Pay=None): + """ + Simulate quantities and prices for the economy + + Parameters + ---------- + x0 : array_like(float) + The initial state + + ts_length : scalar(int) + Length of the simulation + + Pay : array_like(float) + Vector to price an asset whose payout is Pay*xt + + """ + lq = LQ(self.Q, self.R, self.A, self.B, + self.C, N=self.W, beta=self.beta) + xp, up, wp = lq.compute_sequence(x0, ts_length) + self.h = self.Sh.dot(xp) + self.k = self.Sk.dot(xp) + self.i = self.Si.dot(xp) + self.b = self.Sb.dot(xp) + self.d = self.Sd.dot(xp) + self.c = self.Sc.dot(xp) + self.g = self.Sg.dot(xp) + self.s = self.Ss.dot(xp) + + # === Value of J-period risk-free bonds === # + # === See p.145: Equation (7.11.2) === # + e1 = np.zeros((1, self.nc)) + e1[0, 0] = 1 + self.R1_Price = np.empty((ts_length + 1, 1)) + self.R2_Price = np.empty((ts_length + 1, 1)) + self.R5_Price = np.empty((ts_length + 1, 1)) + for i in range(ts_length + 1): + self.R1_Price[i, 0] = self.beta * e1.dot(self.Mc).dot(np.linalg.matrix_power( + self.A0, 1)).dot(xp[:, i]) / e1.dot(self.Mc).dot(xp[:, i]) + self.R2_Price[i, 0] = self.beta**2 * e1.dot(self.Mc).dot( + np.linalg.matrix_power(self.A0, 2)).dot(xp[:, i]) / e1.dot(self.Mc).dot(xp[:, i]) + self.R5_Price[i, 0] = self.beta**5 * e1.dot(self.Mc).dot( + np.linalg.matrix_power(self.A0, 5)).dot(xp[:, i]) / e1.dot(self.Mc).dot(xp[:, i]) + + # === Gross rates of return on 1-period risk-free bonds === # + self.R1_Gross = 1 / self.R1_Price + + # === Net rates of return on J-period risk-free bonds === # + # === See p.148: log of gross rate of return, divided by j === # + self.R1_Net = np.log(1 / self.R1_Price) / 1 + self.R2_Net = np.log(1 / self.R2_Price) / 2 + self.R5_Net = np.log(1 / self.R5_Price) / 5 + + # === Value of asset whose payout vector is Pay*xt === # + # See p.145: Equation (7.11.1) + if isinstance(Pay, np.ndarray) == True: + self.Za = Pay.T.dot(self.Mc) + self.Q = solve_discrete_lyapunov( + self.A0.T * self.beta**0.5, self.Za) + self.q = self.beta / (1 - self.beta) * \ + np.trace(self.C.T.dot(self.Q).dot(self.C)) + self.Pay_Price = np.empty((ts_length + 1, 1)) + self.Pay_Gross = np.empty((ts_length + 1, 1)) + self.Pay_Gross[0, 0] = np.nan + for i in range(ts_length + 1): + self.Pay_Price[i, 0] = (xp[:, i].T.dot(self.Q).dot( + xp[:, i]) + self.q) / e1.dot(self.Mc).dot(xp[:, i]) + for i in range(ts_length): + self.Pay_Gross[i + 1, 0] = self.Pay_Price[i + 1, + 0] / (self.Pay_Price[i, 0] - Pay.dot(xp[:, i])) + return + + def irf(self, ts_length=100, shock=None): + """ + Create Impulse Response Functions + + Parameters + ---------- + + ts_length : scalar(int) + Number of periods to calculate IRF + + Shock : array_like(float) + Vector of shocks to calculate IRF to. Default is first element of w + + """ + + if type(shock) != np.ndarray: + # Default is to select first element of w + shock = np.vstack((np.ones((1, 1)), np.zeros((self.nw - 1, 1)))) + + self.c_irf = np.empty((ts_length, self.nc)) + self.s_irf = np.empty((ts_length, self.nb)) + self.i_irf = np.empty((ts_length, self.ni)) + self.k_irf = np.empty((ts_length, self.nk)) + self.h_irf = np.empty((ts_length, self.nh)) + self.g_irf = np.empty((ts_length, self.ng)) + self.d_irf = np.empty((ts_length, self.nd)) + self.b_irf = np.empty((ts_length, self.nb)) + + for i in range(ts_length): + self.c_irf[i, :] = self.Sc.dot( + np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T + self.s_irf[i, :] = self.Ss.dot( + np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T + self.i_irf[i, :] = self.Si.dot( + np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T + self.k_irf[i, :] = self.Sk.dot( + np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T + self.h_irf[i, :] = self.Sh.dot( + np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T + self.g_irf[i, :] = self.Sg.dot( + np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T + self.d_irf[i, :] = self.Sd.dot( + np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T + self.b_irf[i, :] = self.Sb.dot( + np.linalg.matrix_power(self.A0, i)).dot(self.C).dot(shock).T + + return + + def canonical(self): + """ + Compute canonical preference representation + Uses auxiliary problem of 9.4.2, with the preference shock process reintroduced + Calculates pihat, llambdahat and ubhat for the equivalent canonical household technology + """ + Ac1 = np.hstack((self.deltah, np.zeros((self.nh, self.nz)))) + Ac2 = np.hstack((np.zeros((self.nz, self.nh)), self.a22)) + Ac = np.vstack((Ac1, Ac2)) + Bc = np.vstack((self.thetah, np.zeros((self.nz, self.nc)))) + Rc1 = np.hstack((self.llambda.T.dot(self.llambda), - + self.llambda.T.dot(self.ub))) + Rc2 = np.hstack((-self.ub.T.dot(self.llambda), self.ub.T.dot(self.ub))) + Rc = np.vstack((Rc1, Rc2)) + Qc = self.pih.T.dot(self.pih) + Nc = np.hstack( + (self.pih.T.dot(self.llambda), -self.pih.T.dot(self.ub))) + + lq_aux = LQ(Qc, Rc, Ac, Bc, N=Nc, beta=self.beta) + + P1, F1, d1 = lq_aux.stationary_values() + + self.F_b = F1[:, 0:self.nh] + self.F_f = F1[:, self.nh:] + + self.pihat = np.linalg.cholesky(self.pih.T.dot( + self.pih) + self.beta.dot(self.thetah.T).dot(P1[0:self.nh, 0:self.nh]).dot(self.thetah)).T + self.llambdahat = self.pihat.dot(self.F_b) + self.ubhat = - self.pihat.dot(self.F_f) + + return diff --git a/quantecon/ecdf.py b/quantecon/ecdf.py index f753b45a5..a42604694 100644 --- a/quantecon/ecdf.py +++ b/quantecon/ecdf.py @@ -1,28 +1,54 @@ -# This file is not meant for public use and will be removed v0.8.0. -# Use the `quantecon` namespace for importing the objects -# included below. +""" +Implements the empirical cumulative distribution function given an array +of observations. -import warnings -from . import _ecdf +""" +import numpy as np -__all__ = ['ECDF'] +class ECDF: + """ + One-dimensional empirical distribution function given a vector of + observations. -def __dir__(): - return __all__ + Parameters + ---------- + observations : array_like + An array of observations + Attributes + ---------- + observations : see Parameters -def __getattr__(name): - if name not in __all__: - raise AttributeError( - "`quantecon.ecdf` is deprecated and has no attribute " - f"'{name}'." - ) + """ - warnings.warn(f"Please use `{name}` from the `quantecon` namespace, " - "the `quantecon.ecdf` namespace is deprecated. You can use " - f"the following instead:\n `from quantecon import {name}`.", - category=DeprecationWarning, stacklevel=2) + def __init__(self, observations): + self.observations = np.asarray(observations) - return getattr(_ecdf, name) + def __repr__(self): + return self.__str__() + + def __str__(self): + m = "Empirical CDF:\n - number of observations: {n}" + return m.format(n=self.observations.size) + + def __call__(self, x): + """ + Evaluates the ecdf at x + + Parameters + ---------- + x : scalar(float) + The x at which the ecdf is evaluated + + Returns + ------- + scalar(float) + Fraction of the sample less than x + + """ + def f(a): + return np.mean(self.observations <= a) + vf = np.frompyfunc(f, 1, 1) + return vf(x).astype(float) diff --git a/quantecon/estspec.py b/quantecon/estspec.py index 841dd6344..662dfab8a 100644 --- a/quantecon/estspec.py +++ b/quantecon/estspec.py @@ -1,28 +1,152 @@ -# This file is not meant for public use and will be removed v0.8.0. -# Use the `quantecon` namespace for importing the objects -# included below. +""" +Functions for working with periodograms of scalar data. -import warnings -from . import _estspec +""" +import numpy as np +from numpy.fft import fft -__all__ = ['smooth', 'periodogram', 'ar_periodogram'] +def smooth(x, window_len=7, window='hanning'): + """ + Smooth the data in x using convolution with a window of requested + size and type. + Parameters + ---------- + x : array_like(float) + A flat NumPy array containing the data to smooth + window_len : scalar(int), optional + An odd integer giving the length of the window. Defaults to 7. + window : string + A string giving the window type. Possible values are 'flat', + 'hanning', 'hamming', 'bartlett' or 'blackman' -def __dir__(): - return __all__ + Returns + ------- + array_like(float) + The smoothed values + Notes + ----- + Application of the smoothing window at the top and bottom of x is + done by reflecting x around these points to extend it sufficiently + in each direction. -def __getattr__(name): - if name not in __all__: - raise AttributeError( - "`quantecon.estspec` is deprecated and has no attribute " - f"'{name}'." - ) + """ + if len(x) < window_len: + raise ValueError("Input vector length must be >= window length.") - warnings.warn(f"Please use `{name}` from the `quantecon` namespace, " - "the `quantecon.estspec` namespace is deprecated. You can use" - f" the following instead:\n `from quantecon import {name}`.", - category=DeprecationWarning, stacklevel=2) + if window_len < 3: + raise ValueError("Window length must be at least 3.") - return getattr(_estspec, name) + if not window_len % 2: # window_len is even + window_len += 1 + print("Window length reset to {}".format(window_len)) + + windows = {'hanning': np.hanning, + 'hamming': np.hamming, + 'bartlett': np.bartlett, + 'blackman': np.blackman, + 'flat': np.ones # moving average + } + + # === Reflect x around x[0] and x[-1] prior to convolution === # + k = int(window_len / 2) + xb = x[:k] # First k elements + xt = x[-k:] # Last k elements + s = np.concatenate((xb[::-1], x, xt[::-1])) + + # === Select window values === # + if window in windows.keys(): + w = windows[window](window_len) + else: + msg = "Unrecognized window type '{}'".format(window) + print(msg + " Defaulting to hanning") + w = windows['hanning'](window_len) + + return np.convolve(w / w.sum(), s, mode='valid') + + +def periodogram(x, window=None, window_len=7): + r""" + Computes the periodogram + + .. math:: + + I(w) = \frac{1}{n} \Big[ \sum_{t=0}^{n-1} x_t e^{itw} \Big] ^2 + + at the Fourier frequencies :math:`w_j := \frac{2 \pi j}{n}`, + :math:`j = 0, \dots, n - 1`, using the fast Fourier transform. Only the + frequencies :math:`w_j` in :math:`[0, \pi]` and corresponding values + :math:`I(w_j)` are returned. If a window type is given then smoothing + is performed. + + Parameters + ---------- + x : array_like(float) + A flat NumPy array containing the data to smooth + window_len : scalar(int), optional(default=7) + An odd integer giving the length of the window. Defaults to 7. + window : string + A string giving the window type. Possible values are 'flat', + 'hanning', 'hamming', 'bartlett' or 'blackman' + + Returns + ------- + w : array_like(float) + Fourier frequencies at which periodogram is evaluated + I_w : array_like(float) + Values of periodogram at the Fourier frequencies + + """ + n = len(x) + I_w = np.abs(fft(x))**2 / n + w = 2 * np.pi * np.arange(n) / n # Fourier frequencies + w, I_w = w[:int(n/2)+1], I_w[:int(n/2)+1] # Take only values on [0, pi] + if window: + I_w = smooth(I_w, window_len=window_len, window=window) + return w, I_w + + +def ar_periodogram(x, window='hanning', window_len=7): + """ + Compute periodogram from data x, using prewhitening, smoothing and + recoloring. The data is fitted to an AR(1) model for prewhitening, + and the residuals are used to compute a first-pass periodogram with + smoothing. The fitted coefficients are then used for recoloring. + + Parameters + ---------- + x : array_like(float) + A flat NumPy array containing the data to smooth + window_len : scalar(int), optional + An odd integer giving the length of the window. Defaults to 7. + window : string + A string giving the window type. Possible values are 'flat', + 'hanning', 'hamming', 'bartlett' or 'blackman' + + Returns + ------- + w : array_like(float) + Fourier frequencies at which periodogram is evaluated + I_w : array_like(float) + Values of periodogram at the Fourier frequencies + + """ + # === run regression === # + x_lag = x[:-1] # lagged x + X = np.array([np.ones(len(x_lag)), x_lag]).T # add constant + + y = np.array(x[1:]) # current x + + beta_hat = np.linalg.solve(X.T @ X, X.T @ y) # solve for beta hat + e_hat = y - X @ beta_hat # compute residuals + phi = beta_hat[1] # pull out phi parameter + + # === compute periodogram on residuals === # + w, I_w = periodogram(e_hat, window=window, window_len=window_len) + + # === recolor and return === # + I_w = I_w / np.abs(1 - phi * np.exp(1j * w))**2 + + return w, I_w diff --git a/quantecon/filter.py b/quantecon/filter.py index a78a67d23..44d45f9a9 100644 --- a/quantecon/filter.py +++ b/quantecon/filter.py @@ -1,28 +1,58 @@ -# This file is not meant for public use and will be removed v0.8.0. -# Use the `quantecon` namespace for importing the objects -# included below. - -import warnings -from . import _filter - - -__all__ = ['hamilton_filter'] - - -def __dir__(): - return __all__ - - -def __getattr__(name): - if name not in __all__: - raise AttributeError( - "`quantecon.filter` is deprecated and has no attribute " - f"'{name}'." - ) - - warnings.warn(f"Please use `{name}` from the `quantecon` namespace, " - "the `quantecon.filter` namespace is deprecated. You can use" - f" the following instead:\n `from quantecon import {name}`.", - category=DeprecationWarning, stacklevel=2) - - return getattr(_filter, name) +""" + +function for filtering + +""" +import numpy as np + + +def hamilton_filter(data, h, p=None): + r""" + This function applies "Hamilton filter" to the data + + http://econweb.ucsd.edu/~jhamilto/hp.pdf + + Parameters + ---------- + data : array or dataframe + h : integer + Time horizon that we are likely to predict incorrectly. + Original paper recommends 2 for annual data, 8 for quarterly data, + 24 for monthly data. + p : integer (optional) + If supplied, it is p in the paper. Number of lags in regression. + If not supplied, random walk process is assumed. + + Returns + ------- + cycle : array of cyclical component + trend : trend component + + Notes + ----- + For seasonal data, it's desirable for p and h to be integer multiples of + the number of obsevations in a year. E.g. for quarterly data, h = 8 and p = + 4 are recommended. + + """ + # transform data to array + y = np.asarray(data, float) + # sample size + T = len(y) + + if p is not None: # if p is supplied + # construct X matrix of lags + X = np.ones((T-p-h+1, p+1)) + for j in range(1, p+1): + X[:, j] = y[p-j:T-h-j+1:1] + + # do OLS regression + b = np.linalg.solve(X.transpose()@X, X.transpose()@y[p+h-1:T]) + # trend component (`nan` for the first p+h-1 period) + trend = np.append(np.zeros(p+h-1)+np.nan, X@b) + # cyclical component + cycle = y - trend + else: # if p is not supplied (random walk) + cycle = np.append(np.zeros(h)+np.nan, y[h:T] - y[0:T-h]) + trend = y - cycle + return cycle, trend diff --git a/quantecon/game_theory/game_generators/bimatrix_generators.py b/quantecon/game_theory/game_generators/bimatrix_generators.py index 294194514..a16674227 100644 --- a/quantecon/game_theory/game_generators/bimatrix_generators.py +++ b/quantecon/game_theory/game_generators/bimatrix_generators.py @@ -95,8 +95,8 @@ from numba import jit from ..normal_form_game import Player, NormalFormGame from ...util import check_random_state, rng_integers -from ..._gridtools import simplex_grid -from ..._graph_tools import random_tournament_graph +from ...gridtools import simplex_grid +from ...graph_tools import random_tournament_graph from ...util.combinatorics import next_k_array, k_array_rank_jit __all__ = [ diff --git a/quantecon/game_theory/game_generators/tests/test_bimatrix_generators.py b/quantecon/game_theory/game_generators/tests/test_bimatrix_generators.py index 4055a54eb..cde2061f7 100644 --- a/quantecon/game_theory/game_generators/tests/test_bimatrix_generators.py +++ b/quantecon/game_theory/game_generators/tests/test_bimatrix_generators.py @@ -5,7 +5,7 @@ import numpy as np from scipy.special import comb from numpy.testing import assert_array_equal, assert_, assert_raises -from quantecon import num_compositions +from quantecon.gridtools import num_compositions from quantecon.game_theory import pure_nash_brute from quantecon.game_theory import ( diff --git a/quantecon/game_theory/mclennan_tourky.py b/quantecon/game_theory/mclennan_tourky.py index a4e72191a..01536583f 100644 --- a/quantecon/game_theory/mclennan_tourky.py +++ b/quantecon/game_theory/mclennan_tourky.py @@ -6,7 +6,7 @@ """ import numbers import numpy as np -from quantecon._compute_fp import _compute_fixed_point_ig +from ..compute_fp import _compute_fixed_point_ig from .normal_form_game import pure2mixed from .utilities import NashResult diff --git a/quantecon/game_theory/normal_form_game.py b/quantecon/game_theory/normal_form_game.py index 9e1a6f8d5..179779fe9 100644 --- a/quantecon/game_theory/normal_form_game.py +++ b/quantecon/game_theory/normal_form_game.py @@ -130,7 +130,7 @@ import numpy as np from numba import jit -from quantecon.util import check_random_state, rng_integers +from ..util import check_random_state, rng_integers class Player: diff --git a/quantecon/game_theory/support_enumeration.py b/quantecon/game_theory/support_enumeration.py index 0a082adf4..5500e1e7b 100644 --- a/quantecon/game_theory/support_enumeration.py +++ b/quantecon/game_theory/support_enumeration.py @@ -11,8 +11,8 @@ """ import numpy as np from numba import jit -from quantecon.util.numba import _numba_linalg_solve -from quantecon.util.combinatorics import next_k_array +from ..util.numba import _numba_linalg_solve +from ..util.combinatorics import next_k_array def support_enumeration(g): diff --git a/quantecon/graph_tools.py b/quantecon/graph_tools.py index dd8d0fbaa..3f4dca799 100644 --- a/quantecon/graph_tools.py +++ b/quantecon/graph_tools.py @@ -1,28 +1,439 @@ -# This file is not meant for public use and will be removed v0.8.0. -# Use the `quantecon` namespace for importing the objects -# included below. +""" +Tools for dealing with a directed graph. -import warnings -from . import _graph_tools +""" +import numpy as np +from scipy import sparse +from scipy.sparse import csgraph +from math import gcd +from numba import jit +from .util import check_random_state -__all__ = ['DiGraph', 'random_tournament_graph', 'annotate_nodes'] +# Decorator for *_components properties +def annotate_nodes(func): + def new_func(self): + list_of_components = func(self) + if self.node_labels is not None: + return [self.node_labels[c] for c in list_of_components] + return list_of_components + return new_func -def __dir__(): - return __all__ +class DiGraph: + r""" + Class for a directed graph. It stores useful information about the + graph structure such as strong connectivity [1]_ and periodicity + [2]_. -def __getattr__(name): - if name not in __all__: - raise AttributeError( - "`quantecon.graph_tools` is deprecated and has no attribute " - f"'{name}'." + Parameters + ---------- + adj_matrix : array_like(ndim=2) + Adjacency matrix representing a directed graph. Must be of shape + n x n. + + weighted : bool, optional(default=False) + Whether to treat `adj_matrix` as a weighted adjacency matrix. + + node_labels : array_like(default=None) + Array_like of length n containing the labels associated with the + nodes, which must be homogeneous in type. If None, the labels + default to integers 0 through n-1. + + Attributes + ---------- + csgraph : scipy.sparse.csr_matrix + Compressed sparse representation of the digraph. + + is_strongly_connected : bool + Indicate whether the digraph is strongly connected. + + num_strongly_connected_components : int + The number of the strongly connected components. + + strongly_connected_components_indices : list(ndarray(int)) + List of numpy arrays containing the indices of the strongly + connected components. + + strongly_connected_components : list(ndarray) + List of numpy arrays containing the strongly connected + components, where the nodes are annotated with their labels (if + `node_labels` is not None). + + num_sink_strongly_connected_components : int + The number of the sink strongly connected components. + + sink_strongly_connected_components_indices : list(ndarray(int)) + List of numpy arrays containing the indices of the sink strongly + connected components. + + sink_strongly_connected_components : list(ndarray) + List of numpy arrays containing the sink strongly connected + components, where the nodes are annotated with their labels (if + `node_labels` is not None). + + is_aperiodic : bool + Indicate whether the digraph is aperiodic. + + period : int + The period of the digraph. Defined only for a strongly connected + digraph. + + cyclic_components_indices : list(ndarray(int)) + List of numpy arrays containing the indices of the cyclic + components. + + cyclic_components : list(ndarray) + List of numpy arrays containing the cyclic components, where the + nodes are annotated with their labels (if `node_labels` is not + None). + + References + ---------- + .. [1] `Strongly connected component + `_, + Wikipedia. + + .. [2] `Aperiodic graph + `_, Wikipedia. + + """ + + def __init__(self, adj_matrix, weighted=False, node_labels=None): + if weighted: + dtype = None + else: + dtype = bool + self.csgraph = sparse.csr_matrix(adj_matrix, dtype=dtype) + + m, n = self.csgraph.shape + if n != m: + raise ValueError('input matrix must be square') + + self.n = n # Number of nodes + + # Call the setter method + self.node_labels = node_labels + + self._num_scc = None + self._scc_proj = None + self._sink_scc_labels = None + + self._period = None + + def __repr__(self): + return self.__str__() + + def __str__(self): + return "Directed Graph:\n - n(number of nodes): {n}".format(n=self.n) + + @property + def node_labels(self): + return self._node_labels + + @node_labels.setter + def node_labels(self, values): + if values is None: + self._node_labels = None + else: + values = np.asarray(values) + if (values.ndim < 1) or (values.shape[0] != self.n): + raise ValueError( + 'node_labels must be an array_like of length n' + ) + if np.issubdtype(values.dtype, np.object_): + raise ValueError( + 'data in node_labels must be homogeneous in type' + ) + self._node_labels = values + + def _find_scc(self): + """ + Set ``self._num_scc`` and ``self._scc_proj`` + by calling ``scipy.sparse.csgraph.connected_components``: + * docs.scipy.org/doc/scipy/reference/sparse.csgraph.html + * github.com/scipy/scipy/blob/master/scipy/sparse/csgraph/_traversal.pyx + + ``self._scc_proj`` is a list of length `n` that assigns to each node + the label of the strongly connected component to which it belongs. + + """ + # Find the strongly connected components + self._num_scc, self._scc_proj = \ + csgraph.connected_components(self.csgraph, connection='strong') + + @property + def num_strongly_connected_components(self): + if self._num_scc is None: + self._find_scc() + return self._num_scc + + @property + def scc_proj(self): + if self._scc_proj is None: + self._find_scc() + return self._scc_proj + + @property + def is_strongly_connected(self): + return (self.num_strongly_connected_components == 1) + + def _condensation_lil(self): + """ + Return the sparse matrix representation of the condensation digraph + in lil format. + + """ + condensation_lil = sparse.lil_matrix( + (self.num_strongly_connected_components, + self.num_strongly_connected_components), dtype=bool + ) + + scc_proj = self.scc_proj + for node_from, node_to in _csr_matrix_indices(self.csgraph): + scc_from, scc_to = scc_proj[node_from], scc_proj[node_to] + if scc_from != scc_to: + condensation_lil[scc_from, scc_to] = True + + return condensation_lil + + def _find_sink_scc(self): + """ + Set self._sink_scc_labels, which is a list containing the labels of + the strongly connected components. + + """ + condensation_lil = self._condensation_lil() + + # A sink SCC is a SCC such that none of its members is strongly + # connected to nodes in other SCCs + # Those k's such that graph_condensed_lil.rows[k] == [] + self._sink_scc_labels = \ + np.where(np.logical_not(condensation_lil.rows))[0] + + @property + def sink_scc_labels(self): + if self._sink_scc_labels is None: + self._find_sink_scc() + return self._sink_scc_labels + + @property + def num_sink_strongly_connected_components(self): + return len(self.sink_scc_labels) + + @property + def strongly_connected_components_indices(self): + if self.is_strongly_connected: + return [np.arange(self.n)] + else: + return [np.where(self.scc_proj == k)[0] + for k in range(self.num_strongly_connected_components)] + + @property + @annotate_nodes + def strongly_connected_components(self): + return self.strongly_connected_components_indices + + @property + def sink_strongly_connected_components_indices(self): + if self.is_strongly_connected: + return [np.arange(self.n)] + else: + return [np.where(self.scc_proj == k)[0] + for k in self.sink_scc_labels.tolist()] + + @property + @annotate_nodes + def sink_strongly_connected_components(self): + return self.sink_strongly_connected_components_indices + + def _compute_period(self): + """ + Set ``self._period`` and ``self._cyclic_components_proj``. + + Use the algorithm described in: + J. P. Jarvis and D. R. Shier, + "Graph-Theoretic Analysis of Finite Markov Chains," 1996. + + """ + # Degenerate graph with a single node (which is strongly connected) + # csgraph.reconstruct_path would raise an exception + # github.com/scipy/scipy/issues/4018 + if self.n == 1: + if self.csgraph[0, 0] == 0: # No edge: "trivial graph" + self._period = 1 # Any universally accepted definition? + self._cyclic_components_proj = np.zeros(self.n, dtype=int) + return None + else: # Self loop + self._period = 1 + self._cyclic_components_proj = np.zeros(self.n, dtype=int) + return None + + if not self.is_strongly_connected: + raise NotImplementedError( + 'Not defined for a non strongly-connected digraph' ) - warnings.warn(f"Please use `{name}` from the `quantecon` namespace, the" - "`quantecon.graph_tools` namespace is deprecated. You can use" - f" the following instead:\n `from quantecon import {name}`.", - category=DeprecationWarning, stacklevel=2) + if np.any(self.csgraph.diagonal() > 0): + self._period = 1 + self._cyclic_components_proj = np.zeros(self.n, dtype=int) + return None + + # Construct a breadth-first search tree rooted at 0 + node_order, predecessors = \ + csgraph.breadth_first_order(self.csgraph, i_start=0) + bfs_tree_csr = \ + csgraph.reconstruct_path(self.csgraph, predecessors) + + # Edges not belonging to tree_csr + non_bfs_tree_csr = self.csgraph - bfs_tree_csr + non_bfs_tree_csr.eliminate_zeros() + + # Distance to 0 + level = np.zeros(self.n, dtype=int) + for i in range(1, self.n): + level[node_order[i]] = level[predecessors[node_order[i]]] + 1 + + # Determine the period + d = 0 + for node_from, node_to in _csr_matrix_indices(non_bfs_tree_csr): + value = level[node_from] - level[node_to] + 1 + d = gcd(d, value) + if d == 1: + self._period = 1 + self._cyclic_components_proj = np.zeros(self.n, dtype=int) + return None + + self._period = d + self._cyclic_components_proj = level % d + + @property + def period(self): + if self._period is None: + self._compute_period() + return self._period + + @property + def is_aperiodic(self): + return (self.period == 1) + + @property + def cyclic_components_indices(self): + if self.is_aperiodic: + return [np.arange(self.n)] + else: + return [np.where(self._cyclic_components_proj == k)[0] + for k in range(self.period)] + + @property + @annotate_nodes + def cyclic_components(self,): + return self.cyclic_components_indices + + def subgraph(self, nodes): + """ + Return the subgraph consisting of the given nodes and edges + between thses nodes. + + Parameters + ---------- + nodes : array_like(int, ndim=1) + Array of node indices. + + Returns + ------- + DiGraph + A DiGraph representing the subgraph. + + """ + adj_matrix = self.csgraph[np.ix_(nodes, nodes)] + + weighted = True # To copy the dtype + + if self.node_labels is not None: + node_labels = self.node_labels[nodes] + else: + node_labels = None + + return DiGraph(adj_matrix, weighted=weighted, node_labels=node_labels) + + +def _csr_matrix_indices(S): + """ + Generate the indices of nonzero entries of a csr_matrix S + + """ + m, n = S.shape + + for i in range(m): + for j in range(S.indptr[i], S.indptr[i+1]): + row_index, col_index = i, S.indices[j] + yield row_index, col_index + + +def random_tournament_graph(n, random_state=None): + """ + Return a random tournament graph [1]_ with n nodes. + + Parameters + ---------- + n : scalar(int) + Number of nodes. + + random_state : int or np.random.RandomState/Generator, optional + Random seed (integer) or np.random.RandomState or Generator + instance to set the initial state of the random number generator + for reproducibility. If None, a randomly initialized RandomState + is used. + + Returns + ------- + DiGraph + A DiGraph representing the tournament graph. + + References + ---------- + .. [1] `Tournament (graph theory) + `_, + Wikipedia. + + """ + random_state = check_random_state(random_state) + num_edges = n * (n-1) // 2 + r = random_state.random(num_edges) + row = np.empty(num_edges, dtype=int) + col = np.empty(num_edges, dtype=int) + _populate_random_tournament_row_col(n, r, row, col) + data = np.ones(num_edges, dtype=bool) + adj_matrix = sparse.coo_matrix((data, (row, col)), shape=(n, n)) + return DiGraph(adj_matrix) + + +@jit(nopython=True, cache=True) +def _populate_random_tournament_row_col(n, r, row, col): + """ + Populate ndarrays `row` and `col` with directed edge indices + determined by random numbers in `r` for a tournament graph with n + nodes, which has num_edges = n * (n-1) // 2 edges. + + Parameters + ---------- + n : scalar(int) + Number of nodes. + + r : ndarray(float, ndim=1) + ndarray of length num_edges containing random numbers in [0, 1). + + row, col : ndarray(int, ndim=1) + ndarrays of length num_edges to be modified in place. - return getattr(_graph_tools, name) + """ + k = 0 + for i in range(n): + for j in range(i+1, n): + if r[k] < 0.5: + row[k], col[k] = i, j + else: + row[k], col[k] = j, i + k += 1 diff --git a/quantecon/gridtools.py b/quantecon/gridtools.py index 5ae3b7546..8915c3451 100644 --- a/quantecon/gridtools.py +++ b/quantecon/gridtools.py @@ -1,29 +1,435 @@ -# This file is not meant for public use and will be removed v0.8.0. -# Use the `quantecon` namespace for importing the objects -# included below. +""" +Implements cartesian products and regular cartesian grids, and provides +a function that constructs a grid for a simplex as well as one that +determines the index of a point in the simplex. -import warnings -from . import _gridtools +""" +import numpy as np +import scipy.special +from numba import jit, njit +from .util.numba import comb_jit -__all__ = ['cartesian', 'mlinspace', 'simplex_grid', 'simplex_index', - 'num_compositions', 'num_compositions_jit'] +def cartesian(nodes, order='C'): + ''' + Cartesian product of a list of arrays + Parameters + ---------- + nodes : list(array_like(ndim=1)) -def __dir__(): - return __all__ + order : str, optional(default='C') + ('C' or 'F') order in which the product is enumerated + Returns + ------- + out : ndarray(ndim=2) + each line corresponds to one point of the product space + ''' -def __getattr__(name): - if name not in __all__: - raise AttributeError( - "`quantecon.gridtools` is deprecated and has no attribute " - f"'{name}'." - ) + nodes = [np.asarray(e) for e in nodes] + shapes = [e.shape[0] for e in nodes] - warnings.warn(f"Please use `{name}` from the `quantecon` namespace, the" - "`quantecon.gridtools` namespace is deprecated. You can use" - f" the following instead:\n `from quantecon import {name}`.", - category=DeprecationWarning, stacklevel=2) + dtype = np.result_type(*nodes) - return getattr(_gridtools, name) + n = len(nodes) + l = np.prod(shapes) + out = np.zeros((l, n), dtype=dtype) + + if order == 'C': + repetitions = np.cumprod([1] + shapes[:-1]) + else: + shapes.reverse() + sh = [1] + shapes[:-1] + repetitions = np.cumprod(sh) + repetitions = repetitions.tolist() + repetitions.reverse() + + for i in range(n): + _repeat_1d(nodes[i], repetitions[i], out[:, i]) + + return out + + +def mlinspace(a, b, nums, order='C'): + ''' + Constructs a regular cartesian grid + + Parameters + ---------- + a : array_like(ndim=1) + lower bounds in each dimension + + b : array_like(ndim=1) + upper bounds in each dimension + + nums : array_like(ndim=1) + number of nodes along each dimension + + order : str, optional(default='C') + ('C' or 'F') order in which the product is enumerated + + Returns + ------- + out : ndarray(ndim=2) + each line corresponds to one point of the product space + ''' + + a = np.asarray(a, dtype='float64') + b = np.asarray(b, dtype='float64') + nums = np.asarray(nums, dtype='int64') + nodes = [np.linspace(a[i], b[i], nums[i]) for i in range(len(nums))] + + return cartesian(nodes, order=order) + + +@njit +def _repeat_1d(x, K, out): + ''' + Repeats each element of a vector many times and repeats the whole + result many times + + Parameters + ---------- + x : ndarray(ndim=1) + vector to be repeated + + K : scalar(int) + number of times each element of x is repeated (inner iterations) + + out : ndarray(ndim=1) + placeholder for the result + + Returns + ------- + None + ''' + + N = x.shape[0] + L = out.shape[0] // (K*N) # number of outer iterations + # K # number of inner iterations + + # the result out should enumerate in C-order the elements + # of a 3-dimensional array T of dimensions (K,N,L) + # such that for all k,n,l, we have T[k,n,l] == x[n] + + for n in range(N): + val = x[n] + for k in range(K): + for l in range(L): + ind = k*N*L + n*L + l + out[ind] = val + + +def cartesian_nearest_index(x, nodes, order='C'): + """ + Return the index of the point closest to `x` within the cartesian + product generated by `nodes`. Each array in `nodes` must be sorted + in ascending order. + + Parameters + ---------- + x : array_like(ndim=1 or 2) + Point(s) to search the closest point(s) for. + + nodes : array_like(array_like(ndim=1)) + Array of sorted arrays. + + order : str, optional(default='C') + ('C' or 'F') order in which the product is enumerated. + + Returns + ------- + scalar(int) or ndarray(int, ndim=1) + Index (indices) of the closest point(s) to `x`. + + Examples + -------- + >>> nodes = (np.arange(3), np.arange(2)) + >>> prod = qe.cartesian(nodes) + >>> print(prod) + [[0 0] + [0 1] + [1 0] + [1 1] + [2 0] + [2 1]] + + Among the 6 points in the cartesian product `prod`, the closest to + the point (0.6, 0.4) is `prod[2]`: + + >>> x = (0.6, 0.4) + >>> qe.cartesian_nearest_index(x, nodes) # Pass `nodes`, not `prod` + 2 + + The closest to (-0.1, 1.2) and (2, 0) are `prod[1]` and `prod[4]`, + respectively: + + >>> x = [(-0.1, 1.2), (2, 0)] + >>> qe.cartesian_nearest_index(x, nodes) + array([1, 4]) + + Internally, the index in each dimension is searched by binary search + and then the index in the cartesian product is calculated (*not* by + constructing the cartesian product and then searching linearly over + it). + + """ + x = np.asarray(x) + is_1d = False + shape = x.shape + if len(shape) == 1: + is_1d = True + x = x[np.newaxis] + types = [type(e[0]) for e in nodes] + dtype = np.result_type(*types) + nodes = tuple(np.asarray(e, dtype=dtype) for e in nodes) + + n = shape[1-is_1d] + if len(nodes) != n: + msg = 'point `x`' if is_1d else 'points in `x`' + msg += ' must have same length as `nodes`' + raise ValueError(msg) + + out = _cartesian_nearest_indices(x, nodes, order=order) + if is_1d: + return out[0] + return out + + +@njit(cache=True) +def _cartesian_nearest_indices(X, nodes, order='C'): + """ + The main body of `cartesian_nearest_index`, jit-complied by Numba. + Note that `X` must be a 2-dim ndarray, and a Python list is not + accepted for `nodes`. + + Parameters + ---------- + X : ndarray(ndim=2) + Points to search the closest points for. + + nodes : tuple(ndarray(ndim=1)) + Tuple of sorted ndarrays of same dtype. + + order : str, optional(default='C') + ('C' or 'F') order in which the product is enumerated. + + Returns + ------- + ndarray(int, ndim=1) + Indices of the closest points to the points in `X`. + + """ + m, n = X.shape # m vectors of length n + nums_grids = np.empty(n, dtype=np.intp) + for i in range(n): + nums_grids[i] = len(nodes[i]) + + ind = np.empty(n, dtype=np.intp) + out = np.empty(m, dtype=np.intp) + + step = -1 if order == 'F' else 1 + slice_ = slice(None, None, step) + + for t in range(m): + for i in range(n): + if X[t, i] <= nodes[i][0]: + ind[i] = 0 + elif X[t, i] >= nodes[i][-1]: + ind[i] = nums_grids[i] - 1 + else: + k = np.searchsorted(nodes[i], X[t, i]) + ind[i] = ( + k if nodes[i][k] - X[t, i] < X[t, i] - nodes[i][k-1] + else k - 1 + ) + out[t] = _cartesian_index(ind[slice_], nums_grids[slice_]) + + return out + + +@njit(cache=True) +def _cartesian_index(indices, nums_grids): + n = len(indices) + idx = 0 + de_cumprod = 1 + for i in range(1,n+1): + idx += de_cumprod * indices[n-i] + de_cumprod *= nums_grids[n-i] + return idx + + +_msg_max_size_exceeded = 'Maximum allowed size exceeded' + + +@jit(nopython=True, cache=True) +def simplex_grid(m, n): + r""" + Construct an array consisting of the integer points in the + (m-1)-dimensional simplex :math:`\{x \mid x_0 + \cdots + x_{m-1} = n + \}`, or equivalently, the m-part compositions of n, which are listed + in lexicographic order. The total number of the points (hence the + length of the output array) is L = (n+m-1)!/(n!*(m-1)!) (i.e., + (n+m-1) choose (m-1)). + + Parameters + ---------- + m : scalar(int) + Dimension of each point. Must be a positive integer. + + n : scalar(int) + Number which the coordinates of each point sum to. Must be a + nonnegative integer. + + Returns + ------- + out : ndarray(int, ndim=2) + Array of shape (L, m) containing the integer points in the + simplex, aligned in lexicographic order. + + Notes + ----- + A grid of the (m-1)-dimensional *unit* simplex with n subdivisions + along each dimension can be obtained by `simplex_grid(m, n) / n`. + + Examples + -------- + >>> simplex_grid(3, 4) + array([[0, 0, 4], + [0, 1, 3], + [0, 2, 2], + [0, 3, 1], + [0, 4, 0], + [1, 0, 3], + [1, 1, 2], + [1, 2, 1], + [1, 3, 0], + [2, 0, 2], + [2, 1, 1], + [2, 2, 0], + [3, 0, 1], + [3, 1, 0], + [4, 0, 0]]) + + >>> simplex_grid(3, 4) / 4 + array([[ 0. , 0. , 1. ], + [ 0. , 0.25, 0.75], + [ 0. , 0.5 , 0.5 ], + [ 0. , 0.75, 0.25], + [ 0. , 1. , 0. ], + [ 0.25, 0. , 0.75], + [ 0.25, 0.25, 0.5 ], + [ 0.25, 0.5 , 0.25], + [ 0.25, 0.75, 0. ], + [ 0.5 , 0. , 0.5 ], + [ 0.5 , 0.25, 0.25], + [ 0.5 , 0.5 , 0. ], + [ 0.75, 0. , 0.25], + [ 0.75, 0.25, 0. ], + [ 1. , 0. , 0. ]]) + + References + ---------- + A. Nijenhuis and H. S. Wilf, Combinatorial Algorithms, Chapter 5, + Academic Press, 1978. + + """ + L = num_compositions_jit(m, n) + if L == 0: # Overflow occured + raise ValueError(_msg_max_size_exceeded) + out = np.empty((L, m), dtype=np.int_) + + x = np.zeros(m, dtype=np.int_) + x[m-1] = n + + for j in range(m): + out[0, j] = x[j] + + h = m + + for i in range(1, L): + h -= 1 + + val = x[h] + x[h] = 0 + x[m-1] = val - 1 + x[h-1] += 1 + + for j in range(m): + out[i, j] = x[j] + + if val != 1: + h = m + + return out + + +def simplex_index(x, m, n): + r""" + Return the index of the point x in the lexicographic order of the + integer points of the (m-1)-dimensional simplex :math:`\{x \mid x_0 + + \cdots + x_{m-1} = n\}`. + + Parameters + ---------- + x : array_like(int, ndim=1) + Integer point in the simplex, i.e., an array of m nonnegative + itegers that sum to n. + + m : scalar(int) + Dimension of each point. Must be a positive integer. + + n : scalar(int) + Number which the coordinates of each point sum to. Must be a + nonnegative integer. + + Returns + ------- + idx : scalar(int) + Index of x. + + """ + if m == 1: + return 0 + + decumsum = np.cumsum(x[-1:0:-1])[::-1] + idx = num_compositions(m, n) - 1 + for i in range(m-1): + if decumsum[i] == 0: + break + idx -= num_compositions(m-i, decumsum[i]-1) + return idx + + +def num_compositions(m, n): + """ + The total number of m-part compositions of n, which is equal to + (n+m-1) choose (m-1). + + Parameters + ---------- + m : scalar(int) + Number of parts of composition. + + n : scalar(int) + Integer to decompose. + + Returns + ------- + scalar(int) + Total number of m-part compositions of n. + + """ + # docs.scipy.org/doc/scipy/reference/generated/scipy.special.comb.html + return scipy.special.comb(n+m-1, m-1, exact=True) + + +@jit(nopython=True, cache=True) +def num_compositions_jit(m, n): + """ + Numba jit version of `num_compositions`. Return `0` if the outcome + exceeds the maximum value of `np.intp`. + + """ + return comb_jit(n+m-1, m-1) diff --git a/quantecon/inequality.py b/quantecon/inequality.py index 05a07a065..40b9024e4 100644 --- a/quantecon/inequality.py +++ b/quantecon/inequality.py @@ -1,28 +1,153 @@ -# This file is not meant for public use and will be removed v0.8.0. -# Use the `quantecon` namespace for importing the objects -# included below. +""" +Implements inequality and segregation measures such as Gini, Lorenz Curve -import warnings -from . import _inequality +""" +import numpy as np +from numba import njit, prange -__all__ = ['lorenz_curve', 'gini_coefficient', 'shorrocks_index', 'rank_size'] +@njit +def lorenz_curve(y): + """ + Calculates the Lorenz Curve, a graphical representation of + the distribution of income or wealth. -def __dir__(): - return __all__ + It returns the cumulative share of people (x-axis) and + the cumulative share of income earned. + Parameters + ---------- + y : array_like(float or int, ndim=1) + Array of income/wealth for each individual. + Unordered or ordered is fine. -def __getattr__(name): - if name not in __all__: - raise AttributeError( - "`quantecon.inequality` is deprecated and has no attribute " - f"'{name}'." - ) + Returns + ------- + cum_people : array_like(float, ndim=1) + Cumulative share of people for each person index (i/n) + cum_income : array_like(float, ndim=1) + Cumulative share of income for each person index - warnings.warn(f"Please use `{name}` from the `quantecon` namespace, the" - "`quantecon.inequality` namespace is deprecated. You can use" - f" the following instead:\n `from quantecon import {name}`.", - category=DeprecationWarning, stacklevel=2) - return getattr(_inequality, name) + References + ---------- + .. [1] https://en.wikipedia.org/wiki/Lorenz_curve + + Examples + -------- + >>> a_val, n = 3, 10_000 + >>> y = np.random.pareto(a_val, size=n) + >>> f_vals, l_vals = lorenz(y) + + """ + + n = len(y) + y = np.sort(y) + s = np.zeros(n + 1) + s[1:] = np.cumsum(y) + cum_people = np.zeros(n + 1) + cum_income = np.zeros(n + 1) + for i in range(1, n + 1): + cum_people[i] = i / n + cum_income[i] = s[i] / s[n] + return cum_people, cum_income + + +@njit(parallel=True) +def gini_coefficient(y): + r""" + Implements the Gini inequality index + + Parameters + ---------- + y : array_like(float) + Array of income/wealth for each individual. + Ordered or unordered is fine + + Returns + ------- + Gini index: float + The gini index describing the inequality of the array of income/wealth + + References + ---------- + + https://en.wikipedia.org/wiki/Gini_coefficient + """ + n = len(y) + i_sum = np.zeros(n) + for i in prange(n): + for j in range(n): + i_sum[i] += abs(y[i] - y[j]) + return np.sum(i_sum) / (2 * n * np.sum(y)) + + +def shorrocks_index(A): + r""" + Implements Shorrocks mobility index + + Parameters + ---------- + A : array_like(float) + Square matrix with transition probabilities (mobility matrix) of + dimension m + + Returns + ------- + Shorrocks index: float + The Shorrocks mobility index calculated as + + .. math:: + + s(A) = \frac{m - \sum_j a_{jj} }{m - 1} \in (0, 1) + + An index equal to 0 indicates complete immobility. + + References + ---------- + .. [1] Wealth distribution and social mobility in the US: + A quantitative approach (Benhabib, Bisin, Luo, 2017). + https://www.econ.nyu.edu/user/bisina/RevisionAugust.pdf + """ + + A = np.asarray(A) # Convert to array if not already + m, n = A.shape + + if m != n: + raise ValueError('A must be a square matrix') + + diag_sum = np.diag(A).sum() + + return (m - diag_sum) / (m - 1) + + +def rank_size(data, c=1.0): + """ + Generate rank-size data corresponding to distribution data. + + Examples + -------- + >>> y = np.exp(np.random.randn(1000)) # simulate data + >>> rank_data, size_data = rank_size(y, c=0.85) + + Parameters + ---------- + data : array_like + the set of observations + c : int or float + restrict plot to top (c x 100)% of the distribution + + Returns + ------- + rank_data : array_like(float, ndim=1) + Location in the population when sorted from smallest to largest + size_data : array_like(float, ndim=1) + Size data for top (c x 100)% of the observations + """ + w = - np.sort(- data) # Reverse sort + w = w[:int(len(w) * c)] # extract top (c * 100)% + rank_data = np.arange(len(w)) + 1 + size_data = w + return rank_data, size_data + diff --git a/quantecon/ivp.py b/quantecon/ivp.py index 6f5ddda81..39cf08bd4 100644 --- a/quantecon/ivp.py +++ b/quantecon/ivp.py @@ -1,28 +1,238 @@ -# This file is not meant for public use and will be removed v0.8.0. -# Use the `quantecon` namespace for importing the objects -# included below. +r""" +Base class for solving initial value problems (IVPs) of the form: -import warnings -from . import _ivp +.. math:: + \frac{dy}{dt} = f(t,y),\ y(t_0) = y_0 -__all__ = ['IVP'] +using finite difference methods. The `quantecon.ivp` class uses various +integrators from the `scipy.integrate.ode` module to perform the +integration (i.e., solve the ODE) and parametric B-spline interpolation +from `scipy.interpolate` to approximate the value of the solution +between grid points. The `quantecon.ivp` module also provides a method +for computing the residual of the solution which can be used for +assessing the overall accuracy of the approximated solution. +""" +import numpy as np +from scipy import integrate, interpolate -def __dir__(): - return __all__ +class IVP(integrate.ode): -def __getattr__(name): - if name not in __all__: - raise AttributeError( - "`quantecon.ivp` is deprecated and has no attribute " - f"'{name}'." - ) + r""" + Creates an instance of the IVP class. - warnings.warn(f"Please use `{name}` from the `quantecon` namespace, the" - "`quantecon.ivp` namespace is deprecated. You can use" - f" the following instead:\n `from quantecon import {name}`.", - category=DeprecationWarning, stacklevel=2) + Parameters + ---------- + f : callable ``f(t, y, *f_args)`` + Right hand side of the system of equations defining the ODE. + The independent variable, ``t``, is a ``scalar``; ``y`` is + an ``ndarray`` of dependent variables with ``y.shape == + (n,)``. The function `f` should return a ``scalar``, + ``ndarray`` or ``list`` (but not a ``tuple``). + jac : callable ``jac(t, y, *jac_args)``, optional(default=None) + Jacobian of the right hand side of the system of equations + defining the ODE. - return getattr(_ivp, name) + .. :math: + + \mathcal{J}_{i,j} = \bigg[\frac{\partial f_i}{\partial y_j}\bigg] + + """ + + def __init__(self, f, jac=None): + + super(IVP, self).__init__(f, jac) + + def _integrate_fixed_trajectory(self, h, T, step, relax): + """Generates a solution trajectory of fixed length.""" + # initialize the solution using initial condition + solution = np.hstack((self.t, self.y)) + + while self.successful(): + + self.integrate(self.t + h, step, relax) + current_step = np.hstack((self.t, self.y)) + solution = np.vstack((solution, current_step)) + + if (h > 0) and (self.t >= T): + break + elif (h < 0) and (self.t <= T): + break + else: + continue + + return solution + + def _integrate_variable_trajectory(self, h, g, tol, step, relax): + """Generates a solution trajectory of variable length.""" + # initialize the solution using initial condition + solution = np.hstack((self.t, self.y)) + + while self.successful(): + + self.integrate(self.t + h, step, relax) + current_step = np.hstack((self.t, self.y)) + solution = np.vstack((solution, current_step)) + + if g(self.t, self.y, *self.f_params) < tol: + break + else: + continue + + return solution + + def _initialize_integrator(self, t0, y0, integrator, **kwargs): + """Initializes the integrator prior to integration.""" + # set the initial condition + self.set_initial_value(y0, t0) + + # select the integrator + self.set_integrator(integrator, **kwargs) + + def compute_residual(self, traj, ti, k=3, ext=2): + r""" + The residual is the difference between the derivative of the B-spline + approximation of the solution trajectory and the right-hand side of the + original ODE evaluated along the approximated solution trajectory. + + Parameters + ---------- + traj : array_like (float) + Solution trajectory providing the data points for constructing the + B-spline representation. + ti : array_like (float) + Array of values for the independent variable at which to + interpolate the value of the B-spline. + k : int, optional(default=3) + Degree of the desired B-spline. Degree must satisfy + :math:`1 \le k \le 5`. + ext : int, optional(default=2) + Controls the value of returned elements for outside the + original knot sequence provided by traj. For extrapolation, set + `ext=0`; `ext=1` returns zero; `ext=2` raises a `ValueError`. + + Returns + ------- + residual : array (float) + Difference between the derivative of the B-spline approximation + of the solution trajectory and the right-hand side of the ODE + evaluated along the approximated solution trajectory. + + """ + # B-spline approximations of the solution and its derivative + soln = self.interpolate(traj, ti, k, 0, ext) + deriv = self.interpolate(traj, ti, k, 1, ext) + + # rhs of ode evaluated along approximate solution + T = ti.size + rhs_ode = np.vstack([self.f(ti[i], soln[i, 1:], *self.f_params) + for i in range(T)]) + rhs_ode = np.hstack((ti[:, np.newaxis], rhs_ode)) + + # should be roughly zero everywhere (if approximation is any good!) + residual = deriv - rhs_ode + + return residual + + def solve(self, t0, y0, h=1.0, T=None, g=None, tol=None, + integrator='dopri5', step=False, relax=False, **kwargs): + r""" + Solve the IVP by integrating the ODE given some initial condition. + + Parameters + ---------- + t0 : float + Initial condition for the independent variable. + y0 : array_like (float, shape=(n,)) + Initial condition for the dependent variables. + h : float, optional(default=1.0) + Step-size for computing the solution. Can be positive or negative + depending on the desired direction of integration. + T : int, optional(default=None) + Terminal value for the independent variable. One of either `T` + or `g` must be specified. + g : callable ``g(t, y, f_args)``, optional(default=None) + Provides a stopping condition for the integration. If specified + user must also specify a stopping tolerance, `tol`. + tol : float, optional (default=None) + Stopping tolerance for the integration. Only required if `g` is + also specifed. + integrator : str, optional(default='dopri5') + Must be one of 'vode', 'lsoda', 'dopri5', or 'dop853' + step : bool, optional(default=False) + Allows access to internal steps for those solvers that use adaptive + step size routines. Currently only 'vode', 'zvode', and 'lsoda' + support `step=True`. + relax : bool, optional(default=False) + Currently only 'vode', 'zvode', and 'lsoda' support `relax=True`. + **kwargs : dict, optional(default=None) + Dictionary of integrator specific keyword arguments. See the + Notes section of the docstring for `scipy.integrate.ode` for a + complete description of solver specific keyword arguments. + + Returns + ------- + solution: ndarray (float) + Simulated solution trajectory. + + """ + self._initialize_integrator(t0, y0, integrator, **kwargs) + + if (g is not None) and (tol is not None): + soln = self._integrate_variable_trajectory(h, g, tol, step, relax) + elif T is not None: + soln = self._integrate_fixed_trajectory(h, T, step, relax) + else: + mesg = "Either both 'g' and 'tol', or 'T' must be specified." + raise ValueError(mesg) + + return soln + + def interpolate(self, traj, ti, k=3, der=0, ext=2): + r""" + Parametric B-spline interpolation in N-dimensions. + + Parameters + ---------- + traj : array_like (float) + Solution trajectory providing the data points for constructing the + B-spline representation. + ti : array_like (float) + Array of values for the independent variable at which to + interpolate the value of the B-spline. + k : int, optional(default=3) + Degree of the desired B-spline. Degree must satisfy + :math:`1 \le k \le 5`. + der : int, optional(default=0) + The order of derivative of the spline to compute (must be less + than or equal to `k`). + ext : int, optional(default=2) Controls the value of returned elements + for outside the original knot sequence provided by traj. For + extrapolation, set `ext=0`; `ext=1` returns zero; `ext=2` raises a + `ValueError`. + + Returns + ------- + interp_traj: ndarray (float) + The interpolated trajectory. + + """ + # array of parameter values + u = traj[:, 0] + + # build list of input arrays + n = traj.shape[1] + x = [traj[:, i] for i in range(1, n)] + + # construct the B-spline representation (s=0 forces interpolation!) + tck, t = interpolate.splprep(x, u=u, k=k, s=0) + + # evaluate the B-spline (returns a list) + out = interpolate.splev(ti, tck, der, ext) + + # convert to a 2D array + interp_traj = np.hstack((ti[:, np.newaxis], np.array(out).T)) + + return interp_traj diff --git a/quantecon/kalman.py b/quantecon/kalman.py index 6d6a99696..ba30b727a 100644 --- a/quantecon/kalman.py +++ b/quantecon/kalman.py @@ -1,28 +1,321 @@ -# This file is not meant for public use and will be removed v0.8.0. -# Use the `quantecon` namespace for importing the objects -# included below. +""" +Implements the Kalman filter for a linear Gaussian state space model. -import warnings -from . import _kalman +References +---------- +https://lectures.quantecon.org/py/kalman.html -__all__ = ['Kalman'] +""" +from textwrap import dedent +import numpy as np +from scipy.linalg import inv +from quantecon.lss import LinearStateSpace +from quantecon.matrix_eqn import solve_discrete_riccati -def __dir__(): - return __all__ +class Kalman: + r""" + Implements the Kalman filter for the Gaussian state space model + .. math:: -def __getattr__(name): - if name not in __all__: - raise AttributeError( - "`quantecon.kalman` is deprecated and has no attribute " - f"'{name}'." - ) + x_{t+1} = A x_t + C w_{t+1} \\ + y_t = G x_t + H v_t - warnings.warn(f"Please use `{name}` from the `quantecon` namespace, the" - "`quantecon.kalman` namespace is deprecated. You can use" - f" the following instead:\n `from quantecon import {name}`.", - category=DeprecationWarning, stacklevel=2) + Here :math:`x_t` is the hidden state and :math:`y_t` is the measurement. + The shocks :math:`w_t` and :math:`v_t` are iid standard normals. Below + we use the notation - return getattr(_kalman, name) + .. math:: + + Q := CC' + R := HH' + + + Parameters + ---------- + ss : instance of LinearStateSpace + An instance of the quantecon.lss.LinearStateSpace class + x_hat : scalar(float) or array_like(float), optional(default=None) + An n x 1 array representing the mean x_hat of the + prior/predictive density. Set to zero if not supplied. + Sigma : scalar(float) or array_like(float), optional(default=None) + An n x n array representing the covariance matrix Sigma of + the prior/predictive density. Must be positive definite. + Set to the identity if not supplied. + + Attributes + ---------- + Sigma, x_hat : as above + Sigma_infinity : array_like or scalar(float) + The infinite limit of Sigma_t + K_infinity : array_like or scalar(float) + The stationary Kalman gain. + + + References + ---------- + + https://lectures.quantecon.org/py/kalman.html + + """ + + def __init__(self, ss, x_hat=None, Sigma=None): + self.ss = ss + self.set_state(x_hat, Sigma) + self._K_infinity = None + self._Sigma_infinity = None + + def set_state(self, x_hat, Sigma): + if Sigma is None: + self.Sigma = np.identity(self.ss.n) + else: + self.Sigma = np.atleast_2d(Sigma) + if x_hat is None: + self.x_hat = np.zeros((self.ss.n, 1)) + else: + self.x_hat = np.atleast_2d(x_hat) + self.x_hat.shape = self.ss.n, 1 + + def __repr__(self): + return self.__str__() + + def __str__(self): + m = """\ + Kalman filter: + - dimension of state space : {n} + - dimension of observation equation : {k} + """ + return dedent(m.format(n=self.ss.n, k=self.ss.k)) + + @property + def Sigma_infinity(self): + if self._Sigma_infinity is None: + self.stationary_values() + return self._Sigma_infinity + + @property + def K_infinity(self): + if self._K_infinity is None: + self.stationary_values() + return self._K_infinity + + def whitener_lss(self): + r""" + This function takes the linear state space system + that is an input to the Kalman class and it converts + that system to the time-invariant whitener represenation + given by + + .. math:: + + \tilde{x}_{t+1}^* = \tilde{A} \tilde{x} + \tilde{C} v + a = \tilde{G} \tilde{x} + + where + + .. math:: + + \tilde{x}_t = [x+{t}, \hat{x}_{t}, v_{t}] + + and + + .. math:: + + \tilde{A} = + \begin{bmatrix} + A & 0 & 0 \\ + KG & A-KG & KH \\ + 0 & 0 & 0 \\ + \end{bmatrix} + + .. math:: + + \tilde{C} = + \begin{bmatrix} + C & 0 \\ + 0 & 0 \\ + 0 & I \\ + \end{bmatrix} + + .. math:: + + \tilde{G} = + \begin{bmatrix} + G & -G & H \\ + \end{bmatrix} + + with :math:`A, C, G, H` coming from the linear state space system + that defines the Kalman instance + + Returns + ------- + whitened_lss : LinearStateSpace + This is the linear state space system that represents + the whitened system + """ + K = self.K_infinity + + # Get the matrix sizes + n, k, m, l = self.ss.n, self.ss.k, self.ss.m, self.ss.l + A, C, G, H = self.ss.A, self.ss.C, self.ss.G, self.ss.H + + Atil = np.vstack([np.hstack([A, np.zeros((n, n)), np.zeros((n, l))]), + np.hstack([np.dot(K, G), + A-np.dot(K, G), + np.dot(K, H)]), + np.zeros((l, 2*n + l))]) + + Ctil = np.vstack([np.hstack([C, np.zeros((n, l))]), + np.zeros((n, m+l)), + np.hstack([np.zeros((l, m)), np.eye(l)])]) + + Gtil = np.hstack([G, -G, H]) + + whitened_lss = LinearStateSpace(Atil, Ctil, Gtil) + self.whitened_lss = whitened_lss + + return whitened_lss + + def prior_to_filtered(self, y): + r""" + Updates the moments (x_hat, Sigma) of the time t prior to the + time t filtering distribution, using current measurement :math:`y_t`. + + The updates are according to + + .. math:: + + \hat{x}^F = \hat{x} + \Sigma G' (G \Sigma G' + R)^{-1} + (y - G \hat{x}) + \Sigma^F = \Sigma - \Sigma G' (G \Sigma G' + R)^{-1} G + \Sigma + + Parameters + ---------- + y : scalar or array_like(float) + The current measurement + + """ + # === simplify notation === # + G, H = self.ss.G, self.ss.H + R = np.dot(H, H.T) + + # === and then update === # + y = np.atleast_2d(y) + y.shape = self.ss.k, 1 + E = np.dot(self.Sigma, G.T) + F = np.dot(np.dot(G, self.Sigma), G.T) + R + M = np.dot(E, inv(F)) + self.x_hat = self.x_hat + np.dot(M, (y - np.dot(G, self.x_hat))) + self.Sigma = self.Sigma - np.dot(M, np.dot(G, self.Sigma)) + + def filtered_to_forecast(self): + """ + Updates the moments of the time t filtering distribution to the + moments of the predictive distribution, which becomes the time + t+1 prior + + """ + # === simplify notation === # + A, C = self.ss.A, self.ss.C + Q = np.dot(C, C.T) + + # === and then update === # + self.x_hat = np.dot(A, self.x_hat) + self.Sigma = np.dot(A, np.dot(self.Sigma, A.T)) + Q + + def update(self, y): + """ + Updates x_hat and Sigma given k x 1 ndarray y. The full + update, from one period to the next + + Parameters + ---------- + y : np.ndarray + A k x 1 ndarray y representing the current measurement + + """ + self.prior_to_filtered(y) + self.filtered_to_forecast() + + def stationary_values(self, method='doubling'): + r""" + Computes the limit of :math:`\Sigma_t` as t goes to infinity by + solving the associated Riccati equation. The outputs are stored in the + attributes `K_infinity` and `Sigma_infinity`. Computation is via the + doubling algorithm (default) or a QZ decomposition method (see the + documentation in `matrix_eqn.solve_discrete_riccati`). + + Parameters + ---------- + method : str, optional(default="doubling") + Solution method used in solving the associated Riccati + equation, str in {'doubling', 'qz'}. + + Returns + ------- + Sigma_infinity : array_like or scalar(float) + The infinite limit of :math:`\Sigma_t` + K_infinity : array_like or scalar(float) + The stationary Kalman gain. + + """ + + # === simplify notation === # + A, C, G, H = self.ss.A, self.ss.C, self.ss.G, self.ss.H + Q, R = np.dot(C, C.T), np.dot(H, H.T) + + # === solve Riccati equation, obtain Kalman gain === # + Sigma_infinity = solve_discrete_riccati(A.T, G.T, Q, R, method=method) + temp1 = np.dot(np.dot(A, Sigma_infinity), G.T) + temp2 = inv(np.dot(G, np.dot(Sigma_infinity, G.T)) + R) + K_infinity = np.dot(temp1, temp2) + + # == record as attributes and return == # + self._Sigma_infinity, self._K_infinity = Sigma_infinity, K_infinity + return Sigma_infinity, K_infinity + + def stationary_coefficients(self, j, coeff_type='ma'): + """ + Wold representation moving average or VAR coefficients for the + steady state Kalman filter. + + Parameters + ---------- + j : int + The lag length + coeff_type : string, either 'ma' or 'var' (default='ma') + The type of coefficent sequence to compute. Either 'ma' for + moving average or 'var' for VAR. + """ + # == simplify notation == # + A, G = self.ss.A, self.ss.G + K_infinity = self.K_infinity + # == compute and return coefficients == # + coeffs = [] + i = 1 + if coeff_type == 'ma': + coeffs.append(np.identity(self.ss.k)) + P_mat = A + P = np.identity(self.ss.n) # Create a copy + elif coeff_type == 'var': + coeffs.append(np.dot(G, K_infinity)) + P_mat = A - np.dot(K_infinity, G) + P = np.copy(P_mat) # Create a copy + else: + raise ValueError("Unknown coefficient type") + while i <= j: + coeffs.append(np.dot(np.dot(G, P), K_infinity)) + P = np.dot(P, P_mat) + i += 1 + return coeffs + + def stationary_innovation_covar(self): + # == simplify notation == # + H, G = self.ss.H, self.ss.G + R = np.dot(H, H.T) + Sigma_infinity = self.Sigma_infinity + + return np.dot(G, np.dot(Sigma_infinity, G.T)) + R diff --git a/quantecon/lae.py b/quantecon/lae.py index bef3a0bcd..83b59c87c 100644 --- a/quantecon/lae.py +++ b/quantecon/lae.py @@ -1,28 +1,83 @@ -# This file is not meant for public use and will be removed v0.8.0. -# Use the `quantecon` namespace for importing the objects -# included below. +r""" +Computes a sequence of marginal densities for a continuous state space +Markov chain :math:`X_t` where the transition probabilities can be represented +as densities. The estimate of the marginal density of :math:`X_t` is -import warnings -from . import _lae +.. math:: + \frac{1}{n} \sum_{i=0}^n p(X_{t-1}^i, y) -__all__ = ['LAE'] +This is a density in :math:`y`. +References +---------- -def __dir__(): - return __all__ +https://lectures.quantecon.org/py/stationary_densities.html +""" +from textwrap import dedent +import numpy as np -def __getattr__(name): - if name not in __all__: - raise AttributeError( - "`quantecon.lae` is deprecated and has no attribute " - f"'{name}'." - ) - warnings.warn(f"Please use `{name}` from the `quantecon` namespace, the" - "`quantecon.lae` namespace is deprecated. You can use" - f" the following instead:\n `from quantecon import {name}`.", - category=DeprecationWarning, stacklevel=2) +class LAE: + """ + An instance is a representation of a look ahead estimator associated + with a given stochastic kernel p and a vector of observations X. - return getattr(_lae, name) + Parameters + ---------- + p : function + The stochastic kernel. A function p(x, y) that is vectorized in + both x and y + X : array_like(float) + A vector containing observations + + Attributes + ---------- + p, X : see Parameters + + Examples + -------- + >>> psi = LAE(p, X) + >>> y = np.linspace(0, 1, 100) + >>> psi(y) # Evaluate look ahead estimate at grid of points y + + """ + + def __init__(self, p, X): + X = X.flatten() # So we know what we're dealing with + n = len(X) + self.p, self.X = p, X.reshape((n, 1)) + + def __repr__(self): + return self.__str__() + + def __str__(self): + m = """\ + Look ahead estimator + - number of observations : {n} + """ + return dedent(m.format(n=self.X.size)) + + def __call__(self, y): + """ + A vectorized function that returns the value of the look ahead + estimate at the values in the array y. + + Parameters + ---------- + y : array_like(float) + A vector of points at which we wish to evaluate the look- + ahead estimator + + Returns + ------- + psi_vals : array_like(float) + The values of the density estimate at the points in y + + """ + k = len(y) + v = self.p(self.X, y.reshape((1, k))) + psi_vals = np.mean(v, axis=0) # Take mean along each row + + return psi_vals.flatten() diff --git a/quantecon/lqcontrol.py b/quantecon/lqcontrol.py index d81953ec1..7c1ff23f1 100644 --- a/quantecon/lqcontrol.py +++ b/quantecon/lqcontrol.py @@ -1,28 +1,624 @@ -# This file is not meant for public use and will be removed v0.8.0. -# Use the `quantecon` namespace for importing the objects -# included below. +""" +Provides a class called LQ for solving linear quadratic control +problems, and a class called LQMarkov for solving Markov jump +linear quadratic control problems. -import warnings -from . import _lqcontrol +""" +from textwrap import dedent +import numpy as np +from scipy.linalg import solve +from .matrix_eqn import solve_discrete_riccati, solve_discrete_riccati_system +from .util import check_random_state +from .markov import MarkovChain -__all__ = ['LQ', 'LQMarkov'] +class LQ: + r""" + This class is for analyzing linear quadratic optimal control + problems of either the infinite horizon form + .. math:: -def __dir__(): - return __all__ + \min \mathbb{E} + \Big[ \sum_{t=0}^{\infty} \beta^t r(x_t, u_t) \Big] + with -def __getattr__(name): - if name not in __all__: - raise AttributeError( - "`quantecon.lqcontrol` is deprecated and has no attribute " - f"'{name}'." - ) + .. math:: - warnings.warn(f"Please use `{name}` from the `quantecon` namespace, the " - "`quantecon.lqcontrol` namespace is deprecated. You can use" - f" the following instead:\n `from quantecon import {name}`.", - category=DeprecationWarning, stacklevel=2) + r(x_t, u_t) := x_t' R x_t + u_t' Q u_t + 2 u_t' N x_t - return getattr(_lqcontrol, name) + or the finite horizon form + + .. math:: + + \min \mathbb{E} + \Big[ + \sum_{t=0}^{T-1} \beta^t r(x_t, u_t) + \beta^T x_T' R_f x_T + \Big] + + Both are minimized subject to the law of motion + + .. math:: + + x_{t+1} = A x_t + B u_t + C w_{t+1} + + Here :math:`x` is n x 1, :math:`u` is k x 1, :math:`w` is j x 1 and the + matrices are conformable for these dimensions. The sequence :math:`{w_t}` + is assumed to be white noise, with zero mean and + :math:`\mathbb{E} [ w_t' w_t ] = I`, the j x j identity. + + If :math:`C` is not supplied as a parameter, the model is assumed to be + deterministic (and :math:`C` is set to a zero matrix of appropriate + dimension). + + For this model, the time t value (i.e., cost-to-go) function :math:`V_t` + takes the form + + .. math:: + + x' P_T x + d_T + + and the optimal policy is of the form :math:`u_T = -F_T x_T`. In the + infinite horizon case, :math:`V, P, d` and :math:`F` are all stationary. + + Parameters + ---------- + Q : array_like(float) + Q is the payoff (or cost) matrix that corresponds with the + control variable u and is k x k. Should be symmetric and + non-negative definite + R : array_like(float) + R is the payoff (or cost) matrix that corresponds with the + state variable x and is n x n. Should be symmetric and + non-negative definite + A : array_like(float) + A is part of the state transition as described above. It should + be n x n + B : array_like(float) + B is part of the state transition as described above. It should + be n x k + C : array_like(float), optional(default=None) + C is part of the state transition as described above and + corresponds to the random variable today. If the model is + deterministic then C should take default value of None + N : array_like(float), optional(default=None) + N is the cross product term in the payoff, as above. It should + be k x n. + beta : scalar(float), optional(default=1) + beta is the discount parameter + T : scalar(int), optional(default=None) + T is the number of periods in a finite horizon problem. + Rf : array_like(float), optional(default=None) + Rf is the final (in a finite horizon model) payoff(or cost) + matrix that corresponds with the control variable u and is n x + n. Should be symmetric and non-negative definite + + Attributes + ---------- + Q, R, N, A, B, C, beta, T, Rf : see Parameters + P : array_like(float) + P is part of the value function representation of + :math:`V(x) = x'Px + d` + d : array_like(float) + d is part of the value function representation of + :math:`V(x) = x'Px + d` + F : array_like(float) + F is the policy rule that determines the choice of control in + each period. + k, n, j : scalar(int) + The dimensions of the matrices as presented above + + """ + + def __init__(self, Q, R, A, B, C=None, N=None, beta=1, T=None, Rf=None): + # == Make sure all matrices can be treated as 2D arrays == # + converter = lambda X: np.atleast_2d(np.asarray(X, dtype='float')) + self.A, self.B, self.Q, self.R, self.N = list(map(converter, + (A, B, Q, R, N))) + # == Record dimensions == # + self.k, self.n = self.Q.shape[0], self.R.shape[0] + + self.beta = beta + + if C is None: + # == If C not given, then model is deterministic. Set C=0. == # + self.j = 1 + self.C = np.zeros((self.n, self.j)) + else: + self.C = converter(C) + self.j = self.C.shape[1] + + if N is None: + # == No cross product term in payoff. Set N=0. == # + self.N = np.zeros((self.k, self.n)) + + if T: + # == Model is finite horizon == # + self.T = T + self.Rf = np.asarray(Rf, dtype='float') + self.P = self.Rf + self.d = 0 + else: + self.P = None + self.d = None + self.T = None + + if (self.C != 0).any() and beta >= 1: + raise ValueError('beta must be strictly smaller than 1 if ' + + 'T = None and C != 0.') + + self.F = None + + def __repr__(self): + return self.__str__() + + def __str__(self): + m = """\ + Linear Quadratic control system + - beta (discount parameter) : {b} + - T (time horizon) : {t} + - n (number of state variables) : {n} + - k (number of control variables) : {k} + - j (number of shocks) : {j} + """ + t = "infinite" if self.T is None else self.T + return dedent(m.format(b=self.beta, n=self.n, k=self.k, j=self.j, + t=t)) + + def update_values(self): + """ + This method is for updating in the finite horizon case. It + shifts the current value function + + .. math:: + + V_t(x) = x' P_t x + d_t + + and the optimal policy :math:`F_t` one step *back* in time, + replacing the pair :math:`P_t` and :math:`d_t` with + :math:`P_{t-1}` and :math:`d_{t-1}`, and :math:`F_t` with + :math:`F_{t-1}` + + """ + # === Simplify notation === # + Q, R, A, B, N, C = self.Q, self.R, self.A, self.B, self.N, self.C + P, d = self.P, self.d + # == Some useful matrices == # + S1 = Q + self.beta * np.dot(B.T, np.dot(P, B)) + S2 = self.beta * np.dot(B.T, np.dot(P, A)) + N + S3 = self.beta * np.dot(A.T, np.dot(P, A)) + # == Compute F as (Q + B'PB)^{-1} (beta B'PA + N) == # + self.F = solve(S1, S2) + # === Shift P back in time one step == # + new_P = R - np.dot(S2.T, self.F) + S3 + # == Recalling that trace(AB) = trace(BA) == # + new_d = self.beta * (d + np.trace(np.dot(P, np.dot(C, C.T)))) + # == Set new state == # + self.P, self.d = new_P, new_d + + def stationary_values(self, method='doubling'): + """ + Computes the matrix :math:`P` and scalar :math:`d` that represent + the value function + + .. math:: + + V(x) = x' P x + d + + in the infinite horizon case. Also computes the control matrix + :math:`F` from :math:`u = - Fx`. Computation is via the solution + algorithm as specified by the `method` option (default to the + doubling algorithm) (see the documentation in + `matrix_eqn.solve_discrete_riccati`). + + Parameters + ---------- + method : str, optional(default='doubling') + Solution method used in solving the associated Riccati + equation, str in {'doubling', 'qz'}. + + Returns + ------- + P : array_like(float) + P is part of the value function representation of + :math:`V(x) = x'Px + d` + F : array_like(float) + F is the policy rule that determines the choice of control + in each period. + d : array_like(float) + d is part of the value function representation of + :math:`V(x) = x'Px + d` + + """ + # === simplify notation === # + Q, R, A, B, N, C = self.Q, self.R, self.A, self.B, self.N, self.C + + # === solve Riccati equation, obtain P === # + A0, B0 = np.sqrt(self.beta) * A, np.sqrt(self.beta) * B + P = solve_discrete_riccati(A0, B0, R, Q, N, method=method) + + # == Compute F == # + S1 = Q + self.beta * np.dot(B.T, np.dot(P, B)) + S2 = self.beta * np.dot(B.T, np.dot(P, A)) + N + F = solve(S1, S2) + + # == Compute d == # + if self.beta == 1: + d = 0 + else: + d = self.beta * np.trace(np.dot(P, np.dot(C, C.T))) / (1 - self.beta) + + # == Bind states and return values == # + self.P, self.F, self.d = P, F, d + + return P, F, d + + def compute_sequence(self, x0, ts_length=None, method='doubling', + random_state=None): + """ + Compute and return the optimal state and control sequences + :math:`x_0, ..., x_T` and :math:`u_0,..., u_T` under the + assumption that :math:`{w_t}` is iid and :math:`N(0, 1)`. + + Parameters + ---------- + x0 : array_like(float) + The initial state, a vector of length n + + ts_length : scalar(int) + Length of the simulation -- defaults to T in finite case + + method : str, optional(default='doubling') + Solution method used in solving the associated Riccati + equation, str in {'doubling', 'qz'}. Only relevant when the + `T` attribute is `None` (i.e., the horizon is infinite). + + random_state : int or np.random.RandomState/Generator, optional + Random seed (integer) or np.random.RandomState or Generator + instance to set the initial state of the random number + generator for reproducibility. If None, a randomly + initialized RandomState is used. + + Returns + ------- + x_path : array_like(float) + An n x T+1 matrix, where the t-th column represents :math:`x_t` + + u_path : array_like(float) + A k x T matrix, where the t-th column represents :math:`u_t` + + w_path : array_like(float) + A j x T+1 matrix, where the t-th column represent :math:`w_t` + + """ + + # === Simplify notation === # + A, B, C = self.A, self.B, self.C + + # == Preliminaries, finite horizon case == # + if self.T: + T = self.T if not ts_length else min(ts_length, self.T) + self.P, self.d = self.Rf, 0 + + # == Preliminaries, infinite horizon case == # + else: + T = ts_length if ts_length else 100 + if self.P is None: + self.stationary_values(method=method) + + # == Set up initial condition and arrays to store paths == # + random_state = check_random_state(random_state) + x0 = np.asarray(x0) + x0 = x0.reshape(self.n, 1) # Make sure x0 is a column vector + x_path = np.empty((self.n, T+1)) + u_path = np.empty((self.k, T)) + w_path = random_state.standard_normal((self.j, T+1)) + Cw_path = np.dot(C, w_path) + + # == Compute and record the sequence of policies == # + policies = [] + for t in range(T): + if self.T: # Finite horizon case + self.update_values() + policies.append(self.F) + + # == Use policy sequence to generate states and controls == # + F = policies.pop() + x_path[:, 0] = x0.flatten() + u_path[:, 0] = - np.dot(F, x0).flatten() + for t in range(1, T): + F = policies.pop() + Ax, Bu = np.dot(A, x_path[:, t-1]), np.dot(B, u_path[:, t-1]) + x_path[:, t] = Ax + Bu + Cw_path[:, t] + u_path[:, t] = - np.dot(F, x_path[:, t]) + Ax, Bu = np.dot(A, x_path[:, T-1]), np.dot(B, u_path[:, T-1]) + x_path[:, T] = Ax + Bu + Cw_path[:, T] + + return x_path, u_path, w_path + + +class LQMarkov: + r""" + This class is for analyzing Markov jump linear quadratic optimal + control problems of the infinite horizon form + + .. math:: + + \min \mathbb{E} + \Big[ \sum_{t=0}^{\infty} \beta^t r(x_t, s_t, u_t) \Big] + + with + + .. math:: + + r(x_t, s_t, u_t) := + (x_t' R(s_t) x_t + u_t' Q(s_t) u_t + 2 u_t' N(s_t) x_t) + + subject to the law of motion + + .. math:: + + x_{t+1} = A(s_t) x_t + B(s_t) u_t + C(s_t) w_{t+1} + + Here :math:`x` is n x 1, :math:`u` is k x 1, :math:`w` is j x 1 and the + matrices are conformable for these dimensions. The sequence :math:`{w_t}` + is assumed to be white noise, with zero mean and + :math:`\mathbb{E} [ w_t' w_t ] = I`, the j x j identity. + + If :math:`C` is not supplied as a parameter, the model is assumed to be + deterministic (and :math:`C` is set to a zero matrix of appropriate + dimension). + + The optimal value function :math:`V(x_t, s_t)` takes the form + + .. math:: + + x_t' P(s_t) x_t + d(s_t) + + and the optimal policy is of the form :math:`u_t = -F(s_t) x_t`. + + Parameters + ---------- + Π : array_like(float, ndim=2) + The Markov chain transition matrix with dimension m x m. + Qs : array_like(float) + Consists of m symmetric and non-negative definite payoff + matrices Q(s) with dimension k x k that corresponds with + the control variable u for each Markov state s + Rs : array_like(float) + Consists of m symmetric and non-negative definite payoff + matrices R(s) with dimension n x n that corresponds with + the state variable x for each Markov state s + As : array_like(float) + Consists of m state transition matrices A(s) with dimension + n x n for each Markov state s + Bs : array_like(float) + Consists of m state transition matrices B(s) with dimension + n x k for each Markov state s + Cs : array_like(float), optional(default=None) + Consists of m state transition matrices C(s) with dimension + n x j for each Markov state s. If the model is deterministic + then Cs should take default value of None + Ns : array_like(float), optional(default=None) + Consists of m cross product term matrices N(s) with dimension + k x n for each Markov state, + beta : scalar(float), optional(default=1) + beta is the discount parameter + + Attributes + ---------- + Π, Qs, Rs, Ns, As, Bs, Cs, beta : see Parameters + Ps : array_like(float) + Ps is part of the value function representation of + :math:`V(x, s) = x' P(s) x + d(s)` + ds : array_like(float) + ds is part of the value function representation of + :math:`V(x, s) = x' P(s) x + d(s)` + Fs : array_like(float) + Fs is the policy rule that determines the choice of control in + each period at each Markov state + m : scalar(int) + The number of Markov states + k, n, j : scalar(int) + The dimensions of the matrices as presented above + + """ + + def __init__(self, Π, Qs, Rs, As, Bs, Cs=None, Ns=None, beta=1): + + # == Make sure all matrices for each state are 2D arrays == # + def converter(Xs): + return np.array([np.atleast_2d(np.asarray(X, dtype='float')) + for X in Xs]) + self.As, self.Bs, self.Qs, self.Rs = list(map(converter, + (As, Bs, Qs, Rs))) + + # == Record number of states == # + self.m = self.Qs.shape[0] + # == Record dimensions == # + self.k, self.n = self.Qs.shape[1], self.Rs.shape[1] + + if Ns is None: + # == No cross product term in payoff. Set N=0. == # + Ns = [np.zeros((self.k, self.n)) for i in range(self.m)] + + self.Ns = converter(Ns) + + if Cs is None: + # == If C not given, then model is deterministic. Set C=0. == # + self.j = 1 + Cs = [np.zeros((self.n, self.j)) for i in range(self.m)] + + self.Cs = converter(Cs) + self.j = self.Cs.shape[2] + + self.beta = beta + + self.Π = np.asarray(Π, dtype='float') + + self.Ps = None + self.ds = None + self.Fs = None + + def __repr__(self): + return self.__str__() + + def __str__(self): + m = """\ + Markov Jump Linear Quadratic control system + - beta (discount parameter) : {b} + - T (time horizon) : {t} + - m (number of Markov states) : {m} + - n (number of state variables) : {n} + - k (number of control variables) : {k} + - j (number of shocks) : {j} + """ + t = "infinite" + return dedent(m.format(b=self.beta, m=self.m, n=self.n, k=self.k, + j=self.j, t=t)) + + def stationary_values(self, max_iter=1000): + """ + Computes the matrix :math:`P(s)` and scalar :math:`d(s)` that + represent the value function + + .. math:: + + V(x, s) = x' P(s) x + d(s) + + in the infinite horizon case. Also computes the control matrix + :math:`F` from :math:`u = - F(s) x`. + + Parameters + ---------- + max_iter : scalar(int), optional(default=1000) + The maximum number of iterations allowed + + Returns + ------- + Ps : array_like(float) + Ps is part of the value function representation of + :math:`V(x, s) = x' P(s) x + d(s)` + ds : array_like(float) + ds is part of the value function representation of + :math:`V(x, s) = x' P(s) x + d(s)` + Fs : array_like(float) + Fs is the policy rule that determines the choice of control in + each period at each Markov state + + """ + + # == Simplify notations == # + beta, Π = self.beta, self.Π + m, n, k = self.m, self.n, self.k + As, Bs, Cs = self.As, self.Bs, self.Cs + Qs, Rs, Ns = self.Qs, self.Rs, self.Ns + + # == Solve for P(s) by iterating discrete riccati system== # + Ps = solve_discrete_riccati_system(Π, As, Bs, Cs, Qs, Rs, Ns, beta, + max_iter=max_iter) + + # == calculate F and d == # + Fs = np.array([np.empty((k, n)) for i in range(m)]) + X = np.empty((m, m)) + sum1, sum2 = np.empty((k, k)), np.empty((k, n)) + for i in range(m): + # CCi = C_i C_i' + CCi = Cs[i] @ Cs[i].T + sum1[:, :] = 0. + sum2[:, :] = 0. + for j in range(m): + # for F + sum1 += beta * Π[i, j] * Bs[i].T @ Ps[j] @ Bs[i] + sum2 += beta * Π[i, j] * Bs[i].T @ Ps[j] @ As[i] + + # for d + X[j, i] = np.trace(Ps[j] @ CCi) + + Fs[i][:, :] = solve(Qs[i] + sum1, sum2 + Ns[i]) + + ds = solve(np.eye(m) - beta * Π, + np.diag(beta * Π @ X).reshape((m, 1))).flatten() + + self.Ps, self.ds, self.Fs = Ps, ds, Fs + + return Ps, ds, Fs + + def compute_sequence(self, x0, ts_length=None, random_state=None): + """ + Compute and return the optimal state and control sequences + :math:`x_0, ..., x_T` and :math:`u_0,..., u_T` under the + assumption that :math:`{w_t}` is iid and :math:`N(0, 1)`, + with Markov states sequence :math:`s_0, ..., s_T` + + Parameters + ---------- + x0 : array_like(float) + The initial state, a vector of length n + + ts_length : scalar(int), optional(default=None) + Length of the simulation. If None, T is set to be 100 + + random_state : int or np.random.RandomState/Generator, optional + Random seed (integer) or np.random.RandomState or Generator + instance to set the initial state of the random number + generator for reproducibility. If None, a randomly + initialized RandomState is used. + + Returns + ------- + x_path : array_like(float) + An n x T+1 matrix, where the t-th column represents :math:`x_t` + + u_path : array_like(float) + A k x T matrix, where the t-th column represents :math:`u_t` + + w_path : array_like(float) + A j x T+1 matrix, where the t-th column represent :math:`w_t` + + state : array_like(int) + Array containing the state values :math:`s_t` of the sample path + + """ + + # === solve for optimal policies === # + if self.Ps is None: + self.stationary_values() + + # === Simplify notation === # + As, Bs, Cs = self.As, self.Bs, self.Cs + Fs = self.Fs + + random_state = check_random_state(random_state) + x0 = np.asarray(x0) + x0 = x0.reshape(self.n, 1) + + T = ts_length if ts_length else 100 + + # == Simulate Markov states == # + chain = MarkovChain(self.Π) + state = chain.simulate_indices(ts_length=T+1, + random_state=random_state) + + # == Prepare storage arrays == # + x_path = np.empty((self.n, T+1)) + u_path = np.empty((self.k, T)) + w_path = random_state.standard_normal((self.j, T+1)) + Cw_path = np.empty((self.n, T+1)) + for i in range(T+1): + Cw_path[:, i] = Cs[state[i]] @ w_path[:, i] + + # == Use policy sequence to generate states and controls == # + x_path[:, 0] = x0.flatten() + u_path[:, 0] = - (Fs[state[0]] @ x0).flatten() + for t in range(1, T): + Ax = As[state[t]] @ x_path[:, t-1] + Bu = Bs[state[t]] @ u_path[:, t-1] + x_path[:, t] = Ax + Bu + Cw_path[:, t] + u_path[:, t] = - (Fs[state[t]] @ x_path[:, t]) + Ax = As[state[T]] @ x_path[:, T-1] + Bu = Bs[state[T]] @ u_path[:, T-1] + x_path[:, T] = Ax + Bu + Cw_path[:, T] + + return x_path, u_path, w_path, state diff --git a/quantecon/lqnash.py b/quantecon/lqnash.py index bc2d622f9..2b2fbbbe0 100644 --- a/quantecon/lqnash.py +++ b/quantecon/lqnash.py @@ -1,28 +1,153 @@ -# This file is not meant for public use and will be removed v0.8.0. -# Use the `quantecon` namespace for importing the objects -# included below. +import numpy as np +from scipy.linalg import solve +from .util import check_random_state -import warnings -from . import _lqnash +def nnash(A, B1, B2, R1, R2, Q1, Q2, S1, S2, W1, W2, M1, M2, + beta=1.0, tol=1e-8, max_iter=1000, random_state=None): + r""" + Compute the limit of a Nash linear quadratic dynamic game. In this + problem, player i minimizes -__all__ = ['nnash'] + .. math:: + \sum_{t=0}^{\infty} + \left\{ + x_t' r_i x_t + 2 x_t' w_i + u_{it} +u_{it}' q_i u_{it} + u_{jt}' s_i u_{jt} + 2 u_{jt}' + m_i u_{it} + \right\} + subject to the law of motion -def __dir__(): - return __all__ + .. math:: + x_{t+1} = A x_t + b_1 u_{1t} + b_2 u_{2t} + and a perceived control law :math:`u_j(t) = - f_j x_t` for the other + player. -def __getattr__(name): - if name not in __all__: - raise AttributeError( - "`quantecon.lqnash` is deprecated and has no attribute " - f"'{name}'." - ) + The solution computed in this routine is the :math:`f_i` and + :math:`p_i` of the associated double optimal linear regulator + problem. - warnings.warn(f"Please use `{name}` from the `quantecon` namespace, the" - "`quantecon.lqnash` namespace is deprecated. You can use" - f" the following instead:\n `from quantecon import {name}`.", - category=DeprecationWarning, stacklevel=2) + Parameters + ---------- + A : scalar(float) or array_like(float) + Corresponds to the above equation, should be of size (n, n) + B1 : scalar(float) or array_like(float) + As above, size (n, k_1) + B2 : scalar(float) or array_like(float) + As above, size (n, k_2) + R1 : scalar(float) or array_like(float) + As above, size (n, n) + R2 : scalar(float) or array_like(float) + As above, size (n, n) + Q1 : scalar(float) or array_like(float) + As above, size (k_1, k_1) + Q2 : scalar(float) or array_like(float) + As above, size (k_2, k_2) + S1 : scalar(float) or array_like(float) + As above, size (k_1, k_1) + S2 : scalar(float) or array_like(float) + As above, size (k_2, k_2) + W1 : scalar(float) or array_like(float) + As above, size (n, k_1) + W2 : scalar(float) or array_like(float) + As above, size (n, k_2) + M1 : scalar(float) or array_like(float) + As above, size (k_2, k_1) + M2 : scalar(float) or array_like(float) + As above, size (k_1, k_2) + beta : scalar(float), optional(default=1.0) + Discount rate + tol : scalar(float), optional(default=1e-8) + This is the tolerance level for convergence + max_iter : scalar(int), optional(default=1000) + This is the maximum number of iteratiosn allowed + random_state : int or np.random.RandomState/Generator, optional + Random seed (integer) or np.random.RandomState or Generator + instance to set the initial state of the random number generator + for reproducibility. If None, a randomly initialized RandomState + is used. - return getattr(_lqnash, name) + Returns + ------- + F1 : array_like, dtype=float, shape=(k_1, n) + Feedback law for agent 1 + F2 : array_like, dtype=float, shape=(k_2, n) + Feedback law for agent 2 + P1 : array_like, dtype=float, shape=(n, n) + The steady-state solution to the associated discrete matrix + Riccati equation for agent 1 + P2 : array_like, dtype=float, shape=(n, n) + The steady-state solution to the associated discrete matrix + Riccati equation for agent 2 + + """ + # == Unload parameters and make sure everything is an array == # + params = A, B1, B2, R1, R2, Q1, Q2, S1, S2, W1, W2, M1, M2 + params = map(np.asarray, params) + A, B1, B2, R1, R2, Q1, Q2, S1, S2, W1, W2, M1, M2 = params + + # == Multiply A, B1, B2 by sqrt(beta) to enforce discounting == # + A, B1, B2 = [np.sqrt(beta) * x for x in (A, B1, B2)] + + n = A.shape[0] + + if B1.ndim == 1: + k_1 = 1 + B1 = np.reshape(B1, (n, 1)) + else: + k_1 = B1.shape[1] + + if B2.ndim == 1: + k_2 = 1 + B2 = np.reshape(B2, (n, 1)) + else: + k_2 = B2.shape[1] + + random_state = check_random_state(random_state) + v1 = np.eye(k_1) + v2 = np.eye(k_2) + P1 = np.zeros((n, n)) + P2 = np.zeros((n, n)) + F1 = random_state.standard_normal((k_1, n)) + F2 = random_state.standard_normal((k_2, n)) + + for it in range(max_iter): + # update + F10 = F1 + F20 = F2 + + G2 = solve(np.dot(B2.T, P2.dot(B2))+Q2, v2) + G1 = solve(np.dot(B1.T, P1.dot(B1))+Q1, v1) + H2 = np.dot(G2, B2.T.dot(P2)) + H1 = np.dot(G1, B1.T.dot(P1)) + + # break up the computation of F1, F2 + F1_left = v1 - np.dot(H1.dot(B2)+G1.dot(M1.T), + H2.dot(B1)+G2.dot(M2.T)) + F1_right = H1.dot(A)+G1.dot(W1.T) - np.dot(H1.dot(B2)+G1.dot(M1.T), + H2.dot(A)+G2.dot(W2.T)) + F1 = solve(F1_left, F1_right) + F2 = H2.dot(A)+G2.dot(W2.T) - np.dot(H2.dot(B1)+G2.dot(M2.T), F1) + + Lambda1 = A - B2.dot(F2) + Lambda2 = A - B1.dot(F1) + Pi1 = R1 + np.dot(F2.T, S1.dot(F2)) + Pi2 = R2 + np.dot(F1.T, S2.dot(F1)) + + P1 = np.dot(Lambda1.T, P1.dot(Lambda1)) + Pi1 - \ + np.dot(np.dot(Lambda1.T, P1.dot(B1)) + W1 - F2.T.dot(M1), F1) + P2 = np.dot(Lambda2.T, P2.dot(Lambda2)) + Pi2 - \ + np.dot(np.dot(Lambda2.T, P2.dot(B2)) + W2 - F1.T.dot(M2), F2) + + dd = np.max(np.abs(F10 - F1)) + np.max(np.abs(F20 - F2)) + + if dd < tol: # success! + break + + else: + msg = 'No convergence: Iteration limit of {0} reached in nnash' + raise ValueError(msg.format(max_iter)) + + return F1, F2, P1, P2 diff --git a/quantecon/lss.py b/quantecon/lss.py index 55af0237e..321b871e9 100644 --- a/quantecon/lss.py +++ b/quantecon/lss.py @@ -1,24 +1,470 @@ -# This file is not meant for public use and will be removed v0.8.0. -# Use the `quantecon` namespace for importing the objects -# included below. +""" +Computes quantities associated with the Gaussian linear state space model. -import warnings -from . import _lss +References +---------- +https://lectures.quantecon.org/py/linear_models.html -__all__ = ['LinearStateSpace', 'simulate_linear_model'] +""" +from textwrap import dedent +import numpy as np +from scipy.linalg import solve +from .matrix_eqn import solve_discrete_lyapunov +from numba import jit +from .util import check_random_state -def __getattr__(name): - if name not in __all__: - raise AttributeError( - "`quantecon.lss` is deprecated and has no attribute " - f"'{name}'." - ) - warnings.warn(f"Please use `{name}` from the `quantecon` namespace, the" - "`quantecon.lss` namespace is deprecated. You can use" - f" the following instead:\n `from quantecon import {name}`.", - category=DeprecationWarning, stacklevel=2) +@jit +def simulate_linear_model(A, x0, v, ts_length): + r""" + This is a separate function for simulating a vector linear system of + the form - return getattr(_lss, name) + .. math:: + + x_{t+1} = A x_t + v_t + + given :math:`x_0` = x0 + + Here :math:`x_t` and :math:`v_t` are both n x 1 and :math:`A` is n x n. + + The purpose of separating this functionality out is to target it for + optimization by Numba. For the same reason, matrix multiplication is + broken down into for loops. + + Parameters + ---------- + A : array_like or scalar(float) + Should be n x n + x0 : array_like + Should be n x 1. Initial condition + v : np.ndarray + Should be n x ts_length-1. Its t-th column is used as the time t + shock :math:`v_t` + ts_length : int + The length of the time series + + Returns + ------- + x : np.ndarray + Time series with ts_length columns, the t-th column being :math:`x_t` + """ + A = np.asarray(A) + n = A.shape[0] + x = np.empty((n, ts_length)) + x[:, 0] = x0 + for t in range(ts_length-1): + # x[:, t+1] = A.dot(x[:, t]) + v[:, t] + for i in range(n): + x[i, t+1] = v[i, t] # Shock + for j in range(n): + x[i, t+1] += A[i, j] * x[j, t] # Dot Product + return x + + +class LinearStateSpace: + r""" + A class that describes a Gaussian linear state space model of the + form: + + .. math:: + + x_{t+1} = A x_t + C w_{t+1} + + y_t = G x_t + H v_t + + where :math:`{w_t}` and :math:`{v_t}` are independent and standard normal + with dimensions k and l respectively. The initial conditions are + :math:`\mu_0` and :math:`\Sigma_0` for :math:`x_0 \sim N(\mu_0, \Sigma_0)`. + When :math:`\Sigma_0=0`, the draw of :math:`x_0` is exactly :math:`\mu_0`. + + Parameters + ---------- + A : array_like or scalar(float) + Part of the state transition equation. It should be `n x n` + C : array_like or scalar(float) + Part of the state transition equation. It should be `n x m` + G : array_like or scalar(float) + Part of the observation equation. It should be `k x n` + H : array_like or scalar(float), optional(default=None) + Part of the observation equation. It should be `k x l` + mu_0 : array_like or scalar(float), optional(default=None) + This is the mean of initial draw and is `n x 1` + Sigma_0 : array_like or scalar(float), optional(default=None) + This is the variance of the initial draw and is `n x n` and + also should be positive definite and symmetric + + Attributes + ---------- + A, C, G, H, mu_0, Sigma_0 : see Parameters + n, k, m, l : scalar(int) + The dimensions of x_t, y_t, w_t and v_t respectively + + """ + + def __init__(self, A, C, G, H=None, mu_0=None, Sigma_0=None): + self.A, self.G, self.C = list(map(self.convert, (A, G, C))) + # = Check Input Shapes = # + ni, nj = self.A.shape + if ni != nj: + raise ValueError( + "Matrix A (shape: %s) needs to be square" % (self.A.shape, )) + if ni != self.C.shape[0]: + raise ValueError( + "Matrix C (shape: %s) does not have compatible dimensions " + "with A. It should be shape: %s" % (self.C.shape, (ni, 1))) + self.m = self.C.shape[1] + self.k, self.n = self.G.shape + if self.n != ni: + raise ValueError("Matrix G (shape: %s) does not have compatible" + "dimensions with A (%s)" % (self.G.shape, + self.A.shape)) + if H is None: + self.H = None + self.l = None + else: + self.H = self.convert(H) + self.l = self.H.shape[1] + if mu_0 is None: + self.mu_0 = np.zeros((self.n, 1)) + else: + self.mu_0 = self.convert(mu_0) + self.mu_0.shape = self.n, 1 + if Sigma_0 is None: + self.Sigma_0 = np.zeros((self.n, self.n)) + else: + self.Sigma_0 = self.convert(Sigma_0) + + def __repr__(self): + return self.__str__() + + def __str__(self): + m = """\ + Linear Gaussian state space model: + - dimension of state space : {n} + - number of innovations : {m} + - dimension of observation equation : {k} + """ + return dedent(m.format(n=self.n, k=self.k, m=self.m)) + + def convert(self, x): + """ + Convert array_like objects (lists of lists, floats, etc.) into + well formed 2D NumPy arrays + + """ + return np.atleast_2d(np.asarray(x, dtype='float')) + + def simulate(self, ts_length=100, random_state=None): + r""" + Simulate a time series of length ts_length, first drawing + + .. math:: + + x_0 \sim N(\mu_0, \Sigma_0) + + Parameters + ---------- + ts_length : scalar(int), optional(default=100) + The length of the simulation + random_state : int or np.random.RandomState/Generator, optional + Random seed (integer) or np.random.RandomState or Generator + instance to set the initial state of the random number + generator for reproducibility. If None, a randomly + initialized RandomState is used. + + Returns + ------- + x : array_like(float) + An n x ts_length array, where the t-th column is :math:`x_t` + y : array_like(float) + A k x ts_length array, where the t-th column is :math:`y_t` + + """ + random_state = check_random_state(random_state) + + x0 = random_state.multivariate_normal(self.mu_0.flatten(), + self.Sigma_0) + w = random_state.standard_normal((self.m, ts_length-1)) + v = self.C.dot(w) # Multiply each w_t by C to get v_t = C w_t + # == simulate time series == # + x = simulate_linear_model(self.A, x0, v, ts_length) + + if self.H is not None: + v = random_state.standard_normal((self.l, ts_length)) + y = self.G.dot(x) + self.H.dot(v) + else: + y = self.G.dot(x) + + return x, y + + def replicate(self, T=10, num_reps=100, random_state=None): + r""" + Simulate num_reps observations of :math:`x_T` and :math:`y_T` given + :math:`x_0 \sim N(\mu_0, \Sigma_0)`. + + Parameters + ---------- + T : scalar(int), optional(default=10) + The period that we want to replicate values for + num_reps : scalar(int), optional(default=100) + The number of replications that we want + random_state : int or np.random.RandomState/Generator, optional + Random seed (integer) or np.random.RandomState or Generator + instance to set the initial state of the random number + generator for reproducibility. If None, a randomly + initialized RandomState is used. + + Returns + ------- + x : array_like(float) + An n x num_reps array, where the j-th column is the j_th + observation of :math:`x_T` + + y : array_like(float) + A k x num_reps array, where the j-th column is the j_th + observation of :math:`y_T` + + """ + random_state = check_random_state(random_state) + + x = np.empty((self.n, num_reps)) + for j in range(num_reps): + x_T, _ = self.simulate(ts_length=T+1, random_state=random_state) + x[:, j] = x_T[:, -1] + if self.H is not None: + v = random_state.standard_normal((self.l, num_reps)) + y = self.G.dot(x) + self.H.dot(v) + else: + y = self.G.dot(x) + + return x, y + + def moment_sequence(self): + r""" + Create a generator to calculate the population mean and + variance-covariance matrix for both :math:`x_t` and :math:`y_t` + starting at the initial condition (self.mu_0, self.Sigma_0). + Each iteration produces a 4-tuple of items (mu_x, mu_y, Sigma_x, + Sigma_y) for the next period. + + Yields + ------ + mu_x : array_like(float) + An n x 1 array representing the population mean of x_t + mu_y : array_like(float) + A k x 1 array representing the population mean of y_t + Sigma_x : array_like(float) + An n x n array representing the variance-covariance matrix + of x_t + Sigma_y : array_like(float) + A k x k array representing the variance-covariance matrix + of y_t + + """ + # == Simplify names == # + A, C, G, H = self.A, self.C, self.G, self.H + # == Initial moments == # + mu_x, Sigma_x = self.mu_0, self.Sigma_0 + + while 1: + mu_y = G.dot(mu_x) + if H is None: + Sigma_y = G.dot(Sigma_x).dot(G.T) + else: + Sigma_y = G.dot(Sigma_x).dot(G.T) + H.dot(H.T) + + yield mu_x, mu_y, Sigma_x, Sigma_y + + # == Update moments of x == # + mu_x = A.dot(mu_x) + Sigma_x = A.dot(Sigma_x).dot(A.T) + C.dot(C.T) + + def stationary_distributions(self): + r""" + Compute the moments of the stationary distributions of :math:`x_t` and + :math:`y_t` if possible. Computation is by solving the discrete + Lyapunov equation. + + Returns + ------- + mu_x : array_like(float) + An n x 1 array representing the stationary mean of :math:`x_t` + mu_y : array_like(float) + An k x 1 array representing the stationary mean of :math:`y_t` + Sigma_x : array_like(float) + An n x n array representing the stationary var-cov matrix + of :math:`x_t` + Sigma_y : array_like(float) + An k x k array representing the stationary var-cov matrix + of :math:`y_t` + Sigma_yx : array_like(float) + An k x n array representing the stationary cov matrix + between :math:`y_t` and :math:`x_t`. + + """ + self.__partition() + num_const, sorted_idx = self.num_const, self.sorted_idx + A21, A22 = self.A21, self.A22 + CC2 = self.C2 @ self.C2.T + n = self.n + + if num_const > 0: + μ = solve(np.eye(n-num_const) - A22, A21) + else: + μ = solve(np.eye(n-num_const) - A22, np.zeros((n, 1))) + Σ = solve_discrete_lyapunov(A22, CC2, method='bartels-stewart') + + mu_x = np.empty((n, 1)) + mu_x[:num_const] = self.mu_0[sorted_idx[:num_const]] + mu_x[num_const:] = μ + + Sigma_x = np.zeros((n, n)) + Sigma_x[num_const:, num_const:] = Σ + + mu_x = self.P.T @ mu_x + Sigma_x = self.P.T @ Sigma_x @ self.P + + mu_y = self.G @ mu_x + Sigma_y = self.G @ Sigma_x @ self.G.T + if self.H is not None: + Sigma_y += self.H @ self.H.T + Sigma_yx = self.G @ Sigma_x + + self.mu_x, self.mu_y = mu_x, mu_y + self.Sigma_x, self.Sigma_y, self.Sigma_yx = Sigma_x, Sigma_y, Sigma_yx + + return mu_x, mu_y, Sigma_x, Sigma_y, Sigma_yx + + def geometric_sums(self, beta, x_t): + r""" + Forecast the geometric sums + + .. math:: + + S_x := E \Big[ \sum_{j=0}^{\infty} \beta^j x_{t+j} | x_t \Big] + + S_y := E \Big[ \sum_{j=0}^{\infty} \beta^j y_{t+j} | x_t \Big] + + Parameters + ---------- + beta : scalar(float) + Discount factor, in [0, 1) + + beta : array_like(float) + The term x_t for conditioning + + Returns + ------- + S_x : array_like(float) + Geometric sum as defined above + + S_y : array_like(float) + Geometric sum as defined above + + """ + + I = np.identity(self.n) + S_x = solve(I - beta * self.A, x_t) + S_y = self.G.dot(S_x) + + return S_x, S_y + + def impulse_response(self, j=5): + r""" + Pulls off the imuplse response coefficients to a shock + in :math:`w_{t}` for :math:`x` and :math:`y` + + Important to note: We are uninterested in the shocks to + v for this method + + * :math:`x` coefficients are :math:`C, AC, A^2 C...` + * :math:`y` coefficients are :math:`GC, GAC, GA^2C...` + + Parameters + ---------- + j : Scalar(int) + Number of coefficients that we want + + Returns + ------- + xcoef : list(array_like(float, 2)) + The coefficients for x + ycoef : list(array_like(float, 2)) + The coefficients for y + """ + # Pull out matrices + A, C, G, H = self.A, self.C, self.G, self.H + Apower = np.copy(A) + + # Create room for coefficients + xcoef = [C] + ycoef = [np.dot(G, C)] + + for i in range(j): + xcoef.append(np.dot(Apower, C)) + ycoef.append(np.dot(G, np.dot(Apower, C))) + Apower = np.dot(Apower, A) + + return xcoef, ycoef + + def __partition(self): + r""" + Reorder the states by shifting the constant terms to the + top of the state vector. Then partition the linear state + space model following Appendix C in RMT Ch2 such that the + A22 matrix associated with non-constant states have eigenvalues + all strictly smaller than 1. + + .. math:: + \left[\begin{array}{c} + const\\ + x_{2,t+1} + \end{array}\right]=\left[\begin{array}{cc} + I & 0\\ + A_{21} & A_{22} + \end{array}\right]\left[\begin{array}{c} + 1\\ + x_{2,t} + \end{array}\right]+\left[\begin{array}{c} + 0\\ + C_{2} + \end{array}\right]w_{t+1} + + Returns + ------- + A22 : array_like(float) + Lower right part of the reordered matrix A. + A21 : array_like(float) + Lower left part of the reordered matrix A. + """ + A, C = self.A, self.C + n = self.n + + sorted_idx = [] + A_diag = np.diag(A) + num_const = 0 + for idx in range(n): + if (A_diag[idx] == 1) and (C[idx, :] == 0).all() and \ + np.linalg.norm(A[idx, :]) == 1: + sorted_idx.insert(0, idx) + num_const += 1 + else: + sorted_idx.append(idx) + self.num_const = num_const + self.sorted_idx = sorted_idx + + P = np.zeros((n, n)) + P[range(n), sorted_idx] = 1 + + sorted_A = P @ A @ P.T + sorted_C = P @ C + A21 = sorted_A[num_const:, :num_const] + A22 = sorted_A[num_const:, num_const:] + C2 = sorted_C[num_const:, :] + + self.P, self.A21, self.A22, self.C2 = P, A21, A22, C2 + + return A21, A22 diff --git a/quantecon/markov/approximation.py b/quantecon/markov/approximation.py index 4df30f99f..2cfb809b6 100644 --- a/quantecon/markov/approximation.py +++ b/quantecon/markov/approximation.py @@ -80,8 +80,8 @@ def rouwenhorst(n, rho, sigma, mu=0.): An instance of the MarkovChain class that stores the transition matrix and state values returned by the discretization method - Notes - ----- + Note + ---- UserWarning: The API of `rouwenhorst` was changed from `rouwenhorst(n, ybar, sigma, rho)` to @@ -182,8 +182,8 @@ def tauchen(n, rho, sigma, mu=0., n_std=3): An instance of the MarkovChain class that stores the transition matrix and state values returned by the discretization method - Notes - ----- + Note + ---- UserWarning: The API of `tauchen` was changed from `tauchen(rho, sigma_u, b=0., m=3, n=7)` to diff --git a/quantecon/markov/core.py b/quantecon/markov/core.py index 5443dfd5b..21b6fc1c9 100644 --- a/quantecon/markov/core.py +++ b/quantecon/markov/core.py @@ -86,7 +86,7 @@ from numba import jit from .gth_solve import gth_solve -from .._graph_tools import DiGraph +from ..graph_tools import DiGraph from ..util import searchsorted, check_random_state, rng_integers diff --git a/quantecon/matrix_eqn.py b/quantecon/matrix_eqn.py index 67f7e40d7..09466e268 100644 --- a/quantecon/matrix_eqn.py +++ b/quantecon/matrix_eqn.py @@ -1,29 +1,312 @@ -# This file is not meant for public use and will be removed v0.8.0. -# Use the `quantecon` namespace for importing the objects -# included below. +""" +This file holds several functions that are used to solve matrix +equations. Currently has functionality to solve: -import warnings -from . import _matrix_eqn +* Lyapunov Equations +* Riccati Equations +TODO: 1. See issue 47 on github repository, should add support for + Sylvester equations + 2. Fix warnings from checking conditioning of matrices +""" +import numpy as np +from numpy.linalg import solve +from scipy.linalg import solve_discrete_lyapunov as sp_solve_discrete_lyapunov +from scipy.linalg import solve_discrete_are as sp_solve_discrete_are -__all__ = ['solve_discrete_lyapunov', 'solve_discrete_riccati', - 'solve_discrete_riccati_system'] +EPS = np.finfo(float).eps -def __dir__(): - return __all__ +def solve_discrete_lyapunov(A, B, max_it=50, method="doubling"): + r""" + Computes the solution to the discrete lyapunov equation -def __getattr__(name): - if name not in __all__: - raise AttributeError( - "`quantecon.matrix_eqn` is deprecated and has no attribute " - f"'{name}'." - ) + .. math:: - warnings.warn(f"Please use `{name}` from the `quantecon` namespace, the" - "`quantecon.matrix_eqn` namespace is deprecated. You can use" - f" the following instead:\n `from quantecon import {name}`.", - category=DeprecationWarning, stacklevel=2) + AXA' - X + B = 0 - return getattr(_matrix_eqn, name) + :math:`X` is computed by using a doubling algorithm. In particular, we + iterate to convergence on :math:`X_j` with the following recursions for + :math:`j = 1, 2, \dots` starting from :math:`X_0 = B`, :math:`a_0 = A`: + + .. math:: + + a_j = a_{j-1} a_{j-1} + + .. math:: + + X_j = X_{j-1} + a_{j-1} X_{j-1} a_{j-1}' + + Parameters + ---------- + A : array_like(float, ndim=2) + An n x n matrix as described above. We assume in order for + convergence that the eigenvalues of A have moduli bounded by + unity + B : array_like(float, ndim=2) + An n x n matrix as described above. We assume in order for + convergence that the eigenvalues of A have moduli bounded by + unity + max_it : scalar(int), optional(default=50) + The maximum number of iterations + method : string, optional(default="doubling") + Describes the solution method to use. If it is "doubling" then + uses the doubling algorithm to solve, if it is "bartels-stewart" + then it uses scipy's implementation of the Bartels-Stewart + approach. + + Returns + ------- + gamma1: array_like(float, ndim=2) + Represents the value :math:`X` + + """ + if method == "doubling": + A, B = list(map(np.atleast_2d, [A, B])) + alpha0 = A + gamma0 = B + + diff = 5 + n_its = 1 + + while diff > 1e-15: + + alpha1 = alpha0.dot(alpha0) + gamma1 = gamma0 + np.dot(alpha0.dot(gamma0), alpha0.conjugate().T) + + diff = np.max(np.abs(gamma1 - gamma0)) + alpha0 = alpha1 + gamma0 = gamma1 + + n_its += 1 + + if n_its > max_it: + msg = "Exceeded maximum iterations {}, check input matrics" + raise ValueError(msg.format(n_its)) + + elif method == "bartels-stewart": + gamma1 = sp_solve_discrete_lyapunov(A, B) + + else: + msg = "Check your method input. Should be doubling or bartels-stewart" + raise ValueError(msg) + + return gamma1 + + +def solve_discrete_riccati(A, B, Q, R, N=None, tolerance=1e-10, max_iter=500, + method="doubling"): + """ + Solves the discrete-time algebraic Riccati equation + + .. math:: + + X = A'XA - (N + B'XA)'(B'XB + R)^{-1}(N + B'XA) + Q + + Computation is via a modified structured doubling algorithm, an + explanation of which can be found in the reference below, if + `method="doubling"` (default), and via a QZ decomposition method by + calling `scipy.linalg.solve_discrete_are` if `method="qz"`. + + Parameters + ---------- + A : array_like(float, ndim=2) + k x k array. + B : array_like(float, ndim=2) + k x n array + Q : array_like(float, ndim=2) + k x k, should be symmetric and non-negative definite + R : array_like(float, ndim=2) + n x n, should be symmetric and positive definite + N : array_like(float, ndim=2) + n x k array + tolerance : scalar(float), optional(default=1e-10) + The tolerance level for convergence + max_iter : scalar(int), optional(default=500) + The maximum number of iterations allowed + method : string, optional(default="doubling") + Describes the solution method to use. If it is "doubling" then + uses the doubling algorithm to solve, if it is "qz" then it uses + `scipy.linalg.solve_discrete_are` (in which case `tolerance` and + `max_iter` are irrelevant). + + Returns + ------- + X : array_like(float, ndim=2) + The fixed point of the Riccati equation; a k x k array + representing the approximate solution + + References + ---------- + Chiang, Chun-Yueh, Hung-Yuan Fan, and Wen-Wei Lin. "STRUCTURED DOUBLING + ALGORITHM FOR DISCRETE-TIME ALGEBRAIC RICCATI EQUATIONS WITH SINGULAR + CONTROL WEIGHTING MATRICES." Taiwanese Journal of Mathematics 14, no. 3A + (2010): pp-935. + + """ + methods = ['doubling', 'qz'] + if method not in methods: + msg = "Check your method input. Should be {} or {}".format(*methods) + raise ValueError(msg) + + # == Set up == # + error = tolerance + 1 + fail_msg = "Convergence failed after {} iterations." + + # == Make sure that all array_likes are np arrays, two-dimensional == # + A, B, Q, R = np.atleast_2d(A, B, Q, R) + n, k = R.shape[0], Q.shape[0] + I = np.identity(k) + if N is None: + N = np.zeros((n, k)) + else: + N = np.atleast_2d(N) + + if method == 'qz': + X = sp_solve_discrete_are(A, B, Q, R, s=N.T) + return X + + # if method == 'doubling' + # == Choose optimal value of gamma in R_hat = R + gamma B'B == # + current_min = np.inf + candidates = (0.01, 0.1, 0.25, 0.5, 1.0, 2.0, 10.0, 100.0, 10e5) + BB = np.dot(B.T, B) + BTA = np.dot(B.T, A) + for gamma in candidates: + Z = R + gamma * BB + cn = np.linalg.cond(Z) + if cn * EPS < 1: + Q_tilde = - Q + np.dot(N.T, solve(Z, N + gamma * BTA)) + gamma * I + G0 = np.dot(B, solve(Z, B.T)) + A0 = np.dot(I - gamma * G0, A) - np.dot(B, solve(Z, N)) + H0 = gamma * np.dot(A.T, A0) - Q_tilde + f1 = np.linalg.cond(Z, np.inf) + f2 = gamma * f1 + f3 = np.linalg.cond(I + np.dot(G0, H0)) + f_gamma = max(f1, f2, f3) + if f_gamma < current_min: + best_gamma = gamma + current_min = f_gamma + + # == If no candidate successful then fail == # + if current_min == np.inf: + msg = "Unable to initialize routine due to ill conditioned arguments" + raise ValueError(msg) + + gamma = best_gamma + R_hat = R + gamma * BB + + # == Initial conditions == # + Q_tilde = - Q + np.dot(N.T, solve(R_hat, N + gamma * BTA)) + gamma * I + G0 = np.dot(B, solve(R_hat, B.T)) + A0 = np.dot(I - gamma * G0, A) - np.dot(B, solve(R_hat, N)) + H0 = gamma * np.dot(A.T, A0) - Q_tilde + i = 1 + + # == Main loop == # + while error > tolerance: + + if i > max_iter: + raise ValueError(fail_msg.format(i)) + + else: + A1 = np.dot(A0, solve(I + np.dot(G0, H0), A0)) + G1 = G0 + np.dot(np.dot(A0, G0), solve(I + np.dot(H0, G0), A0.T)) + H1 = H0 + np.dot(A0.T, solve(I + np.dot(H0, G0), np.dot(H0, A0))) + + error = np.max(np.abs(H1 - H0)) + A0 = A1 + G0 = G1 + H0 = H1 + i += 1 + + return H1 + gamma * I # Return X + + +def solve_discrete_riccati_system(Π, As, Bs, Cs, Qs, Rs, Ns, beta, + tolerance=1e-10, max_iter=1000): + """ + Solves the stacked system of algebraic matrix Riccati equations + in the Markov Jump linear quadratic control problems, by iterating + Ps matrices until convergence. + + Parameters + ---------- + Π : array_like(float, ndim=2) + The Markov chain transition matrix with dimension m x m. + As : array_like(float) + Consists of m state transition matrices A(s) with dimension + n x n for each Markov state s + Bs : array_like(float) + Consists of m state transition matrices B(s) with dimension + n x k for each Markov state s + Cs : array_like(float), optional(default=None) + Consists of m state transition matrices C(s) with dimension + n x j for each Markov state s. If the model is deterministic + then Cs should take default value of None + Qs : array_like(float) + Consists of m symmetric and non-negative definite payoff + matrices Q(s) with dimension k x k that corresponds with + the control variable u for each Markov state s + Rs : array_like(float) + Consists of m symmetric and non-negative definite payoff + matrices R(s) with dimension n x n that corresponds with + the state variable x for each Markov state s + Ns : array_like(float), optional(default=None) + Consists of m cross product term matrices N(s) with dimension + k x n for each Markov state, + beta : scalar(float), optional(default=1) + beta is the discount parameter + tolerance : scalar(float), optional(default=1e-10) + The tolerance level for convergence + max_iter : scalar(int), optional(default=500) + The maximum number of iterations allowed + + Returns + ------- + Ps : array_like(float, ndim=2) + The fixed point of the stacked system of algebraic matrix + Riccati equations, consists of m n x n P(s) matrices + + """ + m = Qs.shape[0] + k, n = Qs.shape[1], Rs.shape[1] + # Create the Ps matrices, initialize as identity matrix + Ps = np.array([np.eye(n) for i in range(m)]) + Ps1 = np.copy(Ps) + + # == Set up for iteration on Riccati equations system == # + error = tolerance + 1 + fail_msg = "Convergence failed after {} iterations." + + # == Prepare array for iteration == # + sum1, sum2 = np.empty((n, n)), np.empty((n, n)) + + # == Main loop == # + iteration = 0 + while error > tolerance: + + if iteration > max_iter: + raise ValueError(fail_msg.format(max_iter)) + + else: + error = 0 + for i in range(m): + # Initialize arrays + sum1[:, :] = 0. + sum2[:, :] = 0. + for j in range(m): + sum1 += beta * Π[i, j] * As[i].T @ Ps[j] @ As[i] + sum2 += Π[i, j] * \ + (beta * As[i].T @ Ps[j] @ Bs[i] + Ns[i].T) @ \ + solve(Qs[i] + beta * Bs[i].T @ Ps[j] @ Bs[i], + beta * Bs[i].T @ Ps[j] @ As[i] + Ns[i]) + + Ps1[i][:, :] = Rs[i] + sum1 - sum2 + error += np.max(np.abs(Ps1[i] - Ps[i])) + + Ps[:, :, :] = Ps1[:, :, :] + iteration += 1 + + return Ps diff --git a/quantecon/quad.py b/quantecon/quad.py index cf8860d47..39320ac1d 100644 --- a/quantecon/quad.py +++ b/quantecon/quad.py @@ -14,7 +14,7 @@ import numpy as np import scipy.linalg as la from numba import jit, vectorize -from ._ce_util import ckron, gridmake +from .ce_util import ckron, gridmake from .util import check_random_state __all__ = ['qnwcheb', 'qnwequi', 'qnwlege', 'qnwnorm', 'qnwlogn', diff --git a/quantecon/quadsums.py b/quantecon/quadsums.py index 30b8998b0..f4f135adf 100644 --- a/quantecon/quadsums.py +++ b/quantecon/quadsums.py @@ -1,28 +1,98 @@ -# This file is not meant for public use and will be removed v0.8.0. -# Use the `quantecon` namespace for importing the objects -# included below. +""" +This module provides functions to compute quadratic sums of the form described +in the docstrings. -import warnings -from . import _quadsums +""" -__all__ = ['var_quadratic_sum', 'm_quadratic_sum'] +import numpy as np +import scipy.linalg +from .matrix_eqn import solve_discrete_lyapunov -def __dir__(): - return __all__ +def var_quadratic_sum(A, C, H, beta, x0): + r""" + Computes the expected discounted quadratic sum + .. math:: -def __getattr__(name): - if name not in __all__: - raise AttributeError( - "`quantecon.quadsums` is deprecated and has no attribute " - f"'{name}'." - ) + q(x_0) = \mathbb{E} \Big[ \sum_{t=0}^{\infty} \beta^t x_t' H x_t \Big] - warnings.warn(f"Please use `{name}` from the `quantecon` namespace, the" - "`quantecon.quadsums` namespace is deprecated. You can use" - f" the following instead:\n `from quantecon import {name}`.", - category=DeprecationWarning, stacklevel=2) + Here :math:`{x_t}` is the VAR process :math:`x_{t+1} = A x_t + C w_t` + with :math:`{x_t}` standard normal and :math:`x_0` the initial condition. - return getattr(_quadsums, name) + Parameters + ---------- + A : array_like(float, ndim=2) + The matrix described above in description. Should be n x n + C : array_like(float, ndim=2) + The matrix described above in description. Should be n x n + H : array_like(float, ndim=2) + The matrix described above in description. Should be n x n + beta: scalar(float) + Should take a value in (0, 1) + x_0: array_like(float, ndim=1) + The initial condtion. A conformable array (of length n, or with + n rows) + + Returns + ------- + q0: scalar(float) + Represents the value :math:`q(x_0)` + + Remarks: The formula for computing :math:`q(x_0)` is + :math:`q(x_0) = x_0' Q x_0 + v` + where + + * :math:`Q` is the solution to :math:`Q = H + \beta A' Q A`, and + * :math:`v = \frac{trace(C' Q C) \beta}{(1 - \beta)}` + + """ + # == Make sure that A, C, H and x0 are array_like == # + + A, C, H = list(map(np.atleast_2d, (A, C, H))) + x0 = np.atleast_1d(x0) + # == Start computations == # + Q = scipy.linalg.solve_discrete_lyapunov(np.sqrt(beta) * A.T, H) + cq = np.dot(np.dot(C.T, Q), C) + v = np.trace(cq) * beta / (1 - beta) + q0 = np.dot(np.dot(x0.T, Q), x0) + v + + return q0 + + +def m_quadratic_sum(A, B, max_it=50): + r""" + Computes the quadratic sum + + .. math:: + + V = \sum_{j=0}^{\infty} A^j B A^{j'} + + V is computed by solving the corresponding discrete lyapunov + equation using the doubling algorithm. See the documentation of + `util.solve_discrete_lyapunov` for more information. + + Parameters + ---------- + A : array_like(float, ndim=2) + An n x n matrix as described above. We assume in order for + convergence that the eigenvalues of :math:`A` have moduli bounded by + unity + B : array_like(float, ndim=2) + An n x n matrix as described above. We assume in order for + convergence that the eigenvalues of :math:`A` have moduli bounded by + unity + max_it : scalar(int), optional(default=50) + The maximum number of iterations + + Returns + ------- + gamma1: array_like(float, ndim=2) + Represents the value :math:`V` + + """ + + gamma1 = solve_discrete_lyapunov(A, B, max_it) + + return gamma1 diff --git a/quantecon/rank_nullspace.py b/quantecon/rank_nullspace.py index 798e15974..6eb553c41 100644 --- a/quantecon/rank_nullspace.py +++ b/quantecon/rank_nullspace.py @@ -1,28 +1,95 @@ -# This file is not meant for public use and will be removed v0.8.0. -# Use the `quantecon` namespace for importing the objects -# included below. +import numpy as np +from numpy.linalg import svd -import warnings -from . import _rank_nullspace +def rank_est(A, atol=1e-13, rtol=0): + """ + Estimate the rank (i.e. the dimension of the nullspace) of a matrix. -__all__ = ['rank_est', 'nullspace'] + The algorithm used by this function is based on the singular value + decomposition of `A`. + Parameters + ---------- + A : array_like(float, ndim=1 or 2) + A should be at most 2-D. A 1-D array with length n will be + treated as a 2-D with shape (1, n) + atol : scalar(float), optional(default=1e-13) + The absolute tolerance for a zero singular value. Singular + values smaller than `atol` are considered to be zero. + rtol : scalar(float), optional(default=0) + The relative tolerance. Singular values less than rtol*smax are + considered to be zero, where smax is the largest singular value. -def __dir__(): - return __all__ + Returns + ------- + r : scalar(int) + The estimated rank of the matrix. + Note: If both `atol` and `rtol` are positive, the combined tolerance + is the maximum of the two; that is: -def __getattr__(name): - if name not in __all__: - raise AttributeError( - "`quantecon.rank_nullspace` is deprecated and has no attribute" - f" '{name}'." - ) + tol = max(atol, rtol * smax) - warnings.warn(f"Please use `{name}` from the `quantecon` namespace, the" - "`quantecon.rank_nullspace` namespace is deprecated. You can" - f" use following instead:\n `from quantecon import {name}`.", - category=DeprecationWarning, stacklevel=2) + Note: Singular values smaller than `tol` are considered to be zero. - return getattr(_rank_nullspace, name) + See also + -------- + numpy.linalg.matrix_rank + matrix_rank is basically the same as this function, but it does + not provide the option of the absolute tolerance. + + """ + + A = np.atleast_2d(A) + s = svd(A, compute_uv=False) + tol = max(atol, rtol * s[0]) + rank = int((s >= tol).sum()) + + return rank + + +def nullspace(A, atol=1e-13, rtol=0): + """ + Compute an approximate basis for the nullspace of A. + + The algorithm used by this function is based on the singular value + decomposition of `A`. + + Parameters + ---------- + A : array_like(float, ndim=1 or 2) + A should be at most 2-D. A 1-D array with length k will be + treated as a 2-D with shape (1, k) + atol : scalar(float), optional(default=1e-13) + The absolute tolerance for a zero singular value. Singular + values smaller than `atol` are considered to be zero. + rtol : scalar(float), optional(default=0) + The relative tolerance. Singular values less than rtol*smax are + considered to be zero, where smax is the largest singular value. + + Returns + ------- + ns : array_like(float, ndim=2) + If `A` is an array with shape (m, k), then `ns` will be an array + with shape (k, n), where n is the estimated dimension of the + nullspace of `A`. The columns of `ns` are a basis for the + nullspace; each element in numpy.dot(A, ns) will be + approximately zero. + + Note: If both `atol` and `rtol` are positive, the combined tolerance + is the maximum of the two; that is: + + tol = max(atol, rtol * smax) + + Note: Singular values smaller than `tol` are considered to be zero. + + """ + + A = np.atleast_2d(A) + u, s, vh = svd(A) + tol = max(atol, rtol * s[0]) + nnz = (s >= tol).sum() + ns = vh[nnz:].conj().T + + return ns diff --git a/quantecon/robustlq.py b/quantecon/robustlq.py index 3def5aac8..4f98a7f6c 100644 --- a/quantecon/robustlq.py +++ b/quantecon/robustlq.py @@ -1,28 +1,400 @@ -# This file is not meant for public use and will be removed v0.8.0. -# Use the `quantecon` namespace for importing the objects -# included below. +""" +Solves robust LQ control problems. -import warnings -from . import _robustlq +""" +from textwrap import dedent +import numpy as np +from .lqcontrol import LQ +from .quadsums import var_quadratic_sum +from scipy.linalg import solve, inv, det +from .matrix_eqn import solve_discrete_lyapunov -__all__ = ['RBLQ'] +class RBLQ: + r""" + Provides methods for analysing infinite horizon robust LQ control + problems of the form + .. math:: -def __dir__(): - return __all__ + \min_{u_t} \sum_t \beta^t {x_t' R x_t + u_t' Q u_t } + subject to -def __getattr__(name): - if name not in __all__: - raise AttributeError( - "`quantecon.robustlq` is deprecated and has no attribute " - f"'{name}'." - ) + .. math:: - warnings.warn(f"Please use `{name}` from the `quantecon` namespace, the" - "`quantecon.robustlq` namespace is deprecated. You can use" - f" the following instead:\n `from quantecon import {name}`.", - category=DeprecationWarning, stacklevel=2) + x_{t+1} = A x_t + B u_t + C w_{t+1} - return getattr(_robustlq, name) + and with model misspecification parameter theta. + + Parameters + ---------- + Q : array_like(float, ndim=2) + The cost(payoff) matrix for the controls. See above for more. + Q should be k x k and symmetric and positive definite + R : array_like(float, ndim=2) + The cost(payoff) matrix for the state. See above for more. R + should be n x n and symmetric and non-negative definite + A : array_like(float, ndim=2) + The matrix that corresponds with the state in the state space + system. A should be n x n + B : array_like(float, ndim=2) + The matrix that corresponds with the control in the state space + system. B should be n x k + C : array_like(float, ndim=2) + The matrix that corresponds with the random process in the + state space system. C should be n x j + beta : scalar(float) + The discount factor in the robust control problem + theta : scalar(float) + The robustness factor in the robust control problem + + Attributes + ---------- + Q, R, A, B, C, beta, theta : see Parameters + k, n, j : scalar(int) + The dimensions of the matrices + + """ + + def __init__(self, Q, R, A, B, C, beta, theta): + + # == Make sure all matrices can be treated as 2D arrays == # + A, B, C, Q, R = list(map(np.atleast_2d, (A, B, C, Q, R))) + self.A, self.B, self.C, self.Q, self.R = A, B, C, Q, R + # == Record dimensions == # + self.k = self.Q.shape[0] + self.n = self.R.shape[0] + self.j = self.C.shape[1] + # == Remaining parameters == # + self.beta, self.theta = beta, theta + # == Check for case of no control (pure forecasting problem) == # + self.pure_forecasting = True if not Q.any() and not B.any() else False + + def __repr__(self): + return self.__str__() + + def __str__(self): + m = """\ + Robust linear quadratic control system + - beta (discount parameter) : {b} + - theta (robustness factor) : {th} + - n (number of state variables) : {n} + - k (number of control variables) : {k} + - j (number of shocks) : {j} + """ + return dedent(m.format(b=self.beta, n=self.n, k=self.k, j=self.j, + th=self.theta)) + + def d_operator(self, P): + r""" + The D operator, mapping P into + + .. math:: + + D(P) := P + PC(\theta I - C'PC)^{-1} C'P. + + Parameters + ---------- + P : array_like(float, ndim=2) + A matrix that should be n x n + + Returns + ------- + dP : array_like(float, ndim=2) + The matrix P after applying the D operator + + """ + C, theta = self.C, self.theta + I = np.identity(self.j) + S1 = np.dot(P, C) + S2 = np.dot(C.T, S1) + + dP = P + np.dot(S1, solve(theta * I - S2, S1.T)) + + return dP + + def b_operator(self, P): + r""" + The B operator, mapping P into + + .. math:: + + B(P) := R - \beta^2 A'PB(Q + \beta B'PB)^{-1}B'PA + \beta A'PA + + and also returning + + .. math:: + + F := (Q + \beta B'PB)^{-1} \beta B'PA + + Parameters + ---------- + P : array_like(float, ndim=2) + A matrix that should be n x n + + Returns + ------- + F : array_like(float, ndim=2) + The F matrix as defined above + new_p : array_like(float, ndim=2) + The matrix P after applying the B operator + + """ + A, B, Q, R, beta = self.A, self.B, self.Q, self.R, self.beta + S1 = Q + beta * np.dot(B.T, np.dot(P, B)) + S2 = beta * np.dot(B.T, np.dot(P, A)) + S3 = beta * np.dot(A.T, np.dot(P, A)) + F = solve(S1, S2) if not self.pure_forecasting else np.zeros( + (self.k, self.n)) + new_P = R - np.dot(S2.T, F) + S3 + + return F, new_P + + def robust_rule(self, method='doubling'): + """ + This method solves the robust control problem by tricking it + into a stacked LQ problem, as described in chapter 2 of Hansen- + Sargent's text "Robustness." The optimal control with observed + state is + + .. math:: + + u_t = - F x_t + + And the value function is :math:`-x'Px` + + Parameters + ---------- + method : str, optional(default='doubling') + Solution method used in solving the associated Riccati + equation, str in {'doubling', 'qz'}. + + Returns + ------- + F : array_like(float, ndim=2) + The optimal control matrix from above + P : array_like(float, ndim=2) + The positive semi-definite matrix defining the value + function + K : array_like(float, ndim=2) + the worst-case shock matrix K, where + :math:`w_{t+1} = K x_t` is the worst case shock + + """ + # == Simplify names == # + A, B, C, Q, R = self.A, self.B, self.C, self.Q, self.R + beta, theta = self.beta, self.theta + k, j = self.k, self.j + # == Set up LQ version == # + I = np.identity(j) + Z = np.zeros((k, j)) + + if self.pure_forecasting: + lq = LQ(-beta*I*theta, R, A, C, beta=beta) + + # == Solve and convert back to robust problem == # + P, f, d = lq.stationary_values(method=method) + F = np.zeros((self.k, self.n)) + K = -f[:k, :] + + else: + Ba = np.hstack([B, C]) + Qa = np.vstack([np.hstack([Q, Z]), np.hstack([Z.T, -beta*I*theta])]) + lq = LQ(Qa, R, A, Ba, beta=beta) + + # == Solve and convert back to robust problem == # + P, f, d = lq.stationary_values(method=method) + F = f[:k, :] + K = -f[k:f.shape[0], :] + + return F, K, P + + def robust_rule_simple(self, P_init=None, max_iter=80, tol=1e-8): + """ + A simple algorithm for computing the robust policy F and the + corresponding value function P, based around straightforward + iteration with the robust Bellman operator. This function is + easier to understand but one or two orders of magnitude slower + than self.robust_rule(). For more information see the docstring + of that method. + + Parameters + ---------- + P_init : array_like(float, ndim=2), optional(default=None) + The initial guess for the value function matrix. It will + be a matrix of zeros if no guess is given + max_iter : scalar(int), optional(default=80) + The maximum number of iterations that are allowed + tol : scalar(float), optional(default=1e-8) + The tolerance for convergence + + Returns + ------- + F : array_like(float, ndim=2) + The optimal control matrix from above + P : array_like(float, ndim=2) + The positive semi-definite matrix defining the value + function + K : array_like(float, ndim=2) + the worst-case shock matrix K, where + :math:`w_{t+1} = K x_t` is the worst case shock + + """ + # == Simplify names == # + A, B, C, Q, R = self.A, self.B, self.C, self.Q, self.R + beta, theta = self.beta, self.theta + # == Set up loop == # + P = np.zeros((self.n, self.n)) if P_init is None else P_init + iterate, e = 0, tol + 1 + while iterate < max_iter and e > tol: + F, new_P = self.b_operator(self.d_operator(P)) + e = np.sqrt(np.sum((new_P - P)**2)) + iterate += 1 + P = new_P + I = np.identity(self.j) + S1 = P.dot(C) + S2 = C.T.dot(S1) + K = inv(theta * I - S2).dot(S1.T).dot(A - B.dot(F)) + + return F, K, P + + def F_to_K(self, F, method='doubling'): + """ + Compute agent 2's best cost-minimizing response K, given F. + + Parameters + ---------- + F : array_like(float, ndim=2) + A k x n array + method : str, optional(default='doubling') + Solution method used in solving the associated Riccati + equation, str in {'doubling', 'qz'}. + + Returns + ------- + K : array_like(float, ndim=2) + Agent's best cost minimizing response for a given F + P : array_like(float, ndim=2) + The value function for a given F + + """ + Q2 = self.beta * self.theta + R2 = - self.R - np.dot(F.T, np.dot(self.Q, F)) + A2 = self.A - np.dot(self.B, F) + B2 = self.C + lq = LQ(Q2, R2, A2, B2, beta=self.beta) + neg_P, neg_K, d = lq.stationary_values(method=method) + + return -neg_K, -neg_P + + def K_to_F(self, K, method='doubling'): + """ + Compute agent 1's best value-maximizing response F, given K. + + Parameters + ---------- + K : array_like(float, ndim=2) + A j x n array + method : str, optional(default='doubling') + Solution method used in solving the associated Riccati + equation, str in {'doubling', 'qz'}. + + Returns + ------- + F : array_like(float, ndim=2) + The policy function for a given K + P : array_like(float, ndim=2) + The value function for a given K + + """ + A1 = self.A + np.dot(self.C, K) + B1 = self.B + Q1 = self.Q + R1 = self.R - self.beta * self.theta * np.dot(K.T, K) + lq = LQ(Q1, R1, A1, B1, beta=self.beta) + P, F, d = lq.stationary_values(method=method) + + return F, P + + def compute_deterministic_entropy(self, F, K, x0): + r""" + + Given K and F, compute the value of deterministic entropy, which + is + + .. math:: + + \sum_t \beta^t x_t' K'K x_t` + + with + + .. math:: + + x_{t+1} = (A - BF + CK) x_t + + Parameters + ---------- + F : array_like(float, ndim=2) + The policy function, a k x n array + K : array_like(float, ndim=2) + The worst case matrix, a j x n array + x0 : array_like(float, ndim=1) + The initial condition for state + + Returns + ------- + e : scalar(int) + The deterministic entropy + + """ + H0 = np.dot(K.T, K) + C0 = np.zeros((self.n, 1)) + A0 = self.A - np.dot(self.B, F) + np.dot(self.C, K) + e = var_quadratic_sum(A0, C0, H0, self.beta, x0) + + return e + + def evaluate_F(self, F): + """ + Given a fixed policy F, with the interpretation :math:`u = -F x`, this + function computes the matrix :math:`P_F` and constant :math:`d_F` + associated with discounted cost :math:`J_F(x) = x' P_F x + d_F` + + Parameters + ---------- + F : array_like(float, ndim=2) + The policy function, a k x n array + + Returns + ------- + P_F : array_like(float, ndim=2) + Matrix for discounted cost + d_F : scalar(float) + Constant for discounted cost + K_F : array_like(float, ndim=2) + Worst case policy + O_F : array_like(float, ndim=2) + Matrix for discounted entropy + o_F : scalar(float) + Constant for discounted entropy + + """ + # == Simplify names == # + Q, R, A, B, C = self.Q, self.R, self.A, self.B, self.C + beta, theta = self.beta, self.theta + + # == Solve for policies and costs using agent 2's problem == # + K_F, P_F = self.F_to_K(F) + I = np.identity(self.j) + H = inv(I - C.T.dot(P_F.dot(C)) / theta) + d_F = np.log(det(H)) + + # == Compute O_F and o_F == # + AO = np.sqrt(beta) * (A - np.dot(B, F) + np.dot(C, K_F)) + O_F = solve_discrete_lyapunov(AO.T, beta * np.dot(K_F.T, K_F)) + ho = (np.trace(H - 1) - d_F) / 2.0 + tr = np.trace(np.dot(O_F, C.dot(H.dot(C.T)))) + o_F = (ho + beta * tr) / (1 - beta) + + return K_F, P_F, d_F, O_F, o_F diff --git a/quantecon/tests/test_arma.py b/quantecon/tests/test_arma.py index 66dc1cc6e..d0ef20331 100644 --- a/quantecon/tests/test_arma.py +++ b/quantecon/tests/test_arma.py @@ -5,7 +5,7 @@ """ import numpy as np from numpy.testing import assert_array_equal, assert_ -from quantecon import ARMA +from quantecon.arma import ARMA class TestARMA(): diff --git a/quantecon/tests/test_dle.py b/quantecon/tests/test_dle.py index 1d8f494c1..d731a0489 100644 --- a/quantecon/tests/test_dle.py +++ b/quantecon/tests/test_dle.py @@ -4,7 +4,7 @@ import numpy as np from numpy.testing import assert_allclose -from quantecon import DLE +from quantecon.dle import DLE ATOL = 1e-10 diff --git a/quantecon/tests/test_filter.py b/quantecon/tests/test_filter.py index 5bc72b45f..f489a8e79 100644 --- a/quantecon/tests/test_filter.py +++ b/quantecon/tests/test_filter.py @@ -8,7 +8,7 @@ import pandas as pd import numpy as np from numpy.testing import assert_allclose -from quantecon import hamilton_filter +from quantecon.filter import hamilton_filter from quantecon.tests.util import get_data_dir def test_hamilton_filter(): diff --git a/quantecon/tests/test_graph_tools.py b/quantecon/tests/test_graph_tools.py index 5cde56c6f..5c05a0917 100644 --- a/quantecon/tests/test_graph_tools.py +++ b/quantecon/tests/test_graph_tools.py @@ -6,7 +6,7 @@ import numpy as np from numpy.testing import assert_array_equal, assert_raises, assert_ -from quantecon import DiGraph, random_tournament_graph +from quantecon.graph_tools import DiGraph, random_tournament_graph def list_of_array_equal(s, t): diff --git a/quantecon/tests/test_gridtools.py b/quantecon/tests/test_gridtools.py index 66bc888e5..938379352 100644 --- a/quantecon/tests/test_gridtools.py +++ b/quantecon/tests/test_gridtools.py @@ -9,7 +9,7 @@ assert_array_equal, assert_equal, assert_, assert_raises ) -from quantecon._gridtools import ( +from quantecon.gridtools import ( cartesian, mlinspace, _repeat_1d, simplex_grid, simplex_index, num_compositions, num_compositions_jit, cartesian_nearest_index ) diff --git a/quantecon/tests/test_ivp.py b/quantecon/tests/test_ivp.py index e54054d1e..f33edbf4b 100644 --- a/quantecon/tests/test_ivp.py +++ b/quantecon/tests/test_ivp.py @@ -5,7 +5,7 @@ import numpy as np -from quantecon import IVP +from quantecon.ivp import IVP # use the Solow Model with Cobb-Douglas production as test case def solow_model(t, k, g, n, s, alpha, delta): diff --git a/quantecon/tests/test_kalman.py b/quantecon/tests/test_kalman.py index d10f515f3..8e864efe0 100644 --- a/quantecon/tests/test_kalman.py +++ b/quantecon/tests/test_kalman.py @@ -4,7 +4,8 @@ """ import numpy as np from numpy.testing import assert_allclose -from quantecon import LinearStateSpace, Kalman +from quantecon.lss import LinearStateSpace +from quantecon.kalman import Kalman class TestKalman: diff --git a/quantecon/tests/test_lqcontrol.py b/quantecon/tests/test_lqcontrol.py index bf9c3dc29..8513435ae 100644 --- a/quantecon/tests/test_lqcontrol.py +++ b/quantecon/tests/test_lqcontrol.py @@ -5,7 +5,7 @@ import numpy as np from numpy.testing import assert_allclose, assert_raises from numpy import dot -from quantecon import LQ, LQMarkov +from quantecon.lqcontrol import LQ, LQMarkov class TestLQControl: diff --git a/quantecon/tests/test_lqnash.py b/quantecon/tests/test_lqnash.py index 2470a7c96..9f224b20c 100644 --- a/quantecon/tests/test_lqnash.py +++ b/quantecon/tests/test_lqnash.py @@ -4,7 +4,8 @@ """ import numpy as np from numpy.testing import assert_allclose -from quantecon import nnash, LQ +from quantecon.lqnash import nnash +from quantecon.lqcontrol import LQ class TestLQNash: diff --git a/quantecon/tests/test_lss.py b/quantecon/tests/test_lss.py index 771b4e1f0..c2fa8ed7b 100644 --- a/quantecon/tests/test_lss.py +++ b/quantecon/tests/test_lss.py @@ -4,7 +4,7 @@ """ import numpy as np from numpy.testing import assert_allclose, assert_, assert_raises -from quantecon import LinearStateSpace +from quantecon.lss import LinearStateSpace class TestLinearStateSpace: diff --git a/quantecon/tests/test_lyapunov.py b/quantecon/tests/test_lyapunov.py index d22f9cba5..61e9cefa2 100644 --- a/quantecon/tests/test_lyapunov.py +++ b/quantecon/tests/test_lyapunov.py @@ -4,7 +4,7 @@ """ import numpy as np from numpy.testing import assert_allclose -from quantecon import solve_discrete_lyapunov +from quantecon.matrix_eqn import solve_discrete_lyapunov def test_dlyap_simple_ones(): diff --git a/quantecon/tests/test_matrix_eqn.py b/quantecon/tests/test_matrix_eqn.py index bf1e94250..e2bc911d8 100644 --- a/quantecon/tests/test_matrix_eqn.py +++ b/quantecon/tests/test_matrix_eqn.py @@ -4,7 +4,7 @@ """ import numpy as np from numpy.testing import assert_allclose -from quantecon import solve_discrete_lyapunov +from quantecon import matrix_eqn as qme def test_solve_discrete_lyapunov_zero(): @@ -12,7 +12,7 @@ def test_solve_discrete_lyapunov_zero(): A = np.eye(4) * .95 B = np.zeros((4, 4)) - X = solve_discrete_lyapunov(A, B) + X = qme.solve_discrete_lyapunov(A, B) assert_allclose(X, np.zeros((4, 4))) @@ -22,7 +22,7 @@ def test_solve_discrete_lyapunov_B(): A = np.ones((2, 2)) * .5 B = np.array([[.5, -.5], [-.5, .5]]) - X = solve_discrete_lyapunov(A, B) + X = qme.solve_discrete_lyapunov(A, B) assert_allclose(B, X) @@ -32,7 +32,8 @@ def test_solve_discrete_lyapunov_complex(): [ 1, 0]]) B = np.eye(2) - X = solve_discrete_lyapunov(A, B) + X = qme.solve_discrete_lyapunov(A, B) assert_allclose(np.dot(np.dot(A, X), A.conj().transpose()) - X, -B, atol=1e-15) + diff --git a/quantecon/tests/test_quadsum.py b/quantecon/tests/test_quadsum.py index 60011b6c6..24ff94d91 100644 --- a/quantecon/tests/test_quadsum.py +++ b/quantecon/tests/test_quadsum.py @@ -4,7 +4,7 @@ """ import numpy as np from numpy.testing import assert_allclose -from quantecon import var_quadratic_sum, m_quadratic_sum +from quantecon.quadsums import var_quadratic_sum, m_quadratic_sum def test_var_simplesum(): diff --git a/quantecon/tests/test_rank_nullspace.py b/quantecon/tests/test_rank_nullspace.py index 467c91dfa..3a5ccbd17 100644 --- a/quantecon/tests/test_rank_nullspace.py +++ b/quantecon/tests/test_rank_nullspace.py @@ -4,7 +4,7 @@ """ import numpy as np from numpy.linalg import matrix_rank as np_rank -from quantecon import rank_est, nullspace +from quantecon.rank_nullspace import rank_est, nullspace class TestRankNullspace: diff --git a/quantecon/tests/test_ricatti.py b/quantecon/tests/test_ricatti.py index fff7139fc..6247be326 100644 --- a/quantecon/tests/test_ricatti.py +++ b/quantecon/tests/test_ricatti.py @@ -4,7 +4,7 @@ """ import numpy as np from numpy.testing import assert_allclose, assert_raises -from quantecon import solve_discrete_riccati +from quantecon.matrix_eqn import solve_discrete_riccati import pytest diff --git a/quantecon/tests/test_robustlq.py b/quantecon/tests/test_robustlq.py index 10e4d6770..fc2e8a265 100644 --- a/quantecon/tests/test_robustlq.py +++ b/quantecon/tests/test_robustlq.py @@ -4,8 +4,8 @@ """ import numpy as np from numpy.testing import assert_allclose, assert_ -from quantecon import LQ -from quantecon import RBLQ +from quantecon.lqcontrol import LQ +from quantecon.robustlq import RBLQ class TestRBLQControl: